merge in develop

This commit is contained in:
Matthew Hodgson
2016-08-04 13:39:47 +01:00
90 changed files with 3640 additions and 1145 deletions

View File

@@ -0,0 +1,110 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var classNames = require('classnames');
var React = require('react');
var ReactDOM = require('react-dom');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container",
propTypes: {
menuWidth: React.PropTypes.number,
menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number,
},
getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = this.ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
},
createMenu: function (Element, props) {
var self = this;
var closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) {
props.onFinished.apply(null, arguments);
}
};
var position = {
top: props.top,
};
var chevronOffset = {
top: props.chevronOffset,
}
var chevron = null;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
position.right = props.right;
}
var className = 'mx_ContextualMenu_wrapper';
var menuClasses = classNames({
'mx_ContextualMenu': true,
'mx_ContextualMenu_left': props.left,
'mx_ContextualMenu_right': !props.left,
});
var menuSize = {};
if (props.menuWidth) {
menuSize.width = props.menuWidth;
}
if (props.menuHeight) {
menuSize.height = props.menuHeight;
}
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
var menu = (
<div className={className} style={position}>
<div className={menuClasses} style={menuSize}>
{chevron}
<Element {...props} onFinished={closeMenu}/>
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
</div>
);
ReactDOM.render(menu, this.getOrCreateContainer());
return {close: closeMenu};
},
};

View File

@@ -24,7 +24,6 @@ var PresetValues = {
Custom: "custom",
};
var q = require('q');
var encryption = require("../../encryption");
var sdk = require('../../index');
module.exports = React.createClass({
@@ -108,17 +107,8 @@ module.exports = React.createClass({
var deferred = cli.createRoom(options);
var response;
if (this.state.encrypt) {
deferred = deferred.then(function(res) {
response = res;
return encryption.enableEncryption(
cli, response.room_id, options.invite
);
}).then(function() {
return q(response) }
);
// TODO
}
this.setState({

View File

@@ -21,7 +21,7 @@ var Favico = require('favico.js');
var MatrixClientPeg = require("../../MatrixClientPeg");
var SdkConfig = require("../../SdkConfig");
var Notifier = require("../../Notifier");
var ContextualMenu = require("../../ContextualMenu");
var ContextualMenu = require("./ContextualMenu");
var RoomListSorter = require("../../RoomListSorter");
var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence");
@@ -37,6 +37,7 @@ var sdk = require('../../index');
var MatrixTools = require('../../MatrixTools');
var linkifyMatrix = require("../../linkify-matrix");
var KeyCode = require('../../KeyCode');
var Lifecycle = require('../../Lifecycle');
var createRoom = require("../../createRoom");
@@ -109,10 +110,14 @@ module.exports = React.createClass({
return window.localStorage.getItem("mx_hs_url");
}
else {
return this.props.config.default_hs_url || "https://matrix.org";
return this.getDefaultHsUrl();
}
},
getDefaultHsUrl() {
return this.props.config.default_hs_url || "https://matrix.org";
},
getFallbackHsUrl: function() {
return this.props.config.fallback_hs_url;
},
@@ -127,16 +132,31 @@ module.exports = React.createClass({
return window.localStorage.getItem("mx_is_url");
}
else {
return this.props.config.default_is_url || "https://vector.im"
return this.getDefaultIsUrl();
}
},
getDefaultIsUrl() {
return this.props.config.default_is_url || "https://vector.im";
},
componentWillMount: function() {
SdkConfig.put(this.props.config);
this.favicon = new Favico({animation: 'none'});
// Stashed guest credentials if the user logs out
// whilst logged in as a guest user (so they can change
// their mind & log back in)
this.guestCreds = null;
if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
}
},
componentDidMount: function() {
let clientStarted = false;
this._autoRegisterAsGuest = false;
if (this.props.enableGuest) {
if (!this.getCurrentHsUrl()) {
@@ -150,13 +170,14 @@ module.exports = React.createClass({
this.props.startingQueryParams.guest_access_token)
{
this._autoRegisterAsGuest = false;
this.onLoggedIn({
Lifecycle.setLoggedIn({
userId: this.props.startingQueryParams.guest_user_id,
accessToken: this.props.startingQueryParams.guest_access_token,
homeserverUrl: this.props.config.default_hs_url,
identityServerUrl: this.props.config.default_is_url,
homeserverUrl: this.getDefaultHsUrl(),
identityServerUrl: this.getDefaultIsUrl(),
guest: true
});
clientStarted = true;
}
else {
this._autoRegisterAsGuest = true;
@@ -168,7 +189,9 @@ module.exports = React.createClass({
// Don't auto-register as a guest. This applies if you refresh the page on a
// logged in client THEN hit the Sign Out button.
this._autoRegisterAsGuest = false;
this.startMatrixClient();
if (!clientStarted) {
Lifecycle.startMatrixClient();
}
}
this.focusComposer = false;
// scrollStateMap is a map from room id to the scroll state returned by
@@ -223,7 +246,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().registerGuest().done(function(creds) {
console.log("Registered as guest: %s", creds.user_id);
self._setAutoRegisterAsGuest(false);
self.onLoggedIn({
Lifecycle.setLoggedIn({
userId: creds.user_id,
accessToken: creds.access_token,
homeserverUrl: hsUrl,
@@ -254,34 +277,10 @@ module.exports = React.createClass({
var self = this;
switch (payload.action) {
case 'logout':
var guestCreds;
if (MatrixClientPeg.get().isGuest()) {
guestCreds = { // stash our guest creds so we can backout if needed
userId: MatrixClientPeg.get().credentials.userId,
accessToken: MatrixClientPeg.get().getAccessToken(),
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(),
guest: true
}
this.guestCreds = MatrixClientPeg.getCredentials();
}
if (window.localStorage) {
var hsUrl = this.getCurrentHsUrl();
var isUrl = this.getCurrentIsUrl();
window.localStorage.clear();
// preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
window.localStorage.setItem("mx_hs_url", hsUrl);
window.localStorage.setItem("mx_is_url", isUrl);
}
this._stopMatrixClient();
this.notifyNewScreen('login');
this.replaceState({
logged_in: false,
ready: false,
guestCreds: guestCreds,
});
Lifecycle.logout();
break;
case 'start_registration':
var newState = payload.params || {};
@@ -307,7 +306,6 @@ module.exports = React.createClass({
if (this.state.logged_in) return;
this.replaceState({
screen: 'login',
guestCreds: this.state.guestCreds,
});
this.notifyNewScreen('login');
break;
@@ -317,17 +315,12 @@ module.exports = React.createClass({
});
break;
case 'start_upgrade_registration':
// stash our guest creds so we can backout if needed
this.guestCreds = MatrixClientPeg.getCredentials();
this.replaceState({
screen: "register",
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
guestAccessToken: MatrixClientPeg.get().getAccessToken(),
guestCreds: { // stash our guest creds so we can backout if needed
userId: MatrixClientPeg.get().credentials.userId,
accessToken: MatrixClientPeg.get().getAccessToken(),
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
identityServerUrl: MatrixClientPeg.get().getIdentityServerUrl(),
guest: true
}
});
this.notifyNewScreen('register');
break;
@@ -349,10 +342,13 @@ module.exports = React.createClass({
var client = MatrixClientPeg.get();
client.loginWithToken(payload.params.loginToken).done(function(data) {
MatrixClientPeg.replaceUsingAccessToken(
client.getHomeserverUrl(), client.getIdentityServerUrl(),
data.user_id, data.access_token
);
MatrixClientPeg.replaceUsingCreds({
homeserverUrl: client.getHomeserverUrl(),
identityServerUrl: client.getIdentityServerUrl(),
userId: data.user_id,
accessToken: data.access_token,
guest: false,
});
self.setState({
screen: undefined,
logged_in: true
@@ -384,7 +380,7 @@ module.exports = React.createClass({
// FIXME: controller shouldn't be loading a view :(
var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader);
var modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then(function() {
modal.close();
@@ -405,10 +401,7 @@ module.exports = React.createClass({
// known to be in (eg. user clicks on a room in the recents panel), supply the ID
// If the user is clicking on a room in the context of the alias being presented
// to them, supply the room alias. If both are supplied, the room ID will be ignored.
this._viewRoom(
payload.room_id, payload.room_alias, payload.show_settings, payload.event_id,
payload.third_party_invite, payload.oob_data
);
this._viewRoom(payload);
break;
case 'view_prev_room':
roomIndexDelta = -1;
@@ -425,7 +418,7 @@ module.exports = React.createClass({
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this._viewRoom(allRooms[roomIndex].roomId);
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
break;
case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
@@ -433,7 +426,7 @@ module.exports = React.createClass({
);
var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) {
this._viewRoom(allRooms[roomIndex].roomId);
this._viewRoom({ room_id: allRooms[roomIndex].roomId });
}
break;
case 'view_user_settings':
@@ -479,6 +472,15 @@ module.exports = React.createClass({
middleOpacity: payload.middleOpacity,
});
break;
case 'on_logged_in':
this._onLoggedIn();
break;
case 'on_logged_out':
this._onLoggedOut();
break;
case 'will_start_client':
this._onWillStartClient();
break;
}
},
@@ -493,39 +495,45 @@ module.exports = React.createClass({
// switch view to the given room
//
// eventId is optional and will cause a switch to the context of that
// particular event.
// @param {Object} thirdPartyInvite Object containing data about the third party
// @param {Object} room_info Object containing data about the room to be joined
// @param {string=} room_info.room_id ID of the room to join. One of room_id or room_alias must be given.
// @param {string=} room_info.room_alias Alias of the room to join. One of room_id or room_alias must be given.
// @param {boolean=} room_info.auto_join If true, automatically attempt to join the room if not already a member.
// @param {boolean=} room_info.show_settings Makes RoomView show the room settings dialog.
// @param {string=} room_info.event_id ID of the event in this room to show: this will cause a switch to the
// context of that particular event.
// @param {Object=} room_info.third_party_invite Object containing data about the third party
// we received to join the room, if any.
// @param {string} thirdPartyInvite.inviteSignUrl 3pid invite sign URL
// @param {string} thirdPartyInvite.invitedwithEmail The email address the invite was sent to
// @param {Object} oob_data Object of additional data about the room
// @param {string=} room_info.third_party_invite.inviteSignUrl 3pid invite sign URL
// @param {string=} room_info.third_party_invite.invitedEmail The email address the invite was sent to
// @param {Object=} room_info.oob_data Object of additional data about the room
// that has been passed out-of-band (eg.
// room name and avatar from an invite email)
_viewRoom: function(roomId, roomAlias, showSettings, eventId, thirdPartyInvite, oob_data) {
_viewRoom: function(room_info) {
// before we switch room, record the scroll state of the current room
this._updateScrollMap();
this.focusComposer = true;
var newState = {
initialEventId: eventId,
highlightedEventId: eventId,
initialEventId: room_info.event_id,
highlightedEventId: room_info.event_id,
initialEventPixelOffset: undefined,
page_type: this.PageTypes.RoomView,
thirdPartyInvite: thirdPartyInvite,
roomOobData: oob_data,
currentRoomAlias: roomAlias,
thirdPartyInvite: room_info.third_party_invite,
roomOobData: room_info.oob_data,
currentRoomAlias: room_info.room_alias,
autoJoin: room_info.auto_join,
};
if (!roomAlias) {
newState.currentRoomId = roomId;
if (!room_info.room_alias) {
newState.currentRoomId = room_info.room_id;
}
// if we aren't given an explicit event id, look for one in the
// scrollStateMap.
if (!eventId) {
var scrollState = this.scrollStateMap[roomId];
if (!room_info.event_id) {
var scrollState = this.scrollStateMap[room_info.room_id];
if (scrollState) {
newState.initialEventId = scrollState.focussedEvent;
newState.initialEventPixelOffset = scrollState.pixelOffset;
@@ -538,8 +546,8 @@ module.exports = React.createClass({
// the new screen yet (we won't be showing it yet)
// The normal case where this happens is navigating
// to the room in the URL bar on page load.
var presentedId = roomAlias || roomId;
var room = MatrixClientPeg.get().getRoom(roomId);
var presentedId = room_info.room_alias || room_info.room_id;
var room = MatrixClientPeg.get().getRoom(room_info.room_id);
if (room) {
var theAlias = MatrixTools.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias;
@@ -555,15 +563,15 @@ module.exports = React.createClass({
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
}
if (eventId) {
presentedId += "/"+eventId;
if (room_info.event_id) {
presentedId += "/"+room_info.event_id;
}
this.notifyNewScreen('room/'+presentedId);
newState.ready = true;
}
this.setState(newState);
if (this.refs.roomView && showSettings) {
if (this.refs.roomView && room_info.showSettings) {
this.refs.roomView.showSettings(true);
}
},
@@ -583,23 +591,36 @@ module.exports = React.createClass({
this.scrollStateMap[roomId] = state;
},
onLoggedIn: function(credentials) {
credentials.guest = Boolean(credentials.guest);
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
MatrixClientPeg.replaceUsingAccessToken(
credentials.homeserverUrl, credentials.identityServerUrl,
credentials.userId, credentials.accessToken, credentials.guest
);
/**
* Called when a new logged in session has started
*/
_onLoggedIn: function(credentials) {
this.guestCreds = null;
this.notifyNewScreen('');
this.setState({
screen: undefined,
logged_in: true
logged_in: true,
});
this.startMatrixClient();
this.notifyNewScreen('');
},
startMatrixClient: function() {
/**
* Called when the session is logged out
*/
_onLoggedOut: function() {
this.notifyNewScreen('login');
this.replaceState({
logged_in: false,
ready: false,
});
},
/**
* Called just before the matrix client is started
* (useful for setting listeners)
*/
_onWillStartClient() {
var cli = MatrixClientPeg.get();
var self = this;
cli.on('sync', function(state, prevState) {
self.updateFavicon(state, prevState);
@@ -666,13 +687,6 @@ module.exports = React.createClass({
action: 'logout'
});
});
Notifier.start();
UserActivity.start();
Presence.start();
cli.startClient({
pendingEventOrdering: "detached",
initialSyncLimit: this.props.config.sync_timeline_limit || 20,
});
},
// stop all the background processes related to the current client
@@ -910,12 +924,14 @@ module.exports = React.createClass({
onReturnToGuestClick: function() {
// reanimate our guest login
this.onLoggedIn(this.state.guestCreds);
this.setState({ guestCreds: null });
if (this.guestCreds) {
Lifecycle.setLoggedIn(this.guestCreds);
this.guestCreds = null;
}
},
onRegistered: function(credentials) {
this.onLoggedIn(credentials);
Lifecycle.setLoggedIn(credentials);
// do post-registration stuff
// This now goes straight to user settings
// We use _setPage since if we wait for
@@ -1032,6 +1048,7 @@ module.exports = React.createClass({
<RoomView
ref="roomView"
roomAddress={this.state.currentRoomAlias || this.state.currentRoomId}
autoJoin={this.state.autoJoin}
onRoomIdResolved={this.onRoomIdResolved}
eventId={this.state.initialEventId}
thirdPartyInvite={this.state.thirdPartyInvite}
@@ -1111,8 +1128,8 @@ module.exports = React.createClass({
email={this.props.startingQueryParams.email}
username={this.state.upgradeUsername}
guestAccessToken={this.state.guestAccessToken}
defaultHsUrl={this.props.config.default_hs_url}
defaultIsUrl={this.props.config.default_is_url}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
brand={this.props.config.brand}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
@@ -1120,14 +1137,14 @@ module.exports = React.createClass({
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick}
onRegisterClick={this.onRegisterClick}
onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null }
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
/>
);
} else if (this.state.screen == 'forgot_password') {
return (
<ForgotPassword
defaultHsUrl={this.props.config.default_hs_url}
defaultIsUrl={this.props.config.default_is_url}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
onComplete={this.onLoginClick}
@@ -1136,16 +1153,16 @@ module.exports = React.createClass({
} else {
return (
<Login
onLoggedIn={this.onLoggedIn}
onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick}
defaultHsUrl={this.props.config.default_hs_url}
defaultIsUrl={this.props.config.default_is_url}
defaultHsUrl={this.getDefaultHsUrl()}
defaultIsUrl={this.getDefaultIsUrl()}
customHsUrl={this.getCurrentHsUrl()}
customIsUrl={this.getCurrentIsUrl()}
fallbackHsUrl={this.getFallbackHsUrl()}
onForgotPasswordClick={this.onForgotPasswordClick}
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest.bind(this, true) : undefined}
onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null }
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this._registerAsGuest.bind(this, true)}
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
/>
);
}

View File

@@ -44,6 +44,9 @@ module.exports = React.createClass({
// ID of an event to highlight. If undefined, no event will be highlighted.
highlightedEventId: React.PropTypes.string,
// Should we show URL Previews
showUrlPreview: React.PropTypes.bool,
// event after which we should show a read marker
readMarkerEventId: React.PropTypes.string,
@@ -365,6 +368,7 @@ module.exports = React.createClass({
onWidgetLoad={this._onWidgetLoad}
readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap}
showUrlPreview={this.props.showUrlPreview}
checkUnmounting={this._isUnmounting}
eventSendStatus={mxEv.status}
last={last} isSelectedEvent={highlight}/>

View File

@@ -26,9 +26,9 @@ module.exports = React.createClass({
propTypes: {
// the room this statusbar is representing.
room: React.PropTypes.object.isRequired,
// a list of TabCompleteEntries.Entry objects
tabCompleteEntries: React.PropTypes.array,
// a TabComplete object
tabComplete: React.PropTypes.object.isRequired,
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
@@ -208,11 +208,11 @@ module.exports = React.createClass({
);
}
if (this.props.tabCompleteEntries) {
if (this.props.tabComplete.isTabCompleting()) {
return (
<div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper">
<TabCompleteBar entries={this.props.tabCompleteEntries} />
<TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/>
Auto-complete
@@ -233,7 +233,7 @@ module.exports = React.createClass({
<a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }>
Resend all
</a> or <a
</a> or <a
className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onCancelAllClick }>
cancel all
@@ -247,7 +247,7 @@ module.exports = React.createClass({
// unread count trumps who is typing since the unread count is only
// set when you've scrolled up
if (this.props.numUnreadMessages) {
var unreadMsgs = this.props.numUnreadMessages + " new message" +
var unreadMsgs = this.props.numUnreadMessages + " new message" +
(this.props.numUnreadMessages > 1 ? "s" : "");
return (
@@ -291,5 +291,5 @@ module.exports = React.createClass({
{content}
</div>
);
},
},
});

View File

@@ -31,16 +31,15 @@ var Modal = require("../../Modal");
var sdk = require('../../index');
var CallHandler = require('../../CallHandler');
var TabComplete = require("../../TabComplete");
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
var CommandEntry = require("../../TabCompleteEntries").CommandEntry;
var Resend = require("../../Resend");
var SlashCommands = require("../../SlashCommands");
var dis = require("../../dispatcher");
var Tinter = require("../../Tinter");
var rate_limited_func = require('../../ratelimitedfunc');
var ObjectUtils = require('../../ObjectUtils');
var MatrixTools = require('../../MatrixTools');
import UserProvider from '../../autocomplete/UserProvider';
var DEBUG = false;
if (DEBUG) {
@@ -117,6 +116,11 @@ module.exports = React.createClass({
guestsCanJoin: false,
canPeek: false,
// error object, as from the matrix client/server API
// If we failed to load information about the room,
// store the error here.
roomLoadError: null,
// this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. It has the effect of hiding the
// 'scroll to bottom' knob, among a couple of other things.
@@ -134,6 +138,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().on("accountData", this.onAccountData);
this.tabComplete = new TabComplete({
allowLooping: false,
@@ -159,10 +164,11 @@ module.exports = React.createClass({
roomId: result.room_id,
roomLoading: !room,
hasUnsentMessages: this._hasUnsentMessages(room),
}, this._updatePeeking);
}, this._onHaveRoom);
}, (err) => {
this.setState({
roomLoading: false,
roomLoadError: err,
});
});
} else {
@@ -172,11 +178,11 @@ module.exports = React.createClass({
room: room,
roomLoading: !room,
hasUnsentMessages: this._hasUnsentMessages(room),
}, this._updatePeeking);
}, this._onHaveRoom);
}
},
_updatePeeking: function() {
_onHaveRoom: function() {
// 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)
// - This is a room we can publicly join or were invited to. (we can /join)
@@ -187,29 +193,47 @@ module.exports = React.createClass({
// Note that peeking works by room ID and room ID only, as opposed to joining
// which must be by alias or invite wherever possible (peeking currently does
// not work over federation).
if (!this.state.room && this.state.roomId) {
console.log("Attempting to peek into room %s", this.state.roomId);
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
this.setState({
room: room,
roomLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
// This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value,
// it means we can't peek.
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
// This is fine: the room just isn't peekable (we assume).
// NB. We peek if we are not in the room, although if we try to peek into
// a room in which we have a member event (ie. we've left) synapse will just
// send us the same data as we get in the sync (ie. the last events we saw).
var user_is_in_room = null;
if (this.state.room) {
user_is_in_room = this.state.room.hasMembershipState(
MatrixClientPeg.get().credentials.userId, 'join'
);
this._updateAutoComplete();
this.tabComplete.loadEntries(this.state.room);
}
if (!user_is_in_room && this.state.roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
} else if (this.state.roomId) {
console.log("Attempting to peek into room %s", this.state.roomId);
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
this.setState({
room: room,
roomLoading: false,
});
} else {
throw err;
}
}).done();
} else if (this.state.room) {
this._onRoomLoaded(room);
}, (err) => {
// This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value,
// it means we can't peek.
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
// This is fine: the room just isn't peekable (we assume).
this.setState({
roomLoading: false,
});
} else {
throw err;
}
}).done();
}
} else if (user_is_in_room) {
MatrixClientPeg.get().stopPeeking();
this._onRoomLoaded(this.state.room);
}
@@ -244,6 +268,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
window.removeEventListener('resize', this.onResize);
@@ -315,6 +340,10 @@ module.exports = React.createClass({
// ignore events for other rooms
if (!this.state.room || room.roomId != this.state.room.roomId) return;
if (ev.getType() === "org.matrix.room.preview_urls") {
this._updatePreviewUrlVisibility(room);
}
// ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
@@ -334,12 +363,21 @@ module.exports = React.createClass({
});
}
}
// update the tab complete list as it depends on who most recently spoke,
// and that has probably just changed
if (ev.sender) {
this.tabComplete.onMemberSpoke(ev.sender);
// nb. we don't need to update the new autocomplete here since
// its results are currently ordered purely by search score.
}
},
// called when state.room is first initialised (either at initial load,
// after a successful peek, or after we join the room).
_onRoomLoaded: function(room) {
this._calculatePeekRules(room);
this._updatePreviewUrlVisibility(room);
},
_calculatePeekRules: function(room) {
@@ -358,6 +396,42 @@ module.exports = React.createClass({
}
},
_updatePreviewUrlVisibility: function(room) {
// console.log("_updatePreviewUrlVisibility");
// check our per-room overrides
var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
this.setState({
showUrlPreview: !roomPreviewUrls.getContent().disable
});
return;
}
// check our global disable override
var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false
});
return;
}
// check the room state event
var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false
});
return;
}
// otherwise, we assume they're on.
this.setState({
showUrlPreview: true
});
},
onRoom: function(room) {
// This event is fired when the room is 'stored' by the JS SDK, which
// means it's now a fully-fledged room object ready to be used, so
@@ -388,14 +462,23 @@ module.exports = React.createClass({
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
},
onRoomAccountData: function(room, event) {
if (room.roomId == this.props.roomId) {
if (event.getType === "org.matrix.room.color_scheme") {
onAccountData: function(event) {
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
this._updatePreviewUrlVisibility(this.state.room);
}
},
onRoomAccountData: function(event, room) {
if (room.roomId == this.state.roomId) {
if (event.getType() === "org.matrix.room.color_scheme") {
var 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") {
this._updatePreviewUrlVisibility(room);
}
}
},
@@ -410,8 +493,20 @@ module.exports = React.createClass({
return;
}
if (this.props.ConferenceHandler &&
member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
this._updateConfCallNotification();
}
this._updateRoomMembers();
},
// rate limited because a power level change will emit an event for every
// member in the room.
_updateRoomMembers: new rate_limited_func(function() {
// a member state changed in this room, refresh the tab complete list
this._updateTabCompleteList();
this.tabComplete.loadEntries(this.state.room);
this._updateAutoComplete();
// if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking
@@ -422,12 +517,7 @@ module.exports = React.createClass({
joining: false
});
}
if (this.props.ConferenceHandler &&
member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
this._updateConfCallNotification();
}
},
}, 500),
_hasUnsentMessages: function(room) {
return this._getUnsentMessages(room).length > 0;
@@ -476,8 +566,6 @@ module.exports = React.createClass({
window.addEventListener('resize', this.onResize);
this.onResize();
this._updateTabCompleteList();
// XXX: EVIL HACK to autofocus inviting on empty rooms.
// We use the setTimeout to avoid racing with focus_composer.
if (this.state.room &&
@@ -495,22 +583,6 @@ module.exports = React.createClass({
}
},
_updateTabCompleteList: new rate_limited_func(function() {
var cli = MatrixClientPeg.get();
if (!this.state.room || !this.tabComplete) {
return;
}
var members = this.state.room.getJoinedMembers().filter(function(member) {
if (member.userId !== cli.credentials.userId) return true;
});
this.tabComplete.setCompletionList(
MemberEntry.fromMemberList(members).concat(
CommandEntry.fromCommands(SlashCommands.getCommandList())
)
);
}, 500),
componentDidUpdate: function() {
if (this.refs.roomView) {
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
@@ -992,7 +1064,7 @@ module.exports = React.createClass({
this.setState({
rejecting: true
});
MatrixClientPeg.get().leave(this.props.roomAddress).done(function() {
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
self.setState({
rejecting: false
@@ -1235,6 +1307,14 @@ module.exports = React.createClass({
}
},
_updateAutoComplete: function() {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = this.state.room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
UserProvider.getInstance().setUserList(members);
},
render: function() {
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
@@ -1267,6 +1347,7 @@ module.exports = React.createClass({
// We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite.
var room_alias = this.props.roomAddress[0] == '#' ? this.props.roomAddress : null;
return (
<div className="mx_RoomView">
<RoomHeader ref="header"
@@ -1277,7 +1358,8 @@ module.exports = React.createClass({
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
canJoin={ true } canPreview={ false }
canPreview={ false } error={ this.state.roomLoadError }
roomAlias={room_alias}
spinner={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
@@ -1315,7 +1397,7 @@ module.exports = React.createClass({
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onRejectClick={ this.onRejectButtonClicked }
inviterName={ inviterName }
canJoin={ true } canPreview={ false }
canPreview={ false }
spinner={this.state.joining}
room={this.state.room}
/>
@@ -1346,12 +1428,10 @@ module.exports = React.createClass({
statusBar = <UploadBar room={this.state.room} />
} else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
var tabEntries = this.tabComplete.isTabCompleting() ?
this.tabComplete.peek(6) : null;
statusBar = <RoomStatusBar
room={this.state.room}
tabCompleteEntries={tabEntries}
tabComplete={this.tabComplete}
numUnreadMessages={this.state.numUnreadMessages}
hasUnsentMessages={this.state.hasUnsentMessages}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
@@ -1385,7 +1465,7 @@ module.exports = React.createClass({
invitedEmail = this.props.thirdPartyInvite.invitedEmail;
}
aux = (
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true}
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining}
inviterName={inviterName}
@@ -1484,6 +1564,8 @@ module.exports = React.createClass({
hideMessagePanel = true;
}
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
var messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef}
room={this.state.room}
@@ -1493,6 +1575,7 @@ module.exports = React.createClass({
eventPixelOffset={this.props.eventPixelOffset}
onScroll={ this.onMessageListScroll }
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
showUrlPreview = { this.state.showUrlPreview }
opacity={ this.props.opacity }
/>);

View File

@@ -540,7 +540,6 @@ module.exports = React.createClass({
// it's not obvious why we have a separate div and ol anyway.
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
onScroll={this.onScroll} onResize={this.onResize}
relayoutOnUpdate={false}
className={this.props.className} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">

View File

@@ -71,6 +71,9 @@ var TimelinePanel = React.createClass({
// half way down the viewport.
eventPixelOffset: React.PropTypes.number,
// Should we show URL Previews
showUrlPreview: React.PropTypes.bool,
// callback which is called when the panel is scrolled.
onScroll: React.PropTypes.func,
@@ -934,6 +937,7 @@ var TimelinePanel = React.createClass({
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview = { this.props.showUrlPreview }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll }

View File

@@ -214,9 +214,10 @@ module.exports = React.createClass({
onFinished: this.onEmailDialogFinished,
});
}, (err) => {
this.setState({email_add_pending: false});
Modal.createDialog(ErrorDialog, {
title: "Unable to add email address",
description: err.toString()
description: err.message
});
});
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
@@ -261,7 +262,64 @@ module.exports = React.createClass({
});
},
_renderDeviceInfo: function() {
_renderUserInterfaceSettings: function() {
var client = MatrixClientPeg.get();
var settingsLabels = [
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
},
{
id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
];
var syncedSettings = UserSettingsStore.getSyncedSettings();
return (
<div>
<h3>User Interface</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_toggle">
<input id="urlPreviewsDisabled"
type="checkbox"
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
onChange={ e => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
</label>
</div>
</div>
{ settingsLabels.forEach( setting => {
<div className="mx_UserSettings_toggle">
<input id={ setting.id }
type="checkbox"
defaultChecked={ syncedSettings[setting.id] }
onChange={ e => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
/>
<label htmlFor={ setting.id }>
{ settings.label }
</label>
</div>
})}
</div>
);
},
_renderCryptoInfo: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
return null;
}
@@ -282,6 +340,45 @@ module.exports = React.createClass({
);
},
_renderDevicesPanel: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
return null;
}
var DevicesPanel = sdk.getComponent('settings.DevicesPanel');
return (
<div>
<h3>Devices</h3>
<DevicesPanel className="mx_UserSettings_section" />
</div>
);
},
_renderLabs: function () {
let features = LABS_FEATURES.map(feature => (
<div key={feature.id} className="mx_UserSettings_toggle">
<input
type="checkbox"
id={feature.id}
name={feature.id}
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
onChange={e => {
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
this.forceUpdate();
}}/>
<label htmlFor={feature.id}>{feature.name}</label>
</div>
));
return (
<div>
<h3>Labs</h3>
<div className="mx_UserSettings_section">
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
{features}
</div>
</div>
)
},
render: function() {
var self = this;
var Loader = sdk.getComponent("elements.Spinner");
@@ -302,6 +399,7 @@ module.exports = React.createClass({
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var Notifications = sdk.getComponent("settings.Notifications");
var EditableText = sdk.getComponent('elements.EditableText');
var avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
);
@@ -376,36 +474,11 @@ module.exports = React.createClass({
</div>);
}
this._renderLabs = function () {
let features = LABS_FEATURES.map(feature => (
<div key={feature.id}>
<input
type="checkbox"
id={feature.id}
name={feature.id}
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
onChange={e => UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked)} />
<label htmlFor={feature.id}>{feature.name}</label>
</div>
));
return (
<div>
<h3>Labs</h3>
<div className="mx_UserSettings_section">
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
{features}
</div>
</div>
)
};
return (
<div className="mx_UserSettings">
<SimpleRoomHeader title="Settings" onCancelClick={ this.props.onClose }/>
<GeminiScrollbar className="mx_UserSettings_body"
relayoutOnUpdate={false}
autoshow={true}>
<h3>Profile</h3>
@@ -452,9 +525,10 @@ module.exports = React.createClass({
{notification_area}
{this._renderDeviceInfo()}
{this._renderUserInterfaceSettings()}
{this._renderLabs()}
{this._renderDevicesPanel()}
{this._renderCryptoInfo()}
<h3>Advanced</h3>

View File

@@ -232,7 +232,9 @@ module.exports = React.createClass({displayName: 'Login',
<div className="mx_Login_box">
<LoginHeader />
<div>
<h2>Sign in</h2>
<h2>Sign in
{ loader }
</h2>
{ this.componentForStep(this._getCurrentFlowStep()) }
<ServerConfig ref="serverConfig"
withToggleButton={true}
@@ -244,7 +246,6 @@ module.exports = React.createClass({displayName: 'Login',
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={1000}/>
<div className="mx_Login_error">
{ loader }
{ this.state.errorText }
</div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">

View File

@@ -54,6 +54,16 @@ module.exports = React.createClass({
return {
busy: false,
errorText: null,
// We remember the values entered by the user because
// the registration form will be unmounted during the
// course of registration, but if there's an error we
// want to bring back the registration form with the
// values the user entered still in it. We can keep
// them in this component's state since this component
// persist for the duration of the registration process.
formVals: {
email: this.props.email,
},
};
},
@@ -108,7 +118,8 @@ module.exports = React.createClass({
var self = this;
this.setState({
errorText: "",
busy: true
busy: true,
formVals: formVals,
});
if (formVals.username !== this.props.username) {
@@ -228,11 +239,15 @@ module.exports = React.createClass({
break; // NOP
case "Register.START":
case "Register.STEP_m.login.dummy":
// NB. Our 'username' prop is specifically for upgrading
// a guest account
registerStep = (
<RegistrationForm
showEmail={true}
defaultUsername={this.props.username}
defaultEmail={this.props.email}
defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email}
defaultPassword={this.state.formVals.password}
guestUsername={this.props.username}
minPasswordLength={MIN_PASSWORD_LENGTH}
onError={this.onFormValidationFailed}
onRegisterClick={this.onFormSubmit} />