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

View File

@@ -18,6 +18,7 @@ limitations under the License.
var React = require('react');
var AvatarLogic = require("../../../Avatar");
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'BaseAvatar',
@@ -132,32 +133,36 @@ module.exports = React.createClass({
},
render: function() {
var name = this.props.name;
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
const {
name, idName, title, url, urls, width, height, resizeMethod,
defaultToInitialLetter,
...otherProps
} = this.props;
if (imageUrl === this.state.defaultImageUrl) {
var initialLetter = this._getInitialLetter(this.props.name);
var initialLetter = emojifyText(this._getInitialLetter(name));
return (
<span className="mx_BaseAvatar" {...this.props}>
<span className="mx_BaseAvatar" {...otherProps}>
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (this.props.width * 0.65) + "px",
width: this.props.width + "px",
lineHeight: this.props.height + "px" }}>
{ initialLetter }
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
dangerouslySetInnerHTML={initialLetter}>
</span>
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={this.props.title} onError={this.onError}
width={this.props.width} height={this.props.height} />
alt="" title={title} onError={this.onError}
width={width} height={height} />
</span>
);
}
return (
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
onError={this.onError}
width={this.props.width} height={this.props.height}
title={this.props.title} alt=""
{...this.props} />
width={width} height={height}
title={title} alt=""
{...otherProps} />
);
}
});

View File

@@ -59,9 +59,12 @@ module.exports = React.createClass({
render: function() {
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var {member, ...otherProps} = this.props;
return (
<BaseAvatar {...this.props} name={this.state.name} title={this.state.title}
idName={this.props.member.userId} url={this.state.imageUrl} />
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
idName={member.userId} url={this.state.imageUrl} />
);
}
});

View File

@@ -126,11 +126,13 @@ module.exports = React.createClass({
render: function() {
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var roomName = this.props.room ? this.props.room.name : this.props.oobData.name;
var {room, oobData, ...otherProps} = this.props;
var roomName = room ? room.name : oobData.name;
return (
<BaseAvatar {...this.props} name={roomName}
idName={this.props.room ? this.props.room.roomId : null}
<BaseAvatar {...otherProps} name={roomName}
idName={room ? room.roomId : null}
urls={this.state.urls} />
);
}

View File

@@ -59,7 +59,7 @@ module.exports = React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={this.props.focus}>
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={this.props.focus}>
{this.props.button}
</button>
</div>

View File

@@ -46,7 +46,7 @@ module.exports = React.createClass({
Sign out?
</div>
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button autoFocus onClick={this.logOut}>Sign Out</button>
<button className="mx_Dialog_primary" autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button>
</div>
</div>

View File

@@ -63,7 +63,7 @@ module.exports = React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}>
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
Cancel
</button>
<button onClick={this.onRegisterClicked}>

View File

@@ -56,7 +56,7 @@ module.exports = React.createClass({
{this.props.description}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onOk} autoFocus={this.props.focus}>
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button}
</button>

View File

@@ -76,7 +76,7 @@ module.exports = React.createClass({
/>
</div>
<div className="mx_Dialog_buttons">
<input type="submit" value="Set" />
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</div>

View File

@@ -86,7 +86,7 @@ module.exports = React.createClass({
<button onClick={this.onCancel}>
Cancel
</button>
<button onClick={this.onOk}>
<button className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button}
</button>
</div>

View File

@@ -49,6 +49,8 @@ module.exports = React.createClass({
label: '',
placeholder: '',
editable: true,
className: "mx_EditableText",
placeholderClassName: "mx_EditableText_placeholder",
};
},
@@ -92,7 +94,7 @@ module.exports = React.createClass({
this.refs.editable_div.textContent = this.value;
this.refs.editable_div.setAttribute("class", this.props.className);
this.placeholder = false;
}
}
},
getValue: function() {
@@ -101,7 +103,7 @@ module.exports = React.createClass({
setValue: function(value) {
this.value = value;
this.showPlaceholder(!this.value);
this.showPlaceholder(!this.value);
},
edit: function() {
@@ -125,7 +127,7 @@ module.exports = React.createClass({
onKeyDown: function(ev) {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) {
this.showPlaceholder(false);
}
@@ -173,7 +175,7 @@ module.exports = React.createClass({
var range = document.createRange();
range.setStart(node, 0);
range.setEnd(node, node.length);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);

View File

@@ -0,0 +1,147 @@
/*
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.
*/
import React from 'react';
import sdk from '../../../index';
import q from 'q';
/**
* A component which wraps an EditableText, with a spinner while updates take
* place.
*
* Parent components should supply an 'onSubmit' callback which returns a
* promise; a spinner is shown until the promise resolves.
*
* The parent can also supply a 'getIntialValue' callback, which works in a
* similarly asynchronous way. If this is not provided, the initial value is
* taken from the 'initialValue' property.
*/
export default class EditableTextContainer extends React.Component {
constructor(props, context) {
super(props, context);
this._unmounted = false;
this.state = {
busy: false,
errorString: null,
value: props.initialValue,
};
this._onValueChanged = this._onValueChanged.bind(this);
}
componentWillMount() {
if (this.props.getInitialValue === undefined) {
// use whatever was given in the initialValue property.
return;
}
this.setState({busy: true});
this.props.getInitialValue().done(
(result) => {
if (this._unmounted) { return; }
this.setState({
busy: false,
value: result,
});
},
(error) => {
if (this._unmounted) { return; }
this.setState({
errorString: error.toString(),
busy: false,
});
}
);
}
componentWillUnmount() {
this._unmounted = true;
}
_onValueChanged(value, shouldSubmit) {
if (!shouldSubmit) {
return;
}
this.setState({
busy: true,
errorString: null,
});
this.props.onSubmit(value).done(
() => {
if (this._unmounted) { return; }
this.setState({
busy: false,
value: value,
});
},
(error) => {
if (this._unmounted) { return; }
this.setState({
errorString: error.toString(),
busy: false,
});
}
);
}
render() {
if (this.state.busy) {
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
} else if (this.state.errorString) {
return (
<div className="error">{this.state.errorString}</div>
);
} else {
var EditableText = sdk.getComponent('elements.EditableText');
return (
<EditableText initialValue={this.state.value}
placeholder={this.props.placeholder}
onValueChanged={this._onValueChanged}
/>
);
}
}
}
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: React.PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: React.PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: React.PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: React.PropTypes.func,
};
EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
onSubmit: function(v) {return q(); },
};

View File

@@ -34,10 +34,15 @@ module.exports = React.createClass({
propTypes: {
value: React.PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled.
controlled: React.PropTypes.bool.isRequired,
//
// ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool,
// should the user be able to change the value? false by default.
disabled: React.PropTypes.bool,
onChange: React.PropTypes.func,
},

View File

@@ -35,8 +35,16 @@ module.exports = React.createClass({
displayName: 'RegistrationForm',
propTypes: {
// Values pre-filled in the input boxes when the component loads
defaultEmail: React.PropTypes.string,
defaultUsername: React.PropTypes.string,
defaultPassword: React.PropTypes.string,
// A username that will be used if no username is entered.
// Specifying this param will also warn the user that entering
// a different username will cause a fresh account to be generated.
guestUsername: React.PropTypes.string,
showEmail: React.PropTypes.bool,
minPasswordLength: React.PropTypes.number,
onError: React.PropTypes.func,
@@ -55,10 +63,6 @@ module.exports = React.createClass({
getInitialState: function() {
return {
email: this.props.defaultEmail,
username: null,
password: null,
passwordConfirm: null,
fieldValid: {}
};
},
@@ -103,7 +107,7 @@ module.exports = React.createClass({
_doSubmit: function() {
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.defaultUsername,
username: this.refs.username.value.trim() || this.props.guestUsername,
password: this.refs.password.value.trim(),
email: this.refs.email.value.trim()
});
@@ -144,7 +148,7 @@ module.exports = React.createClass({
break;
case FIELD_USERNAME:
// XXX: SPEC-1
var username = this.refs.username.value.trim() || this.props.defaultUsername;
var username = this.refs.username.value.trim() || this.props.guestUsername;
if (encodeURIComponent(username) != username) {
this.markFieldValid(
field_id,
@@ -225,7 +229,7 @@ module.exports = React.createClass({
emailSection = (
<input className="mx_Login_field" type="text" ref="email"
autoFocus={true} placeholder="Email address (optional)"
defaultValue={this.state.email}
defaultValue={this.props.defaultEmail}
style={this._styleField(FIELD_EMAIL)}
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
);
@@ -237,8 +241,8 @@ module.exports = React.createClass({
}
var placeholderUserName = "User name";
if (this.props.defaultUsername) {
placeholderUserName += " (default: " + this.props.defaultUsername + ")"
if (this.props.guestUsername) {
placeholderUserName += " (default: " + this.props.guestUsername + ")"
}
return (
@@ -247,23 +251,23 @@ module.exports = React.createClass({
{emailSection}
<br />
<input className="mx_Login_field" type="text" ref="username"
placeholder={ placeholderUserName } defaultValue={this.state.username}
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
style={this._styleField(FIELD_USERNAME)}
onBlur={function() {self.validateField(FIELD_USERNAME)}} />
<br />
{ this.props.defaultUsername ?
{ this.props.guestUsername ?
<div className="mx_Login_fieldLabel">Setting a user name will create a fresh account</div> : null
}
<input className="mx_Login_field" type="password" ref="password"
style={this._styleField(FIELD_PASSWORD)}
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
placeholder="Password" defaultValue={this.state.password} />
placeholder="Password" defaultValue={this.props.defaultPassword} />
<br />
<input className="mx_Login_field" type="password" ref="passwordConfirm"
placeholder="Confirm password"
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
defaultValue={this.state.passwordConfirm} />
defaultValue={this.props.defaultPassword} />
<br />
{registerButton}
</form>

View File

@@ -34,7 +34,7 @@ module.exports = React.createClass({
}
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
// no scaling needs to be applied
return fullHeight;
return 1;
}
var widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight;

View File

@@ -38,6 +38,9 @@ module.exports = React.createClass({
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
/* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func,
},
@@ -71,6 +74,7 @@ module.exports = React.createClass({
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />;
},
});

View File

@@ -39,6 +39,9 @@ module.exports = React.createClass({
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
/* callback for when our widget has loaded */
onWidgetLoad: React.PropTypes.func,
},
@@ -56,34 +59,47 @@ module.exports = React.createClass({
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
var links = this.findLinks(this.refs.content.children);
if (links.length) {
this.setState({ links: links.map((link)=>{
return link.getAttribute("href");
})});
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
}
this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
},
componentDidUpdate: function() {
this.calculateUrlPreview();
},
shouldComponentUpdate: function(nextProps, nextState) {
//console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
// exploit that events are immutable :)
// ...and that .links is only ever set in componentDidMount and never changes
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.highlights !== this.props.highlights ||
nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextState.links !== this.state.links ||
nextState.widgetHidden !== this.state.widgetHidden);
},
calculateUrlPreview: function() {
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
if (this.props.showUrlPreview && !this.state.links.length) {
var links = this.findLinks(this.refs.content.children);
if (links.length) {
this.setState({ links: links.map((link)=>{
return link.getAttribute("href");
})});
// lazy-load the hidden state of the preview widget from localstorage
if (global.localStorage) {
var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
}
}
},
findLinks: function(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
@@ -163,12 +179,14 @@ module.exports = React.createClass({
render: function() {
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
{highlightLink: this.props.highlightLink});
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
if (this.props.highlightLink) {
body = <a href={ this.props.highlightLink }>{ body }</a>;
}
var widgets;
if (this.state.links.length && !this.state.widgetHidden) {
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget');
widgets = this.state.links.map((link)=>{
return <LinkPreviewWidget

View File

@@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var TextForEvent = require('../../../TextForEvent');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'TextualEvent',
@@ -31,11 +32,11 @@ module.exports = React.createClass({
render: function() {
var text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length == 0) return null;
if (text == null || text.length === 0) return null;
let textHTML = emojifyText(TextForEvent.textForEvent(this.props.mxEvent));
return (
<div className="mx_TextualEvent">
{TextForEvent.textForEvent(this.props.mxEvent)}
<div className="mx_TextualEvent" dangerouslySetInnerHTML={textHTML}>
</div>
);
},

View File

@@ -83,13 +83,11 @@ module.exports = React.createClass({
alias: this.state.canonicalAlias
}, ""
)
);
);
}
// save new aliases for m.room.aliases
var aliasOperations = this.getAliasOperations();
var promises = [];
for (var i = 0; i < aliasOperations.length; i++) {
var alias_operation = aliasOperations[i];
console.log("alias %s %s", alias_operation.place, alias_operation.val);
@@ -301,7 +299,7 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_addAlias">
<img src="img/plus.svg" width="14" height="14" alt="Add"
onClick={ self.onAliasAdded.bind(self, undefined) }/>
</div>
</div>
</div> : ""
}
</div>

View File

@@ -57,7 +57,7 @@ module.exports = React.createClass({
data.primary_color = scheme.primary_color;
data.secondary_color = scheme.secondary_color;
data.index = this._getColorIndex(data);
if (data.index === -1) {
// append the unrecognised colours to our palette
data.index = ROOM_COLORS.length;

View File

@@ -0,0 +1,157 @@
/*
Copyright 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.
*/
var q = require("q");
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require("../../../index");
var Modal = require("../../../Modal");
var UserSettingsStore = require('../../../UserSettingsStore');
module.exports = React.createClass({
displayName: 'UrlPreviewSettings',
propTypes: {
room: React.PropTypes.object,
},
getInitialState: function() {
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
var userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
return {
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
};
},
componentDidMount: function() {
this.originalState = Object.assign({}, this.state);
},
saveSettings: function() {
var promises = [];
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) {
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
promises.push(
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId, "org.matrix.room.preview_urls", {
disable: this.state.globalDisableUrlPreview
}, ""
)
);
}
var content = undefined;
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
content = this.state.userDisableUrlPreview ? { disable : true } : {};
}
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
if (!content || content.disable === undefined) {
content = this.state.userEnableUrlPreview ? { disable : false } : {};
}
}
if (content) {
promises.push(
MatrixClientPeg.get().setRoomAccountData(
this.props.room.roomId, "org.matrix.room.preview_urls", content
)
);
}
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
return promises;
},
onGlobalDisableUrlPreviewChange: function() {
this.setState({
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
});
},
onUserEnableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: false,
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
});
},
onUserDisableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
userEnableUrlPreview: false,
});
},
render: function() {
var self = this;
var roomState = this.props.room.currentState;
var cli = MatrixClientPeg.get();
var maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli);
var disableRoomPreviewUrls;
if (maySetRoomPreviewUrls) {
disableRoomPreviewUrls =
<label>
<input type="checkbox" ref="globalDisableUrlPreview"
onChange={ this.onGlobalDisableUrlPreviewChange }
checked={ this.state.globalDisableUrlPreview } />
Disable URL previews by default for participants in this room
</label>
}
else {
disableRoomPreviewUrls =
<label>
URL previews are { this.state.globalDisableUrlPreview ? "disabled" : "enabled" } by default for participants in this room.
</label>
}
return (
<div className="mx_RoomSettings_toggles">
<h3>URL Previews</h3>
<label>
You have <a href="#/settings">{ UserSettingsStore.getUrlPreviewsDisabled() ? 'disabled' : 'enabled' }</a> URL previews by default.
</label>
{ disableRoomPreviewUrls }
<label>
<input type="checkbox" ref="userEnableUrlPreview"
onChange={ this.onUserEnableUrlPreviewChange }
checked={ this.state.userEnableUrlPreview } />
Enable URL previews for this room (affects only you)
</label>
<label>
<input type="checkbox" ref="userDisableUrlPreview"
onChange={ this.onUserDisableUrlPreviewChange }
checked={ this.state.userDisableUrlPreview } />
Disable URL previews for this room (affects only you)
</label>
</div>
);
}
});

View File

@@ -0,0 +1,164 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import {getCompletions} from '../../../autocomplete/Autocompleter';
export default class Autocomplete extends React.Component {
constructor(props) {
super(props);
this.onConfirm = this.onConfirm.bind(this);
this.state = {
// list of completionResults, each containing completions
completions: [],
// array of completions, so we can look up current selection by offset quickly
completionList: [],
// how far down the completion list we are
selectionOffset: 0,
};
}
componentWillReceiveProps(props, state) {
if (props.query === this.props.query) {
return;
}
getCompletions(props.query, props.selection).forEach(completionResult => {
try {
completionResult.completions.then(completions => {
let i = this.state.completions.findIndex(
completion => completion.provider === completionResult.provider
);
i = i === -1 ? this.state.completions.length : i;
let newCompletions = Object.assign([], this.state.completions);
completionResult.completions = completions;
newCompletions[i] = completionResult;
this.setState({
completions: newCompletions,
completionList: flatMap(newCompletions, provider => provider.completions),
});
}, err => {
console.error(err);
});
} catch (e) {
// An error in one provider shouldn't mess up the rest.
console.error(e);
}
});
}
countCompletions(): number {
return this.state.completions.map(completionResult => {
return completionResult.completions.length;
}).reduce((l, r) => l + r);
}
// called from MessageComposerInput
onUpArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
if (!completionCount) {
return false;
}
this.setSelection(selectionOffset);
return true;
}
// called from MessageComposerInput
onDownArrow(): boolean {
let completionCount = this.countCompletions(),
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
if (!completionCount) {
return false;
}
this.setSelection(selectionOffset);
return true;
}
/** called from MessageComposerInput
* @returns {boolean} whether confirmation was handled
*/
onConfirm(): boolean {
if (this.countCompletions() === 0) {
return false;
}
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
return true;
}
setSelection(selectionOffset: number) {
this.setState({selectionOffset});
}
render() {
let position = 0;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
let className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
position++;
let onMouseOver = () => this.setSelection(componentPosition);
let onClick = () => {
this.setSelection(componentPosition);
this.onConfirm();
};
return (
<div key={i}
className={className}
onMouseOver={onMouseOver}
onClick={onClick}>
{completion.component}
</div>
);
});
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{completions}
</ReactCSSTransitionGroup>
</div>
) : null;
});
return (
<div className="mx_Autocomplete">
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{renderedCompletions}
</ReactCSSTransitionGroup>
</div>
);
}
}
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
query: React.PropTypes.string.isRequired,
// method invoked with range and text content when completion is confirmed
onConfirm: React.PropTypes.func.isRequired,
};

View File

@@ -20,6 +20,7 @@ var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
import {emojifyText} from '../../../HtmlUtils';
var PRESENCE_CLASS = {
@@ -28,6 +29,23 @@ var PRESENCE_CLASS = {
"unavailable": "mx_EntityTile_unavailable"
};
function presenceClassForMember(presenceState, lastActiveAgo) {
// offline is split into two categories depending on whether we have
// a last_active_ago for them.
if (presenceState == 'offline') {
if (lastActiveAgo) {
return PRESENCE_CLASS['offline'] + '_beenactive';
} else {
return PRESENCE_CLASS['offline'] + '_neveractive';
}
} else if (presenceState) {
return PRESENCE_CLASS[presenceState];
} else {
return PRESENCE_CLASS['offline'] + '_neveractive';
}
}
module.exports = React.createClass({
displayName: 'EntityTile',
@@ -78,10 +96,14 @@ module.exports = React.createClass({
},
render: function() {
var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline";
const presenceClass = presenceClassForMember(
this.props.presenceState, this.props.presenceLastActiveAgo
);
var mainClassName = "mx_EntityTile ";
mainClassName += presenceClass + (this.props.className ? (" " + this.props.className) : "");
var nameEl;
let nameHTML = emojifyText(this.props.name);
if (this.state.hover && !this.props.suppressOnHover) {
var activeAgo = this.props.presenceLastActiveAgo ?
@@ -92,7 +114,7 @@ module.exports = React.createClass({
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
<div className="mx_EntityTile_name_hover" dangerouslySetInnerHTML={nameHTML}></div>
<PresenceLabel activeAgo={ activeAgo }
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />
@@ -101,8 +123,7 @@ module.exports = React.createClass({
}
else {
nameEl = (
<div className="mx_EntityTile_name">
{ this.props.name }
<div className="mx_EntityTile_name" dangerouslySetInnerHTML={nameHTML}>
</div>
);
}

View File

@@ -23,7 +23,7 @@ var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu');
var ContextualMenu = require('../../structures/ContextualMenu');
var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
@@ -101,6 +101,9 @@ module.exports = React.createClass({
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* should show URL previews for this event */
showUrlPreview: React.PropTypes.bool,
/* is this the focused event */
isSelectedEvent: React.PropTypes.bool,
@@ -139,7 +142,8 @@ module.exports = React.createClass({
componentDidMount: function() {
this._suppressReadReceiptAnimation = false;
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
MatrixClientPeg.get().on("deviceVerificationChanged",
this.onDeviceVerificationChanged);
},
componentWillReceiveProps: function (nextProps) {
@@ -163,11 +167,12 @@ module.exports = React.createClass({
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified);
client.removeListener("deviceVerificationChanged",
this.onDeviceVerificationChanged);
}
},
onDeviceVerified: function(userId, device) {
onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent);
}
@@ -244,12 +249,15 @@ module.exports = React.createClass({
},
onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu');
var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect()
var x = buttonRect.right;
var y = buttonRect.top + (e.target.height / 2);
// The window X and Y offsets are to adjust position when zoomed in to page
var x = buttonRect.right + window.pageXOffset;
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
var self = this;
ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent,
left: x,
top: y,
@@ -357,6 +365,8 @@ module.exports = React.createClass({
var SenderProfile = sdk.getComponent('messages.SenderProfile');
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype;
@@ -418,6 +428,7 @@ module.exports = React.createClass({
<div className="mx_EventTile_line">
<EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onWidgetLoad={this.props.onWidgetLoad} />
</div>
</div>

View File

@@ -37,17 +37,14 @@ module.exports = React.createClass({
},
componentWillMount: function() {
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
var cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
this._emailEntity = null;
// Load the complete user list for inviting new users
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
// it will do for now not being updated as random new users join different
// rooms as this list will be reloaded every room swap.
if (this._room) {
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
return !this._room.hasMembershipState(u.userId, "join");
});
}
// we have to update the list whenever membership changes
// particularly to avoid bug https://github.com/vector-im/vector-web/issues/1813
this._updateList();
},
componentDidMount: function() {
@@ -55,6 +52,28 @@ module.exports = React.createClass({
this.onSearchQueryChanged('');
},
componentWillUnmount: function() {
var cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.members", this.onRoomStateMember);
}
},
_updateList: function() {
this._room = MatrixClientPeg.get().getRoom(this.props.roomId);
// Load the complete user list for inviting new users
if (this._room) {
this._userList = MatrixClientPeg.get().getUsers().filter((u) => {
return (!this._room.hasMembershipState(u.userId, "join") &&
!this._room.hasMembershipState(u.userId, "invite"));
});
}
},
onRoomStateMember: function(ev, state, member) {
this._updateList();
},
onInvite: function(ev) {
this.props.onInvite(this._input);
},

View File

@@ -36,32 +36,75 @@ module.exports = React.createClass({
);
},
onBlockClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, true
);
},
onUnblockClick: function() {
MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, false
);
},
render: function() {
var indicator = null, button = null;
if (this.props.device.verified) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
var indicator = null, blockButton = null, verifyButton = null;
if (this.props.device.blocked) {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
onClick={this.onUnblockClick}>
Unblock
</div>
);
button = (
} else {
blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_block"
onClick={this.onBlockClick}>
Block
</div>
);
}
if (this.props.device.verified) {
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}>
Unverify
</div>
);
} else {
button = (
verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}>
Verify
</div>
);
}
if (this.props.device.blocked) {
indicator = (
<div className="mx_MemberDeviceInfo_blocked">&#x2716;</div>
);
} else if (this.props.device.verified) {
indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div>
);
} else {
indicator = (
<div className="mx_MemberDeviceInfo_unverified">?</div>
);
}
var deviceName = this.props.device.display_name || this.props.device.id;
return (
<div className="mx_MemberDeviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">{this.props.device.id}</div>
<div className="mx_MemberDeviceInfo_deviceKey">{this.props.device.key}</div>
<div className="mx_MemberDeviceInfo_deviceId">{deviceName}</div>
{indicator}
{button}
{verifyButton}
{blockButton}
</div>
);
},

View File

@@ -32,6 +32,7 @@ var Modal = require("../../../Modal");
var sdk = require('../../../index');
var UserSettingsStore = require('../../../UserSettingsStore');
var createRoom = require('../../../createRoom');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'MemberInfo',
@@ -60,17 +61,21 @@ module.exports = React.createClass({
updating: 0,
devicesLoading: true,
devices: null,
existingOneToOneRoomId: null,
}
},
componentWillMount: function() {
this._cancelDeviceList = null;
this.setState({
existingOneToOneRoomId: this.getExistingOneToOneRoomId()
});
},
componentDidMount: function() {
this._updateStateForNewMember(this.props.member);
MatrixClientPeg.get().on("deviceVerified", this.onDeviceVerified);
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
},
componentWillReceiveProps: function(newProps) {
@@ -82,14 +87,67 @@ module.exports = React.createClass({
componentWillUnmount: function() {
var client = MatrixClientPeg.get();
if (client) {
client.removeListener("deviceVerified", this.onDeviceVerified);
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
if (this._cancelDeviceList) {
this._cancelDeviceList();
}
},
onDeviceVerified: function(userId, device) {
getExistingOneToOneRoomId: function() {
const rooms = MatrixClientPeg.get().getRooms();
const userIds = [
this.props.member.userId,
MatrixClientPeg.get().credentials.userId
];
let existingRoomId = null;
let invitedRoomId = null;
// roomId can be null here because of a hack in MatrixChat.onUserClick where we
// abuse this to view users rather than room members.
let currentMembers;
if (this.props.member.roomId) {
const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
currentMembers = currentRoom.getJoinedMembers();
}
// reuse the first private 1:1 we find
existingRoomId = null;
for (let i = 0; i < rooms.length; i++) {
// don't try to reuse public 1:1 rooms
const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
const members = rooms[i].getJoinedMembers();
if (members.length === 2 &&
userIds.indexOf(members[0].userId) !== -1 &&
userIds.indexOf(members[1].userId) !== -1)
{
existingRoomId = rooms[i].roomId;
break;
}
const invited = rooms[i].getMembersWithMembership('invite');
if (members.length === 1 &&
invited.length === 1 &&
userIds.indexOf(members[0].userId) !== -1 &&
userIds.indexOf(invited[0].userId) !== -1 &&
invitedRoomId === null)
{
invitedRoomId = rooms[i].roomId;
// keep looking: we'll use this one if there's nothing better
}
}
if (existingRoomId === null) {
existingRoomId = invitedRoomId;
}
return existingRoomId;
},
onDeviceVerificationChanged: function(userId, device) {
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
@@ -348,61 +406,29 @@ module.exports = React.createClass({
onChatClick: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere
const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId);
// check if there are any existing rooms with just us and them (1:1)
// If so, just view that room. If not, create a private room with them.
var self = this;
var rooms = MatrixClientPeg.get().getRooms();
var userIds = [
this.props.member.userId,
MatrixClientPeg.get().credentials.userId
];
var existingRoomId;
var currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId);
var currentMembers = currentRoom.getJoinedMembers();
// if we're currently in a 1:1 with this user, start a new chat
if (currentMembers.length === 2 &&
userIds.indexOf(currentMembers[0].userId) !== -1 &&
userIds.indexOf(currentMembers[1].userId) !== -1)
{
existingRoomId = null;
}
// otherwise reuse the first private 1:1 we find
else {
existingRoomId = null;
for (var i = 0; i < rooms.length; i++) {
// don't try to reuse public 1:1 rooms
var join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", '');
if (join_rules && join_rules.getContent().join_rule === 'public') continue;
var members = rooms[i].getJoinedMembers();
if (members.length === 2 &&
userIds.indexOf(members[0].userId) !== -1 &&
userIds.indexOf(members[1].userId) !== -1)
{
existingRoomId = rooms[i].roomId;
break;
}
}
}
if (existingRoomId) {
if (useExistingOneToOneRoom) {
dis.dispatch({
action: 'view_room',
room_id: existingRoomId
room_id: this.state.existingOneToOneRoomId,
});
this.props.onFinished();
}
else {
self.setState({ updating: self.state.updating + 1 });
this.setState({ updating: this.state.updating + 1 });
createRoom({
createOpts: {
invite: [this.props.member.userId],
},
}).finally(function() {
self.props.onFinished();
self.setState({ updating: self.state.updating - 1 });
}).finally(() => {
this.props.onFinished();
this.setState({ updating: this.state.updating - 1 });
}).done();
}
},
@@ -535,7 +561,9 @@ module.exports = React.createClass({
return (
<div>
<h3>Devices</h3>
{devComponents}
<div className="mx_MemberInfo_devices">
{devComponents}
</div>
</div>
);
},
@@ -545,7 +573,22 @@ module.exports = React.createClass({
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
// FIXME: we're referring to a vector component from react-sdk
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
var label;
if (this.state.existingOneToOneRoomId) {
if (this.state.existingOneToOneRoomId == this.props.member.roomId) {
label = "Start new direct chat";
}
else {
label = "Go to direct chat";
}
}
else {
label = "Start direct chat";
}
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg"
label={ label } onClick={ this.onChatClick }/>
}
if (this.state.updating) {
@@ -594,6 +637,8 @@ module.exports = React.createClass({
</div>
}
let memberNameHTML = emojifyText(this.props.member.name);
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
return (
@@ -603,7 +648,7 @@ module.exports = React.createClass({
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
</div>
<h2>{ this.props.member.name }</h2>
<h2 dangerouslySetInnerHTML={memberNameHTML}></h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">

View File

@@ -54,7 +54,7 @@ module.exports = React.createClass({
this.memberDict = this.getMemberDict();
state.members = this.roomMembers(INITIAL_LOAD_NUM_MEMBERS);
state.members = this.roomMembers();
return state;
},
@@ -64,7 +64,10 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomState.events", this.onRoomStateEvent);
cli.on("Room", this.onRoom); // invites
cli.on("User.presence", this.onUserPresence);
// We listen for changes to the lastPresenceTs which is essentially
// listening for all presence events (we display most of not all of
// the information contained in presence events).
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.on("Room.timeline", this.onRoomTimeline);
},
@@ -75,24 +78,11 @@ module.exports = React.createClass({
cli.removeListener("RoomMember.name", this.onRoomMemberName);
cli.removeListener("RoomState.events", this.onRoomStateEvent);
cli.removeListener("Room", this.onRoom);
cli.removeListener("User.presence", this.onUserPresence);
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.removeListener("Room.timeline", this.onRoomTimeline);
}
},
componentDidMount: function() {
var self = this;
// Lazy-load in more than the first N members
setTimeout(function() {
if (!self.isMounted()) return;
// lazy load to prevent it blocking the first render
self.setState({
members: self.roomMembers()
});
}, 50);
},
/*
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// ignore anything but real-time updates at the end of the room:
@@ -121,7 +111,7 @@ module.exports = React.createClass({
},
*/
onUserPresence(event, user) {
onUserLastPresenceTs(event, user) {
// Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile
// evar attaching their own listener.
@@ -325,7 +315,7 @@ module.exports = React.createClass({
return all_members;
},
roomMembers: function(limit) {
roomMembers: function() {
var all_members = this.memberDict || {};
var all_user_ids = Object.keys(all_members);
var ConferenceHandler = CallHandler.getConferenceHandler();
@@ -334,7 +324,7 @@ module.exports = React.createClass({
var to_display = [];
var count = 0;
for (var i = 0; i < all_user_ids.length && (limit === undefined || count < limit); ++i) {
for (var i = 0; i < all_user_ids.length; ++i) {
var user_id = all_user_ids[i];
var m = all_members[user_id];
@@ -425,27 +415,7 @@ module.exports = React.createClass({
// For now, let's just order things by timestamp. It's really annoying
// that a user disappears from sight just because they temporarily go offline
/*
var presenceMap = {
online: 3,
unavailable: 2,
offline: 1
};
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
if (presenceOrdA != presenceOrdB) {
return presenceOrdB - presenceOrdA;
}
*/
var lastActiveTsA = userA && userA.lastActiveTs ? userA.lastActiveTs : 0;
var lastActiveTsB = userB && userB.lastActiveTs ? userB.lastActiveTs : 0;
// console.log("comparing ts: " + lastActiveTsA + " and " + lastActiveTsB);
return lastActiveTsB - lastActiveTsA;
return userB.getLastActiveTs() - userA.getLastActiveTs();
},
onSearchQueryChanged: function(input) {
@@ -462,9 +432,16 @@ module.exports = React.createClass({
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
if (query && m.name.toLowerCase().indexOf(query) === -1) {
return false;
if (query) {
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
if (!matchesName && !matchesId) {
return false;
}
}
return m.membership == membership;
}).map(function(userId) {
var m = self.memberDict[userId];
@@ -515,7 +492,7 @@ module.exports = React.createClass({
invitedSection = (
<div className="mx_MemberList_invited">
<h2>Invited</h2>
<div autoshow={true} className="mx_MemberList_wrapper">
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
</div>
</div>
@@ -544,7 +521,6 @@ module.exports = React.createClass({
<div className="mx_MemberList">
{inviteMemberListSection}
<GeminiScrollbar autoshow={true}
relayoutOnUpdate={false}
className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>

View File

@@ -20,54 +20,53 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
var dis = require('../../../dispatcher');
import Autocomplete from './Autocomplete';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({
displayName: 'MessageComposer',
export default class MessageComposer extends React.Component {
constructor(props, context) {
super(props, context);
this.onCallClick = this.onCallClick.bind(this);
this.onHangupClick = this.onHangupClick.bind(this);
this.onUploadClick = this.onUploadClick.bind(this);
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this._tryComplete = this._tryComplete.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
propTypes: {
tabComplete: React.PropTypes.any,
this.state = {
autocompleteQuery: '',
selection: null,
};
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
}
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// string representing the current voip call state
callState: React.PropTypes.string,
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
},
onUploadClick: function(ev) {
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guest users can't upload files. Please register to upload."
description: "Guest users can't upload files. Please register to upload.",
});
return;
}
this.refs.uploadInput.click();
},
}
onUploadFileSelected: function(ev) {
var files = ev.target.files;
onUploadFileSelected(ev) {
let files = ev.target.files;
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var TintableSvg = sdk.getComponent("elements.TintableSvg");
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let TintableSvg = sdk.getComponent("elements.TintableSvg");
var fileList = [];
for(var i=0; i<files.length; i++) {
let fileList = [];
for (let i=0; i<files.length; i++) {
fileList.push(<li>
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
</li>);
@@ -94,11 +93,11 @@ module.exports = React.createClass({
}
this.refs.uploadInput.value = null;
}
},
});
},
}
onHangupClick: function() {
onHangupClick() {
var call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall();
if (!call) {
@@ -108,27 +107,55 @@ module.exports = React.createClass({
action: 'hangup',
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId
room_id: call.roomId,
});
},
}
onCallClick: function(ev) {
onCallClick(ev) {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
}
onVoiceCallClick: function(ev) {
onVoiceCallClick(ev) {
dis.dispatch({
action: 'place_call',
type: 'voice',
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
}
render: function() {
onInputContentChanged(content: string, selection: {start: number, end: number}) {
this.setState({
autocompleteQuery: content,
selection,
});
}
onUpArrow() {
return this.refs.autocomplete.onUpArrow();
}
onDownArrow() {
return this.refs.autocomplete.onDownArrow();
}
_tryComplete(): boolean {
if (this.refs.autocomplete) {
return this.refs.autocomplete.onConfirm();
}
return false;
}
_onAutocompleteConfirm(range, completion) {
if (this.messageComposerInput) {
this.messageComposerInput.onConfirmAutocompletion(range, completion);
}
}
render() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
@@ -154,12 +181,12 @@ module.exports = React.createClass({
else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
<TintableSvg src="img/voice.svg" width="16" height="26"/>
</div>
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
</div>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
<TintableSvg src="img/call.svg" width="30" height="22"/>
</div>
<TintableSvg src="img/icons-video.svg" width="35" height="35"/>
</div>;
}
var canSendMessages = this.props.room.currentState.maySendMessage(
@@ -172,7 +199,7 @@ module.exports = React.createClass({
var uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title="Upload file">
<TintableSvg src="img/upload.svg" width="19" height="24"/>
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
<input ref="uploadInput" type="file"
style={uploadInputStyle}
multiple
@@ -181,8 +208,16 @@ module.exports = React.createClass({
);
controls.push(
<MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
onResize={this.props.onResize} room={this.props.room} />,
<MessageComposerInput
ref={c => this.messageComposerInput = c}
key="controls_input"
onResize={this.props.onResize}
room={this.props.room}
tryComplete={this._tryComplete}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
onContentChanged={this.onInputContentChanged} />,
uploadButton,
hangupButton,
callButton,
@@ -198,6 +233,13 @@ module.exports = React.createClass({
return (
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
<div className="mx_MessageComposer_autocomplete_wrapper">
<Autocomplete
ref="autocomplete"
onConfirm={this._onAutocompleteConfirm}
query={this.state.autocompleteQuery}
selection={this.state.selection} />
</div>
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
{controls}
@@ -206,5 +248,24 @@ module.exports = React.createClass({
</div>
);
}
});
};
MessageComposer.propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// string representing the current voip call state
callState: React.PropTypes.string,
// callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number
};

View File

@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
var marked = require("marked");
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
import marked from 'marked';
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
@@ -24,7 +24,7 @@ marked.setOptions({
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
smartypants: false,
});
import {Editor, EditorState, RichUtils, CompositeDecorator,
@@ -33,14 +33,14 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
import {stateToMarkdown} from 'draft-js-export-markdown';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
import Modal from '../../../Modal';
import sdk from '../../../index';
var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode");
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
import * as RichText from '../../../RichText';
@@ -49,8 +49,8 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const KEY_M = 77;
// FIXME Breaks markdown with multiple paragraphs, since it only strips first and last <p>
function mdownToHtml(mdown) {
var html = marked(mdown) || "";
function mdownToHtml(mdown: string): string {
let html = marked(mdown) || "";
html = html.trim();
// strip start and end <p> tags else you get 'orrible spacing
if (html.indexOf("<p>") === 0) {
@@ -66,23 +66,38 @@ function mdownToHtml(mdown) {
* The textInput part of the MessageComposer
*/
export default class MessageComposerInput extends React.Component {
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
}
return getDefaultKeyBinding(e);
}
client: MatrixClient;
constructor(props, context) {
super(props, context);
this.onAction = this.onAction.bind(this);
this.onInputClick = this.onInputClick.bind(this);
this.handleReturn = this.handleReturn.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.onChange = this.onChange.bind(this);
this.setEditorState = this.setEditorState.bind(this);
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this.onTab = this.onTab.bind(this);
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
if(isRichtextEnabled == null) {
if (isRichtextEnabled == null) {
isRichtextEnabled = 'true';
}
isRichtextEnabled = isRichtextEnabled === 'true';
this.state = {
isRichtextEnabled: isRichtextEnabled,
editorState: null
editorState: null,
};
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
@@ -91,15 +106,6 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get();
}
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
}
return getDefaultKeyBinding(e);
}
/**
* "Does the right thing" to create an EditorState, based on:
* - whether we've got rich text mode enabled
@@ -207,11 +213,9 @@ export default class MessageComposerInput extends React.Component {
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
if (contentJSON) {
let content = convertFromRaw(JSON.parse(contentJSON));
component.setState({
editorState: component.createEditorState(component.state.isRichtextEnabled, content)
});
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
}
}
},
};
}
@@ -233,7 +237,7 @@ export default class MessageComposerInput extends React.Component {
}
onAction(payload) {
var editor = this.refs.editor;
let editor = this.refs.editor;
switch (payload.action) {
case 'focus_composer':
@@ -251,7 +255,7 @@ export default class MessageComposerInput extends React.Component {
payload.displayname
);
this.setState({
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
});
editor.focus();
}
@@ -344,28 +348,31 @@ export default class MessageComposerInput extends React.Component {
this.refs.editor.focus();
}
onChange(editorState: EditorState) {
setEditorState(editorState: EditorState) {
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
this.setState({editorState});
if(editorState.getCurrentContent().hasText()) {
this.onTypingActivity()
if (editorState.getCurrentContent().hasText()) {
this.onTypingActivity();
} else {
this.onFinishedTyping();
}
if (this.props.onContentChanged) {
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
RichText.selectionStateToTextOffsets(editorState.getSelection(),
editorState.getCurrentContent().getBlocksAsArray()));
}
}
enableRichtext(enabled: boolean) {
if (enabled) {
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
this.setState({
editorState: this.createEditorState(enabled, RichText.HTMLtoContentState(html))
});
this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html)));
} else {
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()),
contentState = ContentState.createFromText(markdown);
this.setState({
editorState: this.createEditorState(enabled, contentState)
});
this.setEditorState(this.createEditorState(enabled, contentState));
}
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
@@ -376,7 +383,7 @@ export default class MessageComposerInput extends React.Component {
}
handleKeyCommand(command: string): boolean {
if(command === 'toggle-mode') {
if (command === 'toggle-mode') {
this.enableRichtext(!this.state.isRichtextEnabled);
return true;
}
@@ -384,7 +391,7 @@ export default class MessageComposerInput extends React.Component {
let newState: ?EditorState = null;
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
if(!this.state.isRichtextEnabled) {
if (!this.state.isRichtextEnabled) {
let contentState = this.state.editorState.getCurrentContent(),
selection = this.state.editorState.getSelection();
@@ -392,10 +399,10 @@ export default class MessageComposerInput extends React.Component {
bold: text => `**${text}**`,
italic: text => `*${text}*`,
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
code: text => `\`${text}\``
code: text => `\`${text}\``,
}[command];
if(modifyFn) {
if (modifyFn) {
newState = EditorState.push(
this.state.editorState,
RichText.modifyText(contentState, selection, modifyFn),
@@ -404,23 +411,26 @@ export default class MessageComposerInput extends React.Component {
}
}
if(newState == null)
if (newState == null)
newState = RichUtils.handleKeyCommand(this.state.editorState, command);
if (newState != null) {
this.onChange(newState);
this.setEditorState(newState);
return true;
}
return false;
}
handleReturn(ev) {
if(ev.shiftKey)
if (ev.shiftKey) {
return false;
}
const contentState = this.state.editorState.getCurrentContent();
if(!contentState.hasText())
if (!contentState.hasText()) {
return true;
}
let contentText = contentState.getPlainText(), contentHTML;
@@ -489,10 +499,47 @@ export default class MessageComposerInput extends React.Component {
return true;
}
onUpArrow(e) {
if (this.props.onUpArrow && this.props.onUpArrow()) {
e.preventDefault();
}
}
onDownArrow(e) {
if (this.props.onDownArrow && this.props.onDownArrow()) {
e.preventDefault();
}
}
onTab(e) {
if (this.props.tryComplete) {
if (this.props.tryComplete()) {
e.preventDefault();
}
}
}
onConfirmAutocompletion(range, content: string) {
let contentState = Modifier.replaceText(
this.state.editorState.getCurrentContent(),
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
content
);
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
this.setEditorState(editorState);
// for some reason, doing this right away does not update the editor :(
setTimeout(() => this.refs.editor.focus(), 50);
}
render() {
let className = "mx_MessageComposer_input";
if(this.state.isRichtextEnabled) {
if (this.state.isRichtextEnabled) {
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
}
@@ -502,11 +549,14 @@ export default class MessageComposerInput extends React.Component {
<Editor ref="editor"
placeholder="Type a message…"
editorState={this.state.editorState}
onChange={this.onChange}
onChange={this.setEditorState}
keyBindingFn={MessageComposerInput.getKeyBinding}
handleKeyCommand={this.handleKeyCommand}
handleReturn={this.handleReturn}
stripPastedStyles={!this.state.isRichtextEnabled}
onTab={this.onTab}
onUpArrow={this.onUpArrow}
onDownArrow={this.onDownArrow}
spellCheck={true} />
</div>
);
@@ -521,5 +571,15 @@ MessageComposerInput.propTypes = {
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired
room: React.PropTypes.object.isRequired,
// called with current plaintext content (as a string) whenever it changes
onContentChanged: React.PropTypes.func,
onUpArrow: React.PropTypes.func,
onDownArrow: React.PropTypes.func,
// attempts to confirm currently selected completion, returns whether actually confirmed
tryComplete: React.PropTypes.func,
};

View File

@@ -163,13 +163,13 @@ module.exports = React.createClass({
};
return (
<Velociraptor>
<Velociraptor
startStyles={this.state.startStyles}
enterTransitionOpts={this.state.enterTransitionOpts} >
<MemberAvatar
member={this.props.member}
width={14} height={14} resizeMethod="crop"
style={style}
startStyle={this.state.startStyles}
enterTransitionOpts={this.state.enterTransitionOpts}
onClick={this.props.onClick}
/>
</Velociraptor>

View File

@@ -24,6 +24,7 @@ var Modal = require("../../../Modal");
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
import {emojifyText} from '../../../HtmlUtils';
linkifyMatrix(linkify);
@@ -211,13 +212,12 @@ module.exports = React.createClass({
roomName = this.props.room.name;
}
let roomNameHTML = emojifyText(roomName);
name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName }>{ roomName }</div>
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ roomName } dangerouslySetInnerHTML={roomNameHTML}></div>
{ searchStatus }
<div className="mx_RoomHeader_settingsButton" title="Settings">
<TintableSvg src="img/settings.svg" width="12" height="12"/>
</div>
</div>
}
@@ -263,10 +263,18 @@ module.exports = React.createClass({
);
}
var settings_button;
if (this.props.onSettingsClick) {
settings_button =
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title="Settings">
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
</div>;
}
var leave_button;
if (this.props.onLeaveClick) {
leave_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
<div className="mx_RoomHeader_button" onClick={this.props.onLeaveClick} title="Leave room">
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</div>;
}
@@ -274,7 +282,7 @@ module.exports = React.createClass({
var forget_button;
if (this.props.onForgetClick) {
forget_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
<div className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title="Forget room">
<TintableSvg src="img/leave.svg" width="26" height="20"/>
</div>;
}
@@ -288,10 +296,11 @@ module.exports = React.createClass({
if (!this.props.editing) {
right_row =
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ forget_button }
{ leave_button }
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/search.svg" width="21" height="19"/>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
</div>
{ rightPanel_buttons }
</div>;

View File

@@ -268,9 +268,11 @@ module.exports = React.createClass({
},
_repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) {
// We access the parent of the parent, as the tooltip is inside a container
// Needs refactoring into a better multipurpose tooltip
if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) {
var scroll = ReactDOM.findDOMNode(this);
this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
}
},
@@ -325,7 +327,6 @@ module.exports = React.createClass({
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
relayoutOnUpdate={false}
autoshow={true} onScroll={ self._repositionTooltips } ref="gemscroll">
<div className="mx_RoomList">
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }

View File

@@ -33,16 +33,24 @@ module.exports = React.createClass({
// If invited by 3rd party invite, the email address the invite was sent to
invitedEmail: React.PropTypes.string,
canJoin: React.PropTypes.bool,
// A standard client/server API error object. If supplied, indicates that the
// caller was unable to fetch details about the room for the given reason.
error: React.PropTypes.object,
canPreview: React.PropTypes.bool,
spinner: React.PropTypes.bool,
room: React.PropTypes.object,
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: React.PropTypes.object,
},
getDefaultProps: function() {
return {
onJoinClick: function() {},
canJoin: false,
canPreview: true,
};
},
@@ -115,8 +123,24 @@ module.exports = React.createClass({
);
}
else if (this.props.canJoin) {
var name = this.props.room ? this.props.room.name : "";
else if (this.props.error) {
var name = this.props.roomAlias || "This room";
var error;
if (this.props.error.errcode == 'M_NOT_FOUND') {
error = name + " does not exist";
} else {
error = name + " is not accessible at this time";
}
joinBlock = (
<div>
<div className="mx_RoomPreviewBar_join_text">
{ error }
</div>
</div>
);
}
else {
var name = this.props.room ? this.props.room.name : (this.props.room_alias || "");
name = name ? <b>{ name }</b> : "a room";
joinBlock = (
<div>

View File

@@ -23,6 +23,14 @@ var Modal = require('../../../Modal');
var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher");
var ScalarAuthClient = require("../../../ScalarAuthClient");
var UserSettingsStore = require('../../../UserSettingsStore');
// parse a string as an integer; if the input is undefined, or cannot be parsed
// as an integer, return a default.
function parseIntWithDefault(val, def) {
var res = parseInt(val);
return isNaN(res) ? def : res;
}
module.exports = React.createClass({
displayName: 'RoomSettings',
@@ -59,9 +67,18 @@ module.exports = React.createClass({
tags_changed: false,
tags: tags,
areNotifsMuted: areNotifsMuted,
<<<<<<< HEAD
isRoomPublished: this._originalIsRoomPublished, // loaded async in componentWillMount
scalar_token: null,
scalar_error: null,
=======
// isRoomPublished is loaded async in componentWillMount so when the component
// inits, the saved value will always be undefined, however getInitialState()
// is also called from the saving code so we must return the correct value here
// if we have it (although this could race if the user saves before we load whether
// the room is published or not).
isRoomPublished: this._originalIsRoomPublished,
>>>>>>> develop
};
},
@@ -209,11 +226,17 @@ module.exports = React.createClass({
}
});
}
console.log("Performing %s operations", promises.length);
// color scheme
promises.push(this.saveColor());
// url preview settings
promises.push(this.saveUrlPreviewSettings());
// encryption
promises.push(this.saveEncryption());
console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises));
return q.allSettled(promises);
},
@@ -227,6 +250,24 @@ module.exports = React.createClass({
return this.refs.color_settings.saveSettings();
},
saveUrlPreviewSettings: function() {
if (!this.refs.url_preview_settings) { return q(); }
return this.refs.url_preview_settings.saveSettings();
},
saveEncryption: function () {
if (!this.refs.encrypt) { return q(); }
var encrypt = this.refs.encrypt.checked;
if (!encrypt) { return q(); }
var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.encryption",
{ algorithm: "m.olm.v1.curve25519-aes-sha2" }
);
},
_hasDiff: function(strA, strB) {
// treat undefined as an empty string because other components may blindly
// call setName("") when there has been no diff made to the name!
@@ -261,7 +302,7 @@ module.exports = React.createClass({
power_levels_changed: true
});
},
_yankValueFromEvent: function(stateEventType, keyName, defaultValue) {
// E.g.("m.room.name","name") would yank the "name" content key from "m.room.name"
var event = this.props.room.currentState.getStateEvents(stateEventType, '');
@@ -296,7 +337,7 @@ module.exports = React.createClass({
},
});
},
_onRoomAccessRadioToggle: function(ev) {
// join_rule
@@ -400,68 +441,72 @@ module.exports = React.createClass({
}, "");
},
_renderEncryptionSection: function() {
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
return null;
}
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
var text = "Encryption is " + (isEncrypted ? "" : "not ") +
"enabled in this room.";
var button;
if (!isEncrypted &&
roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
button = (
<label>
<input type="checkbox" ref="encrypt" />
Enable encryption (warning: cannot be disabled again!)
</label>
);
}
return (
<div className="mx_RoomSettings_toggles">
<h3>Encryption</h3>
<label>{text}</label>
{button}
</div>
);
},
render: function() {
// TODO: go through greying out things you don't have permission to change
// (or turning them into informative stuff)
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
var EditableText = sdk.getComponent('elements.EditableText');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var Loader = sdk.getComponent("elements.Spinner")
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
var events_levels = (power_levels ? power_levels.getContent().events : {}) || {};
var cli = MatrixClientPeg.get();
var roomState = this.props.room.currentState;
var user_id = cli.credentials.userId;
if (power_levels) {
power_levels = power_levels.getContent();
var power_level_event = roomState.getStateEvents('m.room.power_levels', '');
var power_levels = power_level_event ? power_level_event.getContent() : {};
var events_levels = power_levels.events || {};
var user_levels = power_levels.users || {};
var ban_level = parseInt(power_levels.ban);
var kick_level = parseInt(power_levels.kick);
var redact_level = parseInt(power_levels.redact);
var invite_level = parseInt(power_levels.invite || 0);
var send_level = parseInt(power_levels.events_default || 0);
var state_level = parseInt(power_levels.state_default || 50);
var default_user_level = parseInt(power_levels.users_default || 0);
var ban_level = parseIntWithDefault(power_levels.ban, 50);
var kick_level = parseIntWithDefault(power_levels.kick, 50);
var redact_level = parseIntWithDefault(power_levels.redact, 50);
var invite_level = parseIntWithDefault(power_levels.invite, 50);
var send_level = parseIntWithDefault(power_levels.events_default, 0);
var state_level = power_level_event ? parseIntWithDefault(power_levels.state_default, 50) : 0;
var default_user_level = parseIntWithDefault(power_levels.users_default, 0);
if (power_levels.ban == undefined) ban_level = 50;
if (power_levels.kick == undefined) kick_level = 50;
if (power_levels.redact == undefined) redact_level = 50;
var user_levels = power_levels.users || {};
var current_user_level = user_levels[user_id];
if (current_user_level == undefined) current_user_level = default_user_level;
var power_level_level = events_levels["m.room.power_levels"];
if (power_level_level == undefined) {
power_level_level = state_level;
}
var can_change_levels = current_user_level >= power_level_level;
} else {
var ban_level = 50;
var kick_level = 50;
var redact_level = 50;
var invite_level = 0;
var send_level = 0;
var state_level = 0;
var default_user_level = 0;
var user_levels = [];
var events_levels = [];
var current_user_level = 0;
var power_level_level = 0;
var can_change_levels = false;
var current_user_level = user_levels[user_id];
if (current_user_level === undefined) {
current_user_level = default_user_level;
}
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
var can_change_levels = roomState.mayClientSendStateEvent("m.room.power_levels", cli);
var canSetTag = !cli.isGuest();
@@ -530,7 +575,7 @@ module.exports = React.createClass({
var tagsSection = null;
if (canSetTag || self.state.tags) {
var tagsSection =
var tagsSection =
<div className="mx_RoomSettings_tags">
Tagged as: { canSetTag ?
(tags.map(function(tag, i) {
@@ -666,10 +711,6 @@ module.exports = React.createClass({
Members only (since they joined)
</label>
</div>
<label className="mx_RoomSettings_encrypt">
<input type="checkbox" />
Encrypt room
</label>
</div>
@@ -690,6 +731,8 @@ module.exports = React.createClass({
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<h3>Permissions</h3>
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel">
@@ -737,6 +780,8 @@ module.exports = React.createClass({
{ bannedUsersSection }
{ this._renderEncryptionSection() }
<h3>Advanced</h3>
<div className="mx_RoomSettings_settings">
This room's internal ID is <code>{ this.props.room.roomId }</code>

View File

@@ -21,6 +21,8 @@ var classNames = require('classnames');
var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({
displayName: 'RoomTile',
@@ -42,13 +44,48 @@ module.exports = React.createClass({
},
getInitialState: function() {
return( { hover : false });
var areNotifsMuted = false;
var cli = MatrixClientPeg.get();
if (!cli.isGuest()) {
var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
}
}
return({
hover : false,
badgeHover : false,
menu: false,
areNotifsMuted: areNotifsMuted,
});
},
onAction: function(payload) {
switch (payload.action) {
case 'notification_change':
// Is the notification about this room?
if (payload.roomId === this.props.room.roomId) {
this.setState( { areNotifsMuted : payload.isMuted });
}
break;
}
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onClick: function() {
dis.dispatch({
action: 'view_room',
room_id: this.props.room.roomId
room_id: this.props.room.roomId,
});
},
@@ -60,6 +97,48 @@ module.exports = React.createClass({
this.setState( { hover : false });
},
badgeOnMouseEnter: function() {
// Only allow none guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
}
},
badgeOnMouseLeave: function() {
this.setState( { badgeHover : false } );
},
onBadgeClicked: function(e) {
// Only allow none guests to access the context menu
if (!MatrixClientPeg.get().isGuest()) {
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
}
var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu');
var elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
var x = elementRect.right + window.pageXOffset + 3;
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
var self = this;
ContextualMenu.createMenu(Menu, {
menuWidth: 188,
menuHeight: 126,
chevronOffset: 45,
left: x,
top: y,
room: this.props.room,
onFinished: function() {
self.setState({ menu: false });
}
});
this.setState({ menu: true });
}
},
render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId];
@@ -72,42 +151,64 @@ module.exports = React.createClass({
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notificationCount > 0,
'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0),
'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_menu': this.state.menu,
});
var avatarClasses = classNames({
'mx_RoomTile_avatar': true,
'mx_RoomTile_mute': this.state.areNotifsMuted,
});
var badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu,
'mx_RoomTile_badgeMute': this.state.areNotifsMuted,
});
// XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k)
var name = this.props.room.name || this.props.room.roomId;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge;
if (this.props.highlight || notificationCount > 0) {
badge = <div className="mx_RoomTile_badge">{ notificationCount ? notificationCount : '!' }</div>;
var badgeContent;
if (this.state.badgeHover || this.state.menu) {
badgeContent = "\u00B7\u00B7\u00B7";
} else if (this.props.highlight || notificationCount > 0) {
var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
badgeContent = notificationCount ? limitedCount : '!';
} else {
badgeContent = '\u200B';
}
/*
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge">!</div>;
if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) {
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}><img className="mx_RoomTile_badgeIcon" src="img/icon-context-mute.svg" width="16" height="12" /></div>;
} else {
badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
}
else if (this.props.unread) {
badge = <div className="mx_RoomTile_badge">1</div>;
}
var nameCell;
if (badge) {
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
}
else {
nameCell = <div className="mx_RoomTile_name">{name}</div>;
}
*/
var label;
var tooltip;
if (!this.props.collapsed) {
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
var nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_mute': this.state.areNotifsMuted,
'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted,
});
let nameHTML = emojifyText(name);
if (this.props.selected) {
name = <span>{ name }</span>;
let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
} else {
label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>;
}
label = <div className={ className }>{ name }</div>;
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
@@ -129,13 +230,16 @@ module.exports = React.createClass({
var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget(
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} width={24} height={24} />
<div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} />
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ label }
{ badge }
{ incomingCallBox }
{ tooltip }
</div>
));
}

View File

@@ -179,7 +179,6 @@ var SearchableEntityList = React.createClass({
}
list = (
<GeminiScrollbar autoshow={true}
relayoutOnUpdate={false}
className="mx_SearchableEntityList_listWrapper">
{ list }
</GeminiScrollbar>

View File

@@ -24,17 +24,17 @@ module.exports = React.createClass({
displayName: 'TabCompleteBar',
propTypes: {
entries: React.PropTypes.array.isRequired
tabComplete: React.PropTypes.object.isRequired
},
render: function() {
return (
<div className="mx_TabCompleteBar">
{this.props.entries.map(function(entry, i) {
{this.props.tabComplete.peek(6).map((entry, i) => {
return (
<div key={entry.getKey() || i + ""}
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={entry.onClick.bind(entry)} >
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
{entry.getImageJsx()}
<span className="mx_TabCompleteBar_text">
{entry.getText()}

View File

@@ -21,29 +21,10 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({
displayName: 'ChangeDisplayName',
propTypes: {
onFinished: React.PropTypes.func
},
getDefaultProps: function() {
return {
onFinished: function() {},
};
},
getInitialState: function() {
return {
busy: false,
errorString: null
}
},
componentWillMount: function() {
_getDisplayName: function() {
var cli = MatrixClientPeg.get();
this.setState({busy: true});
var self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
return cli.getProfileInfo(cli.credentials.userId).then(function(result) {
var displayname = result.displayname;
if (!displayname) {
if (MatrixClientPeg.get().isGuest()) {
@@ -53,68 +34,26 @@ module.exports = React.createClass({
displayname = MatrixClientPeg.get().getUserIdLocalpart();
}
}
self.setState({
displayName: displayname,
busy: false
});
return displayname;
}, function(error) {
self.setState({
errorString: "Failed to fetch display name",
busy: false
});
throw new Error("Failed to fetch display name");
});
},
changeDisplayname: function(new_displayname) {
this.setState({
busy: true,
errorString: null,
})
var self = this;
MatrixClientPeg.get().setDisplayName(new_displayname).then(function() {
self.setState({
busy: false,
displayName: new_displayname
});
}, function(error) {
self.setState({
busy: false,
errorString: "Failed to set display name"
});
_changeDisplayName: function(new_displayname) {
var cli = MatrixClientPeg.get();
return cli.setDisplayName(new_displayname).catch(function(e) {
throw new Error("Failed to set display name");
});
},
edit: function() {
this.refs.displayname_edit.edit()
},
onValueChanged: function(new_value, shouldSubmit) {
if (shouldSubmit) {
this.changeDisplayname(new_value);
}
},
render: function() {
if (this.state.busy) {
var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
} else if (this.state.errorString) {
return (
<div className="error">{this.state.errorString}</div>
);
} else {
var EditableText = sdk.getComponent('elements.EditableText');
return (
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
className="mx_EditableText"
placeholderClassName="mx_EditableText_placeholder"
placeholder="No display name"
onValueChanged={this.onValueChanged} />
);
}
var EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
return (
<EditableTextContainer
getInitialValue={this._getDisplayName}
placeholder="No display name"
onSubmit={this._changeDisplayName} />
);
}
});

View File

@@ -0,0 +1,138 @@
/*
Copyright 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.
*/
import React from 'react';
import classNames from 'classnames';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
export default class DevicesPanel extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
devices: undefined,
deviceLoadError: undefined,
};
this._unmounted = false;
this._renderDevice = this._renderDevice.bind(this);
}
componentDidMount() {
this._loadDevices();
}
componentWillUnmount() {
this._unmounted = true;
}
_loadDevices() {
MatrixClientPeg.get().getDevices().done(
(resp) => {
if (this._unmounted) { return; }
this.setState({devices: resp.devices || []});
},
(error) => {
if (this._unmounted) { return; }
var errtxt;
if (err.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API.
errtxt = "Your home server does not support device management.";
} else {
console.error("Error loading devices:", error);
errtxt = "Unable to load device list.";
}
this.setState({deviceLoadError: errtxt});
}
);
}
/**
* compare two devices, sorting from most-recently-seen to least-recently-seen
* (and then, for stability, by device id)
*/
_deviceCompare(a, b) {
// return < 0 if a comes before b, > 0 if a comes after b.
const lastSeenDelta =
(b.last_seen_ts || 0) - (a.last_seen_ts || 0);
if (lastSeenDelta !== 0) { return lastSeenDelta; }
const idA = a.device_id;
const idB = b.device_id;
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
}
_onDeviceDeleted(device) {
if (this._unmounted) { return; }
// delete the removed device from our list.
const removed_id = device.device_id;
this.setState((state, props) => {
const newDevices = state.devices.filter(
d => { return d.device_id != removed_id }
);
return { devices: newDevices };
});
}
_renderDevice(device) {
var DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
return (
<DevicesPanelEntry key={device.device_id} device={device}
onDeleted={()=>{this._onDeviceDeleted(device)}} />
);
}
render() {
const Spinner = sdk.getComponent("elements.Spinner");
if (this.state.deviceLoadError !== undefined) {
const classes = classNames(this.props.className, "error");
return (
<div className={classes}>
{this.state.deviceLoadError}
</div>
);
}
const devices = this.state.devices;
if (devices === undefined) {
// still loading
const classes = this.props.className;
return <Spinner className={classes}/>;
}
devices.sort(this._deviceCompare);
const classes = classNames(this.props.className, "mx_DevicesPanel");
return (
<div className={classes}>
<div className="mx_DevicesPanel_header">
<div className="mx_DevicesPanel_deviceName">Name</div>
<div className="mx_DevicesPanel_deviceLastSeen">Last seen</div>
<div className="mx_DevicesPanel_deviceButtons"></div>
</div>
{devices.map(this._renderDevice)}
</div>
);
}
}

View File

@@ -0,0 +1,136 @@
/*
Copyright 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.
*/
import React from 'react';
import classNames from 'classnames';
import q from 'q';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DateUtils from '../../../DateUtils';
export default class DevicesPanelEntry extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
deleting: false,
deleteError: undefined,
};
this._unmounted = false;
this._onDeleteClick = this._onDeleteClick.bind(this);
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
}
componentWillUnmount() {
this._unmounted = true;
}
_onDisplayNameChanged(value) {
const device = this.props.device;
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
display_name: value,
}).catch((e) => {
console.error("Error setting device display name", e);
throw new Error("Failed to set display name");
});
}
_onDeleteClick() {
const device = this.props.device;
this.setState({deleting: true});
MatrixClientPeg.get().deleteDevice(device.device_id).done(
() => {
this.props.onDeleted();
if (this._unmounted) { return; }
this.setState({ deleting: false });
},
(e) => {
console.error("Error deleting device", e);
if (this._unmounted) { return; }
this.setState({
deleting: false,
deleteError: "Failed to delete device",
});
}
);
}
render() {
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
const device = this.props.device;
if (this.state.deleting) {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className="mx_DevicesPanel_device">
<Spinner />
</div>
);
}
let lastSeen = "";
if (device.last_seen_ts) {
// todo: format the timestamp as "5 minutes ago" or whatever.
const lastSeenDate = new Date(device.last_seen_ts);
lastSeen = device.last_seen_ip + " @ " +
lastSeenDate.toLocaleString();
}
let deleteButton;
if (this.state.deleteError) {
deleteButton = <div className="error">{this.state.deleteError}</div>
} else {
deleteButton = (
<div className="textButton"
onClick={this._onDeleteClick}>
Delete
</div>
);
}
return (
<div className="mx_DevicesPanel_device">
<div className="mx_DevicesPanel_deviceName">
<EditableTextContainer initialValue={device.display_name}
onSubmit={this._onDisplayNameChanged}
placeholder={device.device_id}
/>
</div>
<div className="mx_DevicesPanel_lastSeen">
{lastSeen}
</div>
<div className="mx_DevicesPanel_deviceButtons">
{deleteButton}
</div>
</div>
);
}
}
DevicesPanelEntry.propTypes = {
device: React.PropTypes.object.isRequired,
onDeleted: React.PropTypes.func,
};
DevicesPanelEntry.defaultProps = {
onDeleted: function() {},
};