Merge remote-tracking branch 'upstream/develop' into hs/upload-limits
This commit is contained in:
commit
5a72a5863f
232 changed files with 13386 additions and 11189 deletions
|
@ -64,7 +64,9 @@ export default class ContextualMenu extends React.Component {
|
|||
// The component to render as the context menu
|
||||
elementClass: PropTypes.element.isRequired,
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func
|
||||
windowResize: PropTypes.func,
|
||||
// method to close menu
|
||||
closeMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -73,6 +75,7 @@ export default class ContextualMenu extends React.Component {
|
|||
contextMenuRect: null,
|
||||
};
|
||||
|
||||
this.onContextMenu = this.onContextMenu.bind(this);
|
||||
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
|
||||
}
|
||||
|
||||
|
@ -85,6 +88,28 @@ export default class ContextualMenu extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onContextMenu(e) {
|
||||
if (this.props.closeMenu) {
|
||||
this.props.closeMenu();
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||
setImmediate(() => {
|
||||
const clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(
|
||||
'contextmenu', true, true, window, 0,
|
||||
0, 0, x, y, false, false,
|
||||
false, false, 0, null,
|
||||
);
|
||||
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
|
@ -195,7 +220,8 @@ export default class ContextualMenu extends React.Component {
|
|||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu} /> }
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background"
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -480,7 +480,7 @@ export default React.createClass({
|
|||
group_id: groupId,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
willDoOnboarding = true;
|
||||
}
|
||||
this.setState({
|
||||
|
@ -723,6 +723,11 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_onJoinClick: async function() {
|
||||
if (this._matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({membershipBusy: true});
|
||||
|
||||
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector 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.
|
||||
|
@ -30,10 +30,16 @@ import dis from '../../dispatcher';
|
|||
import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "../../stores/RoomListStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
|
@ -80,6 +86,8 @@ const LoggedInView = React.createClass({
|
|||
return {
|
||||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -97,12 +105,18 @@ const LoggedInView = React.createClass({
|
|||
);
|
||||
this._setStateFromSessionStore();
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
|
@ -142,6 +156,56 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSync: function(syncState, oldSyncState, data) {
|
||||
const oldErrCode = this.state.syncErrorData && this.state.syncErrorData.error && this.state.syncErrorData.error.errcode;
|
||||
const newErrCode = data && data.error && data.error.errcode;
|
||||
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
|
||||
|
||||
if (syncState === 'ERROR') {
|
||||
this.setState({
|
||||
syncErrorData: data,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
syncErrorData: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
|
||||
_updateServerNoticeEvents: async function() {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (ev) pinnedEvents.push(ev);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
|
@ -255,20 +319,46 @@ const LoggedInView = React.createClass({
|
|||
), true);
|
||||
},
|
||||
|
||||
_onClick: function(ev) {
|
||||
_onMouseDown: function(ev) {
|
||||
// When the panels are disabled, clicking on them results in a mouse event
|
||||
// which bubbles to certain elements in the tree. When this happens, close
|
||||
// any settings page that is currently open (user/room/group).
|
||||
if (this.props.leftDisabled &&
|
||||
this.props.rightDisabled &&
|
||||
(
|
||||
ev.target.className === 'mx_MatrixChat' ||
|
||||
ev.target.className === 'mx_MatrixChat_middlePanel' ||
|
||||
ev.target.className === 'mx_RoomView'
|
||||
)
|
||||
) {
|
||||
if (this.props.leftDisabled && this.props.rightDisabled) {
|
||||
const targetClasses = new Set(ev.target.className.split(' '));
|
||||
if (
|
||||
targetClasses.has('mx_MatrixChat') ||
|
||||
targetClasses.has('mx_MatrixChat_middlePanel') ||
|
||||
targetClasses.has('mx_RoomView')
|
||||
) {
|
||||
this.setState({
|
||||
mouseDown: {
|
||||
x: ev.pageX,
|
||||
y: ev.pageY,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onMouseUp: function(ev) {
|
||||
if (!this.state.mouseDown) return;
|
||||
|
||||
const deltaX = ev.pageX - this.state.mouseDown.x;
|
||||
const deltaY = ev.pageY - this.state.mouseDown.y;
|
||||
const distance = Math.sqrt((deltaX * deltaX) + (deltaY + deltaY));
|
||||
const maxRadius = 5; // People shouldn't be straying too far, hopefully
|
||||
|
||||
// Note: we track how far the user moved their mouse to help
|
||||
// combat against https://github.com/vector-im/riot-web/issues/7158
|
||||
|
||||
if (distance < maxRadius) {
|
||||
// This is probably a real click, and not a drag
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
|
||||
// Always clear the mouseDown state to ensure we don't accidentally
|
||||
// use stale values due to the mouseDown checks.
|
||||
this.setState({mouseDown: null});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -286,6 +376,7 @@ const LoggedInView = React.createClass({
|
|||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let page_element;
|
||||
let right_panel = '';
|
||||
|
@ -368,9 +459,26 @@ const LoggedInView = React.createClass({
|
|||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
const isGuest = this.props.matrixClient.isGuest();
|
||||
if (this.props.showCookieBar &&
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
|
@ -396,7 +504,7 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
|
|
|
@ -23,7 +23,7 @@ import PropTypes from 'prop-types';
|
|||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import DecryptionFailureTracker from "../../DecryptionFailureTracker";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
@ -45,6 +45,12 @@ import createRoom from "../../createRoom";
|
|||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
|
||||
// Disable warnings for now: we use deprecated bluebird functions
|
||||
// and need to migrate, but they spam the console with warnings.
|
||||
Promise.config({warnings: false});
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
const VIEWS = {
|
||||
|
@ -178,6 +184,8 @@ export default React.createClass({
|
|||
// When showing Modal dialogs we need to set aria-hidden on the root app element
|
||||
// and disable it when there are no dialogs
|
||||
hideToSRUsers: false,
|
||||
|
||||
syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
|
||||
};
|
||||
return s;
|
||||
},
|
||||
|
@ -282,6 +290,14 @@ export default React.createClass({
|
|||
register_hs_url: paramHs,
|
||||
});
|
||||
}
|
||||
// Set a default IS with query param `is_url`
|
||||
const paramIs = this.props.startingFragmentQueryParams.is_url;
|
||||
if (paramIs) {
|
||||
console.log('Setting register_is_url ', paramIs);
|
||||
this.setState({
|
||||
register_is_url: paramIs,
|
||||
});
|
||||
}
|
||||
|
||||
// a thing to call showScreen with once login completes. this is kept
|
||||
// outside this.state because updating it should never trigger a
|
||||
|
@ -471,7 +487,7 @@ export default React.createClass({
|
|||
action: 'do_after_sync_prepared',
|
||||
deferred_action: payload,
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -479,7 +495,11 @@ export default React.createClass({
|
|||
case 'logout':
|
||||
Lifecycle.logout();
|
||||
break;
|
||||
case 'require_registration':
|
||||
startAnyRegistrationFlow(payload);
|
||||
break;
|
||||
case 'start_registration':
|
||||
// This starts the full registration flow
|
||||
this._startRegistration(payload.params || {});
|
||||
break;
|
||||
case 'start_login':
|
||||
|
@ -945,7 +965,7 @@ export default React.createClass({
|
|||
});
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'view_set_mxid',
|
||||
action: 'require_registration',
|
||||
// If the set_mxid dialog is cancelled, view /home because if the browser
|
||||
// was pointing at /user/@someone:domain?action=chat, the URL needs to be
|
||||
// reset so that they can revisit /user/.. // (and trigger
|
||||
|
@ -1132,7 +1152,7 @@ export default React.createClass({
|
|||
*
|
||||
* @param {string} teamToken
|
||||
*/
|
||||
_onLoggedIn: function(teamToken) {
|
||||
_onLoggedIn: async function(teamToken) {
|
||||
this.setState({
|
||||
view: VIEWS.LOGGED_IN,
|
||||
});
|
||||
|
@ -1145,12 +1165,17 @@ export default React.createClass({
|
|||
this._is_registered = false;
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
createRoom({
|
||||
const roomId = await createRoom({
|
||||
dmUserId: this.props.config.welcomeUserId,
|
||||
// Only view the welcome user if we're NOT looking at a room
|
||||
andView: !this.state.currentRoomId,
|
||||
});
|
||||
return;
|
||||
// if successful, return because we're already
|
||||
// viewing the welcomeUserId room
|
||||
// else, if failed, fall through to view_home_page
|
||||
if (roomId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The user has just logged in after registering
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
|
@ -1232,13 +1257,23 @@ export default React.createClass({
|
|||
return self._loggedInView.child.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
cli.on('sync', function(state, prevState, data) {
|
||||
// LifecycleStore and others cannot directly subscribe to matrix client for
|
||||
// events because flux only allows store state changes during flux dispatches.
|
||||
// So dispatch directly from here. Ideally we'd use a SyncStateStore that
|
||||
// would do this dispatch and expose the sync state itself (by listening to
|
||||
// its own dispatch).
|
||||
dis.dispatch({action: 'sync_state', prevState, state});
|
||||
|
||||
if (state === "ERROR" || state === "RECONNECTING") {
|
||||
if (data.error instanceof Matrix.InvalidStoreError) {
|
||||
Lifecycle.handleInvalidStoreError(data.error);
|
||||
}
|
||||
self.setState({syncError: data.error || true});
|
||||
} else if (self.state.syncError) {
|
||||
self.setState({syncError: null});
|
||||
}
|
||||
|
||||
self.updateStatusIndicator(state, prevState);
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
return;
|
||||
|
@ -1262,6 +1297,7 @@ export default React.createClass({
|
|||
}, true);
|
||||
});
|
||||
cli.on('Session.logged_out', function(call) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
|
||||
title: _t('Signed Out'),
|
||||
|
@ -1304,9 +1340,20 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
const dft = new DecryptionFailureTracker((failure) => {
|
||||
// TODO: Pass reason for failure as third argument to trackEvent
|
||||
Analytics.trackEvent('E2E', 'Decryption failure');
|
||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
switch (errorCode) {
|
||||
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
|
||||
return 'olm_keys_not_sent_error';
|
||||
case 'OLM_UNKNOWN_MESSAGE_INDEX':
|
||||
return 'olm_index_error';
|
||||
case undefined:
|
||||
return 'unexpected_error';
|
||||
default:
|
||||
return 'unspecified_error';
|
||||
}
|
||||
});
|
||||
|
||||
// Shelved for later date when we have time to think about persisting history of
|
||||
|
@ -1317,7 +1364,7 @@ export default React.createClass({
|
|||
|
||||
// When logging out, stop tracking failures and destroy state
|
||||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e) => dft.eventDecrypted(e));
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
|
@ -1406,7 +1453,7 @@ export default React.createClass({
|
|||
} else if (screen == 'start') {
|
||||
this.showScreen('home');
|
||||
dis.dispatch({
|
||||
action: 'view_set_mxid',
|
||||
action: 'require_registration',
|
||||
});
|
||||
} else if (screen == 'directory') {
|
||||
dis.dispatch({
|
||||
|
@ -1698,10 +1745,14 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
if (this.state.view === VIEWS.LOGGED_IN) {
|
||||
// store errors stop the client syncing and require user intervention, so we'll
|
||||
// be showing a dialog. Don't show anything else.
|
||||
const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError;
|
||||
|
||||
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
|
||||
// latter is set via the dispatcher). If we don't yet have a `page_type`,
|
||||
// keep showing the spinner for now.
|
||||
if (this.state.ready && this.state.page_type) {
|
||||
if (this.state.ready && this.state.page_type && !isStoreError) {
|
||||
/* for now, we stuff the entirety of our props and state into the LoggedInView.
|
||||
* we should go through and figure out what we actually need to pass down, as well
|
||||
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||
|
@ -1722,8 +1773,15 @@ export default React.createClass({
|
|||
} else {
|
||||
// we think we are logged in, but are still waiting for the /sync to complete
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
let errorBox;
|
||||
if (this.state.syncError && !isStoreError) {
|
||||
errorBox = <div className="mx_MatrixChat_syncError">
|
||||
{messageForSyncError(this.state.syncError)}
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
{errorBox}
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||
{ _t('Logout') }
|
||||
|
|
|
@ -542,7 +542,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
// Receipts are objects which have a 'roomMember' and 'ts'.
|
||||
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
|
||||
_getReadReceiptsForEvent: function(event) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
|
@ -560,10 +560,8 @@ module.exports = React.createClass({
|
|||
return; // ignore ignored users
|
||||
}
|
||||
const member = room.getMember(r.userId);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
}
|
||||
receipts.push({
|
||||
userId: r.userId,
|
||||
roomMember: member,
|
||||
ts: r.data ? r.data.ts : 0,
|
||||
});
|
||||
|
|
|
@ -51,6 +51,7 @@ class HeaderButton extends React.Component {
|
|||
|
||||
return <AccessibleButton
|
||||
aria-label={this.props.title}
|
||||
aria-expanded={this.props.isHighlighted}
|
||||
title={this.props.title}
|
||||
className="mx_RightPanel_headerButton"
|
||||
onClick={this.onClick} >
|
||||
|
@ -160,7 +161,7 @@ module.exports = React.createClass({
|
|||
|
||||
onInviteButtonClick: function() {
|
||||
if (this.context.matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -186,6 +187,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === this.Phase.RoomMemberList && member.roomId === this.props.roomId) {
|
||||
this._delayedUpdate();
|
||||
|
@ -280,7 +284,7 @@ module.exports = React.createClass({
|
|||
const room = cli.getRoom(this.props.roomId);
|
||||
let isUserInRoom;
|
||||
if (room) {
|
||||
const numMembers = room.getJoinedMembers().length;
|
||||
const numMembers = room.getJoinedMemberCount();
|
||||
membersTitle = _t('%(count)s Members', { count: numMembers });
|
||||
membersBadge = <div title={membersTitle}>{ formatCount(numMembers) }</div>;
|
||||
isUserInRoom = room.hasMembershipState(this.context.matrixClient.credentials.userId, 'join');
|
||||
|
@ -342,11 +346,11 @@ module.exports = React.createClass({
|
|||
// being put in the RoomHeader or GroupView header, so only show the minimise
|
||||
// button on these 2 screens or you won't be able to re-expand the panel.
|
||||
headerButtons.push(
|
||||
<div className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
|
||||
<AccessibleButton className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
|
||||
title={_t("Hide panel")} aria-label={_t("Hide panel")} onClick={this.onCollapseClick}
|
||||
>
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" />
|
||||
</div>,
|
||||
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -354,7 +354,7 @@ module.exports = React.createClass({
|
|||
// to the directory.
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
if (!room.world_readable && !room.guest_can_join) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector 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.
|
||||
|
@ -18,13 +18,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -106,6 +108,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
unsentMessages: getUnsentMessages(this.props.room),
|
||||
};
|
||||
|
@ -133,12 +136,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSyncStateChange: function(state, prevState) {
|
||||
onSyncStateChange: function(state, prevState, data) {
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
syncState: state,
|
||||
syncStateData: data,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -157,10 +161,12 @@ module.exports = React.createClass({
|
|||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
|
@ -188,7 +194,7 @@ module.exports = React.createClass({
|
|||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize: function() {
|
||||
if (this.state.syncState === "ERROR" ||
|
||||
if (this._shouldShowConnectionError() ||
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
|
@ -217,14 +223,15 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
if (!this.props.atEndOfLiveTimeline) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
<AccessibleButton className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
<img src="img/scrolldown.svg" width="24" height="24"
|
||||
alt={_t("Scroll to bottom of page")}
|
||||
title={_t("Scroll to bottom of page")} />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -235,7 +242,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.syncState === "ERROR") {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -282,6 +289,21 @@ module.exports = React.createClass({
|
|||
return avatars;
|
||||
},
|
||||
|
||||
_shouldShowConnectionError: function() {
|
||||
// no conn bar trumps unread count since you can't get unread messages
|
||||
// without a connection! (technically may already have some but meh)
|
||||
// It also trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||
// if it's a resource limit exceeded error: those are shown in the top bar.
|
||||
const errorIsMauError = Boolean(
|
||||
this.state.syncStateData &&
|
||||
this.state.syncStateData.error &&
|
||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED'
|
||||
);
|
||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||
},
|
||||
|
||||
_getUnsentMessageContent: function() {
|
||||
const unsentMessages = this.state.unsentMessages;
|
||||
if (!unsentMessages.length) return null;
|
||||
|
@ -305,7 +327,43 @@ module.exports = React.createClass({
|
|||
},
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
let consentError = null;
|
||||
let resourceLimitError = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||
consentError = m.error;
|
||||
break;
|
||||
} else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
resourceLimitError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _t(
|
||||
"You can't send any messages until you review and agree to " +
|
||||
"<consentLink>our terms and conditions</consentLink>.",
|
||||
{},
|
||||
{
|
||||
'consentLink': (sub) =>
|
||||
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else if (resourceLimitError) {
|
||||
title = messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
});
|
||||
} else if (
|
||||
unsentMessages.length === 1 &&
|
||||
unsentMessages[0].error &&
|
||||
unsentMessages[0].error.data &&
|
||||
|
@ -328,12 +386,14 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -342,19 +402,17 @@ module.exports = React.createClass({
|
|||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
// no conn bar trumps unread count since you can't get unread messages
|
||||
// without a connection! (technically may already have some but meh)
|
||||
// It also trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
if (this.state.syncState === "ERROR") {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -428,7 +486,9 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomStatusBar_indicator">
|
||||
{ indicator }
|
||||
</div>
|
||||
{ content }
|
||||
<div role="alert">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -15,56 +16,53 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var classNames = require('classnames');
|
||||
var sdk = require('../../index');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { _t } from '../../languageHandler';
|
||||
var dis = require('../../dispatcher');
|
||||
var Unread = require('../../Unread');
|
||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
var RoomNotifs = require('../../RoomNotifs');
|
||||
var FormattingUtils = require('../../utils/FormattingUtils');
|
||||
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
import Modal from '../../Modal';
|
||||
import dis from '../../dispatcher';
|
||||
import Unread from '../../Unread';
|
||||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
var debug = false;
|
||||
const debug = false;
|
||||
|
||||
const TRUNCATE_AT = 10;
|
||||
|
||||
var RoomSubList = React.createClass({
|
||||
const RoomSubList = React.createClass({
|
||||
displayName: 'RoomSubList',
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
tagName: React.PropTypes.string,
|
||||
editable: React.PropTypes.bool,
|
||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
|
||||
order: React.PropTypes.string.isRequired,
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: React.PropTypes.bool,
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
startAsHidden: React.PropTypes.bool,
|
||||
showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: React.PropTypes.func,
|
||||
alwaysShowHeader: React.PropTypes.bool,
|
||||
incomingCall: React.PropTypes.object,
|
||||
onShowMoreRooms: React.PropTypes.func,
|
||||
searchFilter: React.PropTypes.string,
|
||||
emptyContent: React.PropTypes.node, // content shown if the list is empty
|
||||
headerItems: React.PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
|
||||
startAsHidden: PropTypes.bool,
|
||||
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: PropTypes.func,
|
||||
alwaysShowHeader: PropTypes.bool,
|
||||
incomingCall: PropTypes.object,
|
||||
onShowMoreRooms: PropTypes.func,
|
||||
searchFilter: PropTypes.string,
|
||||
emptyContent: PropTypes.node, // content shown if the list is empty
|
||||
headerItems: PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
showEmpty: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -77,10 +75,13 @@ var RoomSubList = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {}, // NOP
|
||||
onShowMoreRooms: function() {}, // NOP
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
onShowMoreRooms: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
showEmpty: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -115,7 +116,7 @@ var RoomSubList = React.createClass({
|
|||
// The header is collapsable if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
var stuck = this.refs.header.dataset.stuck;
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
||||
return true;
|
||||
} else {
|
||||
|
@ -141,12 +142,12 @@ var RoomSubList = React.createClass({
|
|||
onClick: function(ev) {
|
||||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
var isHidden = !this.state.hidden;
|
||||
this.setState({ hidden : isHidden });
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden});
|
||||
|
||||
if (isHidden) {
|
||||
// as good a way as any to reset the truncate state
|
||||
this.setState({ truncateAt : TRUNCATE_AT });
|
||||
this.setState({truncateAt: TRUNCATE_AT});
|
||||
}
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
|
@ -161,7 +162,7 @@ var RoomSubList = React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
|
||||
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -171,17 +172,17 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowMentionBadge: function(roomNotifState) {
|
||||
return roomNotifState != RoomNotifs.MUTE;
|
||||
return roomNotifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total up all the notification counts from the rooms
|
||||
*
|
||||
* @param {Number} If supplied will only total notifications for rooms outside the truncation number
|
||||
* @param {Number} truncateAt If supplied will only total notifications for rooms outside the truncation number
|
||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||
*/
|
||||
roomNotificationCount: function(truncateAt) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
|
@ -189,9 +190,9 @@ var RoomSubList = React.createClass({
|
|||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
if (truncateAt === undefined || index >= truncateAt) {
|
||||
var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
var highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
var notificationCount = room.getUnreadNotificationCount();
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
||||
|
@ -240,38 +241,83 @@ var RoomSubList = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onNotifBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// find first room which has notifications and switch to it
|
||||
for (const room of this.state.sortedList) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
if (notifBadges || mentionBadges) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onInviteBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// switch to first room in sortedList as that'll be the top of the list for the user
|
||||
if (this.state.sortedList && this.state.sortedList.length > 0) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.sortedList[0].roomId,
|
||||
});
|
||||
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
|
||||
// Group Invites are different in that they are all extra tiles and not rooms
|
||||
// XXX: this is a horrible special case because Group Invite sublist is a hack
|
||||
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.extraTiles[0].props.group.groupId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const subListNotifications = this.roomNotificationCount();
|
||||
const subListNotifCount = subListNotifications[0];
|
||||
const subListNotifHighlight = subListNotifications[1];
|
||||
|
||||
var subListNotifications = this.roomNotificationCount();
|
||||
var subListNotifCount = subListNotifications[0];
|
||||
var subListNotifHighlight = subListNotifications[1];
|
||||
const totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
const roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
var roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var chevronClasses = classNames({
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
|
||||
var badge;
|
||||
let badge;
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses}>{ FormattingUtils.formatCount(subListNotifCount) }</div>;
|
||||
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>;
|
||||
} else if (this.props.isInvite) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses}>!</div>;
|
||||
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
|
||||
}
|
||||
|
||||
// When collapsed, allow a long hover on the header to show user
|
||||
// the full tag name and room count
|
||||
var title;
|
||||
let title;
|
||||
if (this.props.collapsed) {
|
||||
title = this.props.label;
|
||||
if (roomCount !== '') {
|
||||
|
@ -279,63 +325,66 @@ var RoomSubList = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var incomingCall;
|
||||
let incomingCall;
|
||||
if (this.props.incomingCall) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
// Check if the incoming call is for this section
|
||||
var incomingCallRoom = this.props.list.filter(function(room) {
|
||||
const incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
if (incomingCallRoom.length === 1) {
|
||||
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall = <IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={ this.props.incomingCall }/>;
|
||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall =
|
||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
}
|
||||
|
||||
var tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{ this.props.collapsed ? '' : this.props.label }
|
||||
<div className="mx_RoomSubList_roomCount">{ roomCount }</div>
|
||||
<div className={chevronClasses}></div>
|
||||
{ badge }
|
||||
{ incomingCall }
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
||||
<AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{this.props.collapsed ? '' : this.props.label}
|
||||
<div className="mx_RoomSubList_roomCount">{roomCount}</div>
|
||||
<div className={chevronClasses} />
|
||||
{badge}
|
||||
{incomingCall}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
var content = <div className="mx_RoomSubList_chevronDown"></div>;
|
||||
let content = <div className="mx_RoomSubList_chevronDown" />;
|
||||
|
||||
var overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
var overflowNotifCount = overflowNotifications[0];
|
||||
var overflowNotifHighlight = overflowNotifications[1];
|
||||
const overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
const overflowNotifCount = overflowNotifications[0];
|
||||
const overflowNotifHighlight = overflowNotifications[1];
|
||||
if (overflowNotifCount && !this.props.collapsed) {
|
||||
content = FormattingUtils.formatCount(overflowNotifCount);
|
||||
}
|
||||
|
||||
var badgeClasses = classNames({
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_moreBadge': true,
|
||||
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
||||
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<div className="mx_RoomSubList_line"></div>
|
||||
<div className="mx_RoomSubList_more">{ _t("more") }</div>
|
||||
<div className={ badgeClasses }>{ content }</div>
|
||||
<div className="mx_RoomSubList_line" />
|
||||
<div className="mx_RoomSubList_more">{_t("more")}</div>
|
||||
<div className={badgeClasses}>{content}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1
|
||||
truncateAt: -1,
|
||||
});
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
|
@ -343,37 +392,51 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
var TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
var label = this.props.collapsed ? null : this.props.label;
|
||||
const TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
let content;
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
|
||||
if (this.props.showEmpty) {
|
||||
// this is new behaviour with still controversial UX in that in hiding RoomSubLists the drop zones for DnD
|
||||
// are also gone so when filtering users can't DnD rooms to some tags but is a lot cleaner otherwise.
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
||||
// if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing
|
||||
if (!this.props.searchFilter && this.props.emptyContent) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
// don't show an empty sublist
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||
var subList;
|
||||
var classes = "mx_RoomSubList";
|
||||
let subList;
|
||||
const classes = "mx_RoomSubList";
|
||||
|
||||
if (!this.state.hidden) {
|
||||
subList = <TruncatedList className={ classes } truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile} >
|
||||
{ content }
|
||||
</TruncatedList>;
|
||||
}
|
||||
else {
|
||||
subList = <TruncatedList className={ classes }>
|
||||
</TruncatedList>;
|
||||
subList = <TruncatedList className={classes} truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{content}
|
||||
</TruncatedList>;
|
||||
} else {
|
||||
subList = <TruncatedList className={classes}>
|
||||
</TruncatedList>;
|
||||
}
|
||||
|
||||
const subListContent = <div>
|
||||
{ this._getHeaderJsx() }
|
||||
{ subList }
|
||||
{this._getHeaderJsx()}
|
||||
{subList}
|
||||
</div>;
|
||||
|
||||
return this.props.editable ?
|
||||
|
@ -381,23 +444,26 @@ var RoomSubList = React.createClass({
|
|||
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
||||
type="draggable-RoomTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}>
|
||||
{ subListContent }
|
||||
{subListContent}
|
||||
</div>
|
||||
) }
|
||||
)}
|
||||
</Droppable> : subListContent;
|
||||
}
|
||||
else {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
} else {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.props.showSpinner) {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList">
|
||||
{ this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined }
|
||||
{ (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }
|
||||
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
||||
{ this.state.hidden ? undefined : content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -45,7 +46,9 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
|||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../utils/WidgetUtils';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -89,6 +92,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const llMembers = MatrixClientPeg.get().hasLazyLoadMembersEnabled();
|
||||
return {
|
||||
room: null,
|
||||
roomId: null,
|
||||
|
@ -99,6 +103,9 @@ module.exports = React.createClass({
|
|||
// Media limits for uploading.
|
||||
mediaConfig: undefined,
|
||||
|
||||
// used to trigger a rerender in TimelinePanel once the members are loaded,
|
||||
// so RR are rendered again (now with the members available), ...
|
||||
membersLoaded: !llMembers,
|
||||
// The event to be scrolled to initially
|
||||
initialEventId: null,
|
||||
// The offset in pixels from the event with which to scroll vertically
|
||||
|
@ -149,12 +156,14 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
this._fetchMediaConfig();
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
},
|
||||
|
||||
_fetchMediaConfig: function(invalidateCache: boolean = false) {
|
||||
|
@ -212,6 +221,8 @@ module.exports = React.createClass({
|
|||
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
||||
};
|
||||
|
||||
if (this.state.editingRoomSettings && !newState.editingRoomSettings) dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
console.log(
|
||||
'RVS update:',
|
||||
|
@ -266,6 +277,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onWidgetEchoStoreUpdate: function() {
|
||||
this.setState({
|
||||
showApps: this._shouldShowApps(this.state.room),
|
||||
});
|
||||
},
|
||||
|
||||
_setupRoom: function(room, roomId, joining, shouldPeek) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
|
@ -322,11 +339,13 @@ module.exports = React.createClass({
|
|||
throw err;
|
||||
}
|
||||
});
|
||||
} else if (room) {
|
||||
//viewing a previously joined room, try to lazy load members
|
||||
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
}
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -342,14 +361,9 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
}
|
||||
|
||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
// any valid widget = show apps
|
||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
||||
if (appsStateEvents[i].getContent().type && appsStateEvents[i].getContent().url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
|
||||
|
||||
return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -370,7 +384,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 &&
|
||||
this.state.room.getJoinedMembers().length == 1 &&
|
||||
this.state.room.getJoinedMemberCount() == 1 &&
|
||||
this.state.room.getLiveTimeline() &&
|
||||
this.state.room.getLiveTimeline().getEvents() &&
|
||||
this.state.room.getLiveTimeline().getEvents().length <= 6) {
|
||||
|
@ -429,8 +443,8 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.membership", this.onRoomMemberMembership);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
|
||||
|
@ -444,6 +458,8 @@ module.exports = React.createClass({
|
|||
this._roomStoreToken.remove();
|
||||
}
|
||||
|
||||
WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate);
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateRoomMembers.cancelPendingCall();
|
||||
|
||||
|
@ -601,6 +617,27 @@ module.exports = React.createClass({
|
|||
this._warnAboutEncryption(room);
|
||||
this._calculatePeekRules(room);
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
this._loadMembersIfJoined(room);
|
||||
},
|
||||
|
||||
_loadMembersIfJoined: async function(room) {
|
||||
// lazy load members if enabled
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
if (!this.unmounted) {
|
||||
this.setState({membersLoaded: true});
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = `Fetching room members for ${room.roomId} failed.` +
|
||||
" Room members will appear incomplete.";
|
||||
console.error(errorMessage);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_warnAboutEncryption: function(room) {
|
||||
|
@ -647,9 +684,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_updatePreviewUrlVisibility: function(room) {
|
||||
_updatePreviewUrlVisibility: function({roomId}) {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -674,19 +713,23 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
|
||||
const type = event.getType();
|
||||
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(this.state.room);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomAccountData: function(event, room) {
|
||||
if (room.roomId == this.state.roomId) {
|
||||
if (event.getType() === "org.matrix.room.color_scheme") {
|
||||
const type = event.getType();
|
||||
if (type === "org.matrix.room.color_scheme") {
|
||||
const color_scheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
} else if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
}
|
||||
}
|
||||
|
@ -704,12 +747,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
this._updateRoomMembers();
|
||||
this._checkIfAlone(this.state.room);
|
||||
},
|
||||
|
||||
onRoomMemberMembership: function(ev, member, oldMembership) {
|
||||
if (member.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
onMyMembership: function(room, membership, oldMembership) {
|
||||
if (room.roomId === this.state.roomId) {
|
||||
this.forceUpdate();
|
||||
this._loadMembersIfJoined(room);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -720,6 +763,7 @@ module.exports = React.createClass({
|
|||
// refresh the conf call notification state
|
||||
this._updateConfCallNotification();
|
||||
this._updateDMState();
|
||||
this._checkIfAlone(this.state.room);
|
||||
}, 500),
|
||||
|
||||
_checkIfAlone: function(room) {
|
||||
|
@ -732,8 +776,8 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const joinedMembers = room.currentState.getMembers().filter((m) => m.membership === "join" || m.membership === "invite");
|
||||
this.setState({isAlone: joinedMembers.length === 1});
|
||||
const joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount();
|
||||
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
||||
},
|
||||
|
||||
_updateConfCallNotification: function() {
|
||||
|
@ -761,40 +805,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_updateDMState() {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (!me || me.membership !== "join") {
|
||||
const room = this.state.room;
|
||||
if (room.getMyMembership() != "join") {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have accepted an invite with is_direct set
|
||||
if (me.events.member.getPrevContent().membership === "invite" &&
|
||||
me.events.member.getPrevContent().is_direct
|
||||
) {
|
||||
// This is a DM with the sender of the invite event (which we assume
|
||||
// preceded the join event)
|
||||
Rooms.setDMRoom(
|
||||
this.state.room.roomId,
|
||||
me.events.member.getUnsigned().prev_sender,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const invitedMembers = this.state.room.getMembersWithMembership("invite");
|
||||
const joinedMembers = this.state.room.getMembersWithMembership("join");
|
||||
|
||||
// There must be one invited member and one joined member
|
||||
if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have sent an invite with is_direct sent
|
||||
const other = invitedMembers[0];
|
||||
if (other &&
|
||||
other.membership === "invite" &&
|
||||
other.events.member.getContent().is_direct
|
||||
) {
|
||||
Rooms.setDMRoom(this.state.room.roomId, other.userId);
|
||||
return;
|
||||
const dmInviter = room.getDMInviter();
|
||||
if (dmInviter) {
|
||||
Rooms.setDMRoom(room.roomId, dmInviter);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -954,7 +971,7 @@ module.exports = React.createClass({
|
|||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -985,7 +1002,7 @@ module.exports = React.createClass({
|
|||
|
||||
injectSticker: function(url, info, text) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1500,6 +1517,7 @@ module.exports = React.createClass({
|
|||
const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.state.roomLoading || this.state.peekLoading) {
|
||||
|
@ -1546,9 +1564,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
if (myMember && myMember.membership == 'invite') {
|
||||
const myMembership = this.state.room.getMyMembership();
|
||||
if (myMembership == 'invite') {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
|
@ -1556,6 +1573,8 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
const inviteEvent = myMember.events.member;
|
||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
|
||||
|
@ -1625,6 +1644,11 @@ module.exports = React.createClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
const showRoomUpgradeBar = (
|
||||
this.state.room.shouldUpgradeToVersion() &&
|
||||
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
|
||||
);
|
||||
|
||||
let aux = null;
|
||||
let hideCancel = false;
|
||||
if (this.state.editingRoomSettings) {
|
||||
|
@ -1636,10 +1660,13 @@ module.exports = React.createClass({
|
|||
} else if (this.state.searching) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||
hideCancel = true;
|
||||
} else if (this.state.showingPinned) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
||||
} else if (!myMember || myMember.membership !== "join") {
|
||||
} else if (myMembership !== "join") {
|
||||
// We do have a room object for this room, but we're not currently in it.
|
||||
// We may have a 3rd party invite to it.
|
||||
var inviterName = undefined;
|
||||
|
@ -1681,7 +1708,7 @@ module.exports = React.createClass({
|
|||
let messageComposer, searchInfo;
|
||||
const canSpeak = (
|
||||
// joined and not showing search results
|
||||
myMember && (myMember.membership == 'join') && !this.state.searchResults
|
||||
myMembership == 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
messageComposer =
|
||||
|
@ -1783,6 +1810,7 @@ module.exports = React.createClass({
|
|||
onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
@ -1817,15 +1845,15 @@ module.exports = React.createClass({
|
|||
oobData={this.props.oobData}
|
||||
editing={this.state.editingRoomSettings}
|
||||
saving={this.state.uploadingRoomSettings}
|
||||
inRoom={myMember && myMember.membership === 'join'}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onSaveClick={this.onSettingsSaveClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
/>
|
||||
{ auxPanel }
|
||||
<div className={fadableSectionClasses}>
|
||||
|
|
|
@ -76,7 +76,7 @@ const TagPanel = React.createClass({
|
|||
|
||||
_onClientSync(syncState, prevState) {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
// Load joined groups
|
||||
|
|
|
@ -1146,10 +1146,11 @@ var TimelinePanel = React.createClass({
|
|||
// of paginating our way through the entire history of the room.
|
||||
const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
|
||||
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
|
||||
// If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with
|
||||
// the HS and fetch the latest events, so we are effectively forward paginating.
|
||||
const forwardPaginating = (
|
||||
this.state.forwardPaginating || this.state.clientSyncState == 'PREPARED'
|
||||
this.state.forwardPaginating ||
|
||||
['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState)
|
||||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
|
|
|
@ -81,6 +81,7 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "VideoView.flipVideoHorizontally" },
|
||||
{ id: "TagPanel.disableTagPanel" },
|
||||
{ id: "enableWidgetScreenshots" },
|
||||
{ id: "RoomSubList.showEmpty" },
|
||||
];
|
||||
|
||||
// These settings must be defined in SettingsStore
|
||||
|
@ -802,7 +803,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("Debug Logs Submission") }</h3>
|
||||
<h3>{ _t("Submit Debug Logs") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>{
|
||||
_t( "If you've submitted a bug via GitHub, debug logs can help " +
|
||||
|
@ -830,9 +831,9 @@ module.exports = React.createClass({
|
|||
<br />
|
||||
{ _t('Privacy is important to us, so we don\'t collect any personal'
|
||||
+ ' or identifiable data for our analytics.') }
|
||||
<div className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
|
||||
<AccessibleButton className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
|
||||
{ _t('Learn more about how we use analytics.') }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -843,8 +844,16 @@ 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 = (e) => {
|
||||
SettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||
const onChange = async (e) => {
|
||||
const checked = e.target.checked;
|
||||
if (featureId === "feature_lazyloading") {
|
||||
const confirmed = await this._onLazyLoadChanging(checked);
|
||||
if (!confirmed) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await SettingsStore.setFeatureEnabled(featureId, checked);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
@ -854,7 +863,7 @@ module.exports = React.createClass({
|
|||
type="checkbox"
|
||||
id={featureId}
|
||||
name={featureId}
|
||||
defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
|
||||
checked={SettingsStore.isFeatureEnabled(featureId)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
|
||||
|
@ -877,6 +886,30 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_onLazyLoadChanging: async function(enabling) {
|
||||
// don't prevent turning LL off when not supported
|
||||
if (enabling) {
|
||||
const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading();
|
||||
if (!supported) {
|
||||
await new Promise((resolve) => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Lazy loading members not supported"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("Lazy loading is not supported by your " +
|
||||
"current homeserver.") }
|
||||
</div>,
|
||||
button: _t("OK"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_renderDeactivateAccount: function() {
|
||||
return <div>
|
||||
<h3>{ _t("Deactivate Account") }</h3>
|
||||
|
@ -888,6 +921,25 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
},
|
||||
|
||||
_renderTermsAndConditionsLinks: function() {
|
||||
if (SdkConfig.get().terms_and_conditions_links) {
|
||||
const tncLinks = [];
|
||||
for (const tncEntry of SdkConfig.get().terms_and_conditions_links) {
|
||||
tncLinks.push(<div key={tncEntry.url}>
|
||||
<a href={tncEntry.url} rel="noopener" target="_blank">{tncEntry.text}</a>
|
||||
</div>);
|
||||
}
|
||||
return <div>
|
||||
<h3>{ _t("Legal") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{tncLinks}
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
_renderClearCache: function() {
|
||||
return <div>
|
||||
<h3>{ _t("Clear Cache") }</h3>
|
||||
|
@ -1013,9 +1065,9 @@ module.exports = React.createClass({
|
|||
_renderWebRtcDeviceSettings: function() {
|
||||
if (this.state.mediaDevices === false) {
|
||||
return (
|
||||
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||
<AccessibleButton element="p" className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||
{ _t('Missing Media Permissions, click here to request.') }
|
||||
</p>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (!this.state.mediaDevices) return;
|
||||
|
||||
|
@ -1182,7 +1234,7 @@ module.exports = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||
<AccessibleButton element="img" src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||
onClick={onRemoveClick} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1207,7 +1259,7 @@ module.exports = React.createClass({
|
|||
onValueChanged={this._onAddEmailEditFinished} />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
<AccessibleButton element="img" src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1276,13 +1328,13 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<AccessibleButton className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<img src="img/cancel.svg"
|
||||
width="15" height="15"
|
||||
className="mx_filterFlipColor"
|
||||
alt={_t("Remove avatar")}
|
||||
title={_t("Remove avatar")} />
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||
|
@ -1343,11 +1395,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t('Access Token:') + ' ' }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
<AccessibleButton element="span" className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() }
|
||||
|
@ -1374,6 +1426,8 @@ module.exports = React.createClass({
|
|||
|
||||
{ this._renderDeactivateAccount() }
|
||||
|
||||
{ this._renderTermsAndConditionsLinks() }
|
||||
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -20,11 +20,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -93,6 +94,13 @@ module.exports = React.createClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
onPasswordLoginError: function(errorText) {
|
||||
this.setState({
|
||||
errorText,
|
||||
loginIncorrect: Boolean(errorText),
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -114,6 +122,30 @@ module.exports = React.createClass({
|
|||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{errorTop}</div>
|
||||
<div className="mx_Login_smallError">{errorDetail}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
errorText = (
|
||||
|
@ -357,6 +389,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
|
|
|
@ -26,9 +26,10 @@ import sdk from '../../../index';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
|
@ -92,6 +93,7 @@ module.exports = React.createClass({
|
|||
doingUIAuth: Boolean(this.props.sessionId),
|
||||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
flows: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -144,11 +146,27 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_replaceClient: function() {
|
||||
_replaceClient: async function() {
|
||||
this._matrixClient = Matrix.createClient({
|
||||
baseUrl: this.state.hsUrl,
|
||||
idBaseUrl: this.state.isUrl,
|
||||
});
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
// This should never succeed since we specified an empty
|
||||
// auth object.
|
||||
console.log("Expecting 401 from register request but got success!");
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 401) {
|
||||
this.setState({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods"),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onFormSubmit: function(formVals) {
|
||||
|
@ -164,7 +182,29 @@ module.exports = React.createClass({
|
|||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
msg = <div>
|
||||
<p>{errorTop}</p>
|
||||
<p>{errorDetail}</p>
|
||||
</div>;
|
||||
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdnAvailable = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
|
@ -281,6 +321,12 @@ module.exports = React.createClass({
|
|||
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||
errMsg = _t('This doesn\'t look like a valid phone number.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_MISSING_EMAIL":
|
||||
errMsg = _t('An email address is required to register on this homeserver.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_MISSING_PHONE_NUMBER":
|
||||
errMsg = _t('A phone number is required to register on this homeserver.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.');
|
||||
break;
|
||||
|
@ -355,7 +401,7 @@ module.exports = React.createClass({
|
|||
poll={true}
|
||||
/>
|
||||
);
|
||||
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||
} else if (this.state.busy || this.state.teamServerBusy || !this.state.flows) {
|
||||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
|
@ -385,6 +431,7 @@ module.exports = React.createClass({
|
|||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
flows={this.state.flows}
|
||||
/>
|
||||
{ serverConfigSection }
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue