Merge remote-tracking branch 'origin/develop' into dbkr/email_notifs

This commit is contained in:
David Baker 2016-04-21 10:12:27 +01:00
commit 3381e2b057
36 changed files with 908 additions and 357 deletions

View file

@ -1,3 +1,54 @@
Changes in [0.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.1) (2016-04-19)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.4.0...v0.5.1)
* Upgrade to react 15.0
* Fix many thinkos in sorting the MemberList
[\#275](https://github.com/matrix-org/matrix-react-sdk/pull/275)
* Don't setState after unmounting a component
[\#276](https://github.com/matrix-org/matrix-react-sdk/pull/276)
* Drop workaround for object.onLoad
[\#274](https://github.com/matrix-org/matrix-react-sdk/pull/274)
* Make sure that we update the room name
[\#272](https://github.com/matrix-org/matrix-react-sdk/pull/272)
* Matthew/design tweaks
[\#273](https://github.com/matrix-org/matrix-react-sdk/pull/273)
* Hack around absence of String.codePointAt on PhantomJS
[\#271](https://github.com/matrix-org/matrix-react-sdk/pull/271)
* RoomView: Handle joining federated rooms
[\#270](https://github.com/matrix-org/matrix-react-sdk/pull/270)
* Stop the MatrixClient when the MatrixChat is unmounted
[\#269](https://github.com/matrix-org/matrix-react-sdk/pull/269)
* make the UI fadable to help with decluttering
[\#268](https://github.com/matrix-org/matrix-react-sdk/pull/268)
* URL previewing support
[\#260](https://github.com/matrix-org/matrix-react-sdk/pull/260)
* Remember to load new timeline events
[\#267](https://github.com/matrix-org/matrix-react-sdk/pull/267)
* Stop trying to paginate after we get a failure
[\#265](https://github.com/matrix-org/matrix-react-sdk/pull/265)
* Improvements to the react-sdk test framework
[\#264](https://github.com/matrix-org/matrix-react-sdk/pull/264)
* Fix password resetting
[\#263](https://github.com/matrix-org/matrix-react-sdk/pull/263)
* Catch pageup/down and ctrl-home/end at the top level
[\#262](https://github.com/matrix-org/matrix-react-sdk/pull/262)
* Fix an issue where the scroll stopped working.
[\#261](https://github.com/matrix-org/matrix-react-sdk/pull/261)
* Fix a bug where we tried to show two ghost read markers at once.
[\#254](https://github.com/matrix-org/matrix-react-sdk/pull/254)
* File upload improvements
[\#258](https://github.com/matrix-org/matrix-react-sdk/pull/258)
* Show full-size avatar on MemberInfo avatar click
[\#257](https://github.com/matrix-org/matrix-react-sdk/pull/257)
* Whitelist \<u> tag
[\#256](https://github.com/matrix-org/matrix-react-sdk/pull/256)
* Don't reload the DOM if we can jump straight to the RM
[\#253](https://github.com/matrix-org/matrix-react-sdk/pull/253)
[0.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.0) was
incorrectly released.
Changes in [0.4.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.4.0) (2016-03-30) Changes in [0.4.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.4.0) (2016-03-30)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.3.1...v0.4.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.3.1...v0.4.0)

View file

@ -147,6 +147,11 @@ module.exports = function (config) {
}, },
resolve: { resolve: {
alias: { alias: {
// alias any requires to the react module to the one in our
// path, otherwise we tend to get the react source included
// twice when using npm link.
react: path.resolve('./node_modules/react'),
'matrix-react-sdk': path.resolve('test/skinned-sdk.js'), 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
'sinon': 'sinon/pkg/sinon.js', 'sinon': 'sinon/pkg/sinon.js',
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.4.0", "version": "0.5.1",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -17,7 +17,7 @@
"build": "babel src -d lib --source-maps", "build": "babel src -d lib --source-maps",
"start": "babel src -w -d lib --source-maps", "start": "babel src -w -d lib --source-maps",
"clean": "rimraf lib", "clean": "rimraf lib",
"prepublish": "npm run build; git rev-parse HEAD > git-revision.txt", "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt",
"test": "karma start --browsers PhantomJS", "test": "karma start --browsers PhantomJS",
"test-multi": "karma start --single-run=false" "test-multi": "karma start --single-run=false"
}, },
@ -30,20 +30,20 @@
"highlight.js": "^8.9.1", "highlight.js": "^8.9.1",
"linkifyjs": "^2.0.0-beta.4", "linkifyjs": "^2.0.0-beta.4",
"marked": "^0.3.5", "marked": "^0.3.5",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "^0.5.2",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"q": "^1.4.1", "q": "^1.4.1",
"react": "^0.14.2", "react": "^15.0.1",
"react-dom": "^0.14.2", "react-dom": "^15.0.1",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#869a86b", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"velocity-animate": "^1.2.3", "velocity-animate": "^1.2.3",
"velocity-ui-pack": "^1.2.2" "velocity-ui-pack": "^1.2.2"
}, },
"//babelversion": [ "//babelversion": [
"brief experiments with babel6 seems to show that it generates source ", "brief experiments with babel6 seems to show that it generates source ",
"maps which confuse chrome and make setting breakpoints tricky. So ", "maps which confuse chrome and make setting breakpoints tricky. So ",
"let's stick with v5 for now." "let's stick with v5 for now."
], ],
"devDependencies": { "devDependencies": {
"babel": "^5.8.23", "babel": "^5.8.23",
@ -61,7 +61,7 @@
"karma-webpack": "^1.7.0", "karma-webpack": "^1.7.0",
"mocha": "^2.4.5", "mocha": "^2.4.5",
"phantomjs-prebuilt": "^2.1.7", "phantomjs-prebuilt": "^2.1.7",
"react-addons-test-utils": "^0.14.7", "react-addons-test-utils": "^15.0.1",
"require-json": "0.0.1", "require-json": "0.0.1",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"sinon": "^1.17.3", "sinon": "^1.17.3",

View file

@ -37,7 +37,7 @@ if (packageJson['matrix-react-parent']) {
strm.write("module.exports.components = {};\n"); strm.write("module.exports.components = {};\n");
} }
var files = glob.sync('**/*.js', {cwd: componentsDir}); var files = glob.sync('**/*.js', {cwd: componentsDir}).sort();
for (var i = 0; i < files.length; ++i) { for (var i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', ''); var file = files[i].replace('.js', '');

View file

@ -74,10 +74,13 @@ class ContentMessages {
var def = q.defer(); var def = q.defer();
if (file.type.indexOf('image/') == 0) { if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image'; content.msgtype = 'm.image';
infoForImageFile(file).then(function(imageInfo) { infoForImageFile(file).then(function (imageInfo) {
extend(content.info, imageInfo); extend(content.info, imageInfo);
def.resolve(); def.resolve();
}); });
} else if (file.type.indexOf('audio/') == 0) {
content.msgtype = 'm.audio';
def.resolve();
} else { } else {
content.msgtype = 'm.file'; content.msgtype = 'm.file';
def.resolve(); def.resolve();

View file

@ -15,11 +15,14 @@ limitations under the License.
*/ */
var dis = require("./dispatcher"); var dis = require("./dispatcher");
var sdk = require("./index");
// FIXME: these vars should be bundled up and attached to // FIXME: these vars should be bundled up and attached to
// module.exports otherwise this will break when included by both // module.exports otherwise this will break when included by both
// react-sdk and apps layered on top. // react-sdk and apps layered on top.
var DEBUG = 0;
// The colour keys to be replaced as referred to in SVGs // The colour keys to be replaced as referred to in SVGs
var keyRgb = [ var keyRgb = [
"rgb(118, 207, 166)", // Vector Green "rgb(118, 207, 166)", // Vector Green
@ -75,6 +78,7 @@ var svgAttrs = [
var cached = false; var cached = false;
function calcCssFixups() { function calcCssFixups() {
if (DEBUG) console.log("calcSvgFixups start");
for (var i = 0; i < document.styleSheets.length; i++) { for (var i = 0; i < document.styleSheets.length; i++) {
var ss = document.styleSheets[i]; var ss = document.styleSheets[i];
if (!ss) continue; // well done safari >:( if (!ss) continue; // well done safari >:(
@ -105,13 +109,16 @@ function calcCssFixups() {
} }
} }
} }
if (DEBUG) console.log("calcSvgFixups end");
} }
function applyCssFixups() { function applyCssFixups() {
if (DEBUG) console.log("applyCssFixups start");
for (var i = 0; i < cssFixups.length; i++) { for (var i = 0; i < cssFixups.length; i++) {
var cssFixup = cssFixups[i]; var cssFixup = cssFixups[i];
cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
} }
if (DEBUG) console.log("applyCssFixups end");
} }
function hexToRgb(color) { function hexToRgb(color) {
@ -135,6 +142,7 @@ function rgbToHex(rgb) {
module.exports = { module.exports = {
tint: function(primaryColor, secondaryColor, tertiaryColor) { tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) { if (!cached) {
calcCssFixups(); calcCssFixups();
cached = true; cached = true;
@ -173,11 +181,19 @@ module.exports = {
colors = [primaryColor, secondaryColor, tertiaryColor]; colors = [primaryColor, secondaryColor, tertiaryColor];
if (DEBUG) console.log("Tinter.tint");
// go through manually fixing up the stylesheets. // go through manually fixing up the stylesheets.
applyCssFixups(); applyCssFixups();
// tell all the SVGs to go fix themselves up // tell all the SVGs to go fix themselves up
dis.dispatch({ action: 'tint_update' }); // we don't do this as a dispatch otherwise it will visually lag
var TintableSvg = sdk.getComponent("elements.TintableSvg");
if (TintableSvg.mounts) {
Object.keys(TintableSvg.mounts).forEach((id) => {
TintableSvg.mounts[id].tint();
});
}
}, },
// XXX: we could just move this all into TintableSvg, but as it's so similar // XXX: we could just move this all into TintableSvg, but as it's so similar
@ -189,6 +205,7 @@ module.exports = {
// updated would be a PITA, so just brute-force search for the // updated would be a PITA, so just brute-force search for the
// key colour; cache the element and apply. // key colour; cache the element and apply.
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
var fixups = []; var fixups = [];
for (var i = 0; i < svgs.length; i++) { for (var i = 0; i < svgs.length; i++) {
var svgDoc; var svgDoc;
@ -223,14 +240,17 @@ module.exports = {
} }
} }
} }
if (DEBUG) console.log("calcSvgFixups end");
return fixups; return fixups;
}, },
applySvgFixups: function(fixups) { applySvgFixups: function(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (var i = 0; i < fixups.length; i++) { for (var i = 0; i < fixups.length; i++) {
var svgFixup = fixups[i]; var svgFixup = fixups[i];
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
} }
if (DEBUG) console.log("applySvgFixups end");
}, },
}; };

View file

@ -68,6 +68,7 @@ module.exports.components['views.messages.MFileBody'] = require('./components/vi
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody');
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');

View file

@ -67,6 +67,8 @@ module.exports = React.createClass({
collapse_rhs: false, collapse_rhs: false,
ready: false, ready: false,
width: 10000, width: 10000,
sideOpacity: 1.0,
middleOpacity: 1.0,
}; };
if (s.logged_in) { if (s.logged_in) {
if (MatrixClientPeg.get().getRooms().length) { if (MatrixClientPeg.get().getRooms().length) {
@ -183,6 +185,7 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
this._stopMatrixClient();
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown); document.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
@ -258,12 +261,7 @@ module.exports = React.createClass({
window.localStorage.setItem("mx_hs_url", hsUrl); window.localStorage.setItem("mx_hs_url", hsUrl);
window.localStorage.setItem("mx_is_url", isUrl); window.localStorage.setItem("mx_is_url", isUrl);
} }
Notifier.stop(); this._stopMatrixClient();
UserActivity.stop();
Presence.stop();
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.unset();
this.notifyNewScreen('login'); this.notifyNewScreen('login');
this.replaceState({ this.replaceState({
logged_in: false, logged_in: false,
@ -369,7 +367,7 @@ module.exports = React.createClass({
onFinished: function(should_leave) { onFinished: function(should_leave) {
if (should_leave) { if (should_leave) {
var d = MatrixClientPeg.get().leave(roomId); var d = MatrixClientPeg.get().leave(roomId);
// FIXME: controller shouldn't be loading a view :( // FIXME: controller shouldn't be loading a view :(
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
var modal = Modal.createDialog(Loader); var modal = Modal.createDialog(Loader);
@ -534,6 +532,12 @@ module.exports = React.createClass({
collapse_rhs: false, collapse_rhs: false,
}); });
break; break;
case 'ui_opacity':
this.setState({
sideOpacity: payload.sideOpacity,
middleOpacity: payload.middleOpacity,
});
break;
} }
}, },
@ -596,13 +600,15 @@ module.exports = React.createClass({
var theAlias = MatrixTools.getCanonicalAliasForRoom(room); var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
if (theAlias) presentedId = theAlias; if (theAlias) presentedId = theAlias;
var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); // No need to do this given RoomView triggers it itself...
var color_scheme = {}; // var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
if (color_scheme_event) { // var color_scheme = {};
color_scheme = color_scheme_event.getContent(); // if (color_scheme_event) {
// XXX: we should validate the event // color_scheme = color_scheme_event.getContent();
} // // XXX: we should validate the event
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); // }
// console.log("Tinter.tint from _viewRoom");
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
} }
if (eventId) { if (eventId) {
@ -624,10 +630,13 @@ module.exports = React.createClass({
if (!this.refs.roomView) { if (!this.refs.roomView) {
return; return;
} }
var roomview = this.refs.roomView; var roomview = this.refs.roomView;
var roomId = this.refs.roomView.getRoomId();
if (!roomId) {
return;
}
var state = roomview.getScrollState(); var state = roomview.getScrollState();
this.scrollStateMap[roomview.props.roomId] = state; this.scrollStateMap[roomId] = state;
}, },
onLoggedIn: function(credentials) { onLoggedIn: function(credentials) {
@ -722,6 +731,16 @@ module.exports = React.createClass({
}); });
}, },
// stop all the background processes related to the current client
_stopMatrixClient: function() {
Notifier.stop();
UserActivity.stop();
Presence.stop();
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().removeAllListeners();
MatrixClientPeg.unset();
},
onKeyDown: function(ev) { onKeyDown: function(ev) {
/* /*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
@ -887,7 +906,7 @@ module.exports = React.createClass({
dis.dispatch({ dis.dispatch({
action: 'view_user', action: 'view_user',
member: member, member: member,
}); });
}, },
onLogoutClick: function(event) { onLogoutClick: function(event) {
@ -1008,6 +1027,7 @@ module.exports = React.createClass({
onUserSettingsClose: function() { onUserSettingsClose: function() {
// XXX: use browser history instead to find the previous room? // XXX: use browser history instead to find the previous room?
// or maintain a this.state.pageHistory in _setPage()?
if (this.state.currentRoom) { if (this.state.currentRoom) {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
@ -1034,7 +1054,7 @@ module.exports = React.createClass({
var NewVersionBar = sdk.getComponent('globals.NewVersionBar'); var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
// work out the HS URL prompts we should show for // work out the HS URL prompts we should show for
// needs to be before normal PageTypes as you are logged in technically // needs to be before normal PageTypes as you are logged in technically
if (this.state.screen == 'post_registration') { if (this.state.screen == 'post_registration') {
@ -1052,29 +1072,29 @@ module.exports = React.createClass({
page_element = ( page_element = (
<RoomView <RoomView
ref="roomView" ref="roomView"
roomId={this.state.currentRoom} roomAddress={this.state.currentRoom || this.state.currentRoomAlias}
roomAlias={this.state.currentRoomAlias}
eventId={this.state.initialEventId} eventId={this.state.initialEventId}
thirdPartyInvite={this.state.thirdPartyInvite} thirdPartyInvite={this.state.thirdPartyInvite}
oobData={this.state.roomOobData} oobData={this.state.roomOobData}
highlightedEventId={this.state.highlightedEventId} highlightedEventId={this.state.highlightedEventId}
eventPixelOffset={this.state.initialEventPixelOffset} eventPixelOffset={this.state.initialEventPixelOffset}
key={this.state.currentRoom} key={this.state.currentRoom}
opacity={this.state.middleOpacity}
ConferenceHandler={this.props.ConferenceHandler} /> ConferenceHandler={this.props.ConferenceHandler} />
); );
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} /> right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity} />
break; break;
case this.PageTypes.UserSettings: case this.PageTypes.UserSettings:
page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} /> page_element = <UserSettings onClose={this.onUserSettingsClose} version={this.state.version} />
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/> right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
break; break;
case this.PageTypes.CreateRoom: case this.PageTypes.CreateRoom:
page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/> page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/>
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/> right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
break; break;
case this.PageTypes.RoomDirectory: case this.PageTypes.RoomDirectory:
page_element = <RoomDirectory /> page_element = <RoomDirectory />
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/> right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
break; break;
} }
@ -1098,7 +1118,7 @@ module.exports = React.createClass({
<div className="mx_MatrixChat_wrapper"> <div className="mx_MatrixChat_wrapper">
{topBar} {topBar}
<div className={bodyClasses}> <div className={bodyClasses}>
<LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} /> <LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} opacity={this.state.sideOpacity}/>
<main className="mx_MatrixChat_middlePanel"> <main className="mx_MatrixChat_middlePanel">
{page_element} {page_element}
</main> </main>

View file

@ -19,6 +19,8 @@ var ReactDOM = require("react-dom");
var dis = require("../../dispatcher"); var dis = require("../../dispatcher");
var sdk = require('../../index'); var sdk = require('../../index');
var MatrixClientPeg = require('../../MatrixClientPeg')
/* (almost) stateless UI component which builds the event tiles in the room timeline. /* (almost) stateless UI component which builds the event tiles in the room timeline.
*/ */
module.exports = React.createClass({ module.exports = React.createClass({
@ -65,6 +67,9 @@ module.exports = React.createClass({
// callback which is called when more content is needed. // callback which is called when more content is needed.
onFillRequest: React.PropTypes.func, onFillRequest: React.PropTypes.func,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -147,7 +152,7 @@ module.exports = React.createClass({
this.refs.scrollPanel.scrollToBottom(); this.refs.scrollPanel.scrollToBottom();
} }
}, },
/** /**
* Page up/down. * Page up/down.
* *
@ -332,13 +337,17 @@ module.exports = React.createClass({
// Local echos have a send "status". // Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId; var scrollToken = mxEv.status ? undefined : eventId;
var readReceipts = this._getReadReceiptsForEvent(mxEv);
ret.push( ret.push(
<li key={eventId} <li key={eventId}
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}> data-scroll-token={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation} <EventTile mxEvent={mxEv} continuation={continuation}
onWidgetLoad={this._onWidgetLoad} onWidgetLoad={this._onWidgetLoad}
last={last} isSelectedEvent={highlight} /> readReceipts={readReceipts}
eventSendStatus={mxEv.status}
last={last} isSelectedEvent={highlight}/>
</li> </li>
); );
@ -356,6 +365,30 @@ module.exports = React.createClass({
!== new Date(nextEventTs).toDateString()); !== new Date(nextEventTs).toDateString());
}, },
// get a list of the userids whose read receipts should
// be shown next to this event
_getReadReceiptsForEvent: function(event) {
var myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
var room = MatrixClientPeg.get().getRoom(event.getRoomId());
if (!room) {
// huh.
return null;
}
return room.getReceiptsForEvent(event).filter(function(r) {
return r.type === "m.read" && r.userId != myUserId;
}).sort(function(r1, r2) {
return r2.data.ts - r1.data.ts;
}).map(function(r) {
return room.getMember(r.userId);
}).filter(function(m) {
// check that the user is a known room member
return m;
});
},
_getReadMarkerTile: function(visible) { _getReadMarkerTile: function(visible) {
var hr; var hr;
if (visible) { if (visible) {
@ -423,12 +456,15 @@ module.exports = React.createClass({
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>; bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
} }
var style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity;
return ( return (
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel" <ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable"
onScroll={ this.props.onScroll } onScroll={ this.props.onScroll }
onResize={ this.onResize } onResize={ this.onResize }
onFillRequest={ this.props.onFillRequest } onFillRequest={ this.props.onFillRequest }
style={ this.props.hidden ? { display: 'none' } : {} } style={ style }
stickyBottom={ this.props.stickyBottom }> stickyBottom={ this.props.stickyBottom }>
{topSpinner} {topSpinner}
{this._getEventTiles()} {this._getEventTiles()}

View file

@ -54,11 +54,15 @@ module.exports = React.createClass({
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, ConferenceHandler: React.PropTypes.any,
roomId: React.PropTypes.string.isRequired, // the ID for this room (or, if we don't know it, an alias for it)
//
// if we are referring to this room by a given alias (e.g. in the URL), track it. // XXX: if this is an alias, we will display a 'join' dialogue,
// useful for joining rooms by alias correctly (and fixing https://github.com/vector-im/vector-web/issues/819) // regardless of whether we are already a member, or if the room is
roomAlias: React.PropTypes.string, // peekable. Currently there is a big mess, where at least four
// different components (RoomView, MatrixChat, RoomDirectory,
// SlashCommands) have logic for turning aliases into rooms, and each
// of them do it differently and have different edge cases.
roomAddress: React.PropTypes.string.isRequired,
// An object representing a third party invite to join this room // An object representing a third party invite to join this room
// Fields: // Fields:
@ -90,10 +94,13 @@ module.exports = React.createClass({
// ID of an event to highlight. If undefined, no event will be highlighted. // ID of an event to highlight. If undefined, no event will be highlighted.
// Typically this will either be the same as 'eventId', or undefined. // Typically this will either be the same as 'eventId', or undefined.
highlightedEventId: React.PropTypes.string, highlightedEventId: React.PropTypes.string,
// is the RightPanel collapsed?
rightPanelCollapsed: React.PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null; var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
return { return {
room: room, room: room,
roomLoading: !room, roomLoading: !room,
@ -123,7 +130,6 @@ module.exports = React.createClass({
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room", this.onRoom); MatrixClientPeg.get().on("Room", this.onRoom);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
// xchat-style tab complete, add a colon if tab // xchat-style tab complete, add a colon if tab
@ -146,9 +152,9 @@ module.exports = React.createClass({
// We can /peek though. If it fails then we present the join UI. If it // We can /peek though. If it fails then we present the join UI. If it
// succeeds then great, show the preview (but we still may be able to /join!). // succeeds then great, show the preview (but we still may be able to /join!).
if (!this.state.room) { if (!this.state.room) {
console.log("Attempting to peek into room %s", this.props.roomId); console.log("Attempting to peek into room %s", this.props.roomAddress);
MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => { MatrixClientPeg.get().peekInRoom(this.props.roomAddress).then((room) => {
this.setState({ this.setState({
room: room, room: room,
roomLoading: false, roomLoading: false,
@ -200,14 +206,15 @@ module.exports = React.createClass({
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
} }
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
Tinter.tint(); // reset colourscheme // no need to do this as Dir & Settings are now overlays. It just burnt CPU.
// console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme
}, },
onAction: function(payload) { onAction: function(payload) {
@ -233,7 +240,7 @@ module.exports = React.createClass({
return; return;
} }
var call = CallHandler.getCallForRoom(payload.room_id); var call = this._getCallForRoom();
var callState; var callState;
if (call) { if (call) {
@ -256,7 +263,7 @@ module.exports = React.createClass({
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
if (newProps.roomId != this.props.roomId) { if (newProps.roomAddress != this.props.roomAddress) {
throw new Error("changing room on a RoomView is not supported"); throw new Error("changing room on a RoomView is not supported");
} }
@ -270,7 +277,7 @@ module.exports = React.createClass({
if (this.unmounted) return; if (this.unmounted) return;
// ignore events for other rooms // ignore events for other rooms
if (room.roomId != this.props.roomId) return; if (!this.state.room || room.roomId != this.state.room.roomId) return;
// ignore anything but real-time updates at the end of the room: // ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes. // updates from pagination will happen when the paginate completes.
@ -286,7 +293,7 @@ module.exports = React.createClass({
// no change // no change
} }
else { else {
this.setState((state, props) => { this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1}; return {numUnreadMessages: state.numUnreadMessages + 1};
}); });
} }
@ -321,30 +328,18 @@ module.exports = React.createClass({
// set it in our state and start using it (ie. init the timeline) // set it in our state and start using it (ie. init the timeline)
// This will happen if we start off viewing a room we're not joined, // This will happen if we start off viewing a room we're not joined,
// then join it whilst RoomView is looking at that room. // then join it whilst RoomView is looking at that room.
if (room.roomId == this.props.roomId && !this.state.room) { if (!this.state.room && room.roomId == this._joiningRoomId) {
this._joiningRoomId = undefined;
this.setState({ this.setState({
room: room room: room,
joining: false,
}); });
this._onRoomLoaded(room); this._onRoomLoaded(room);
} }
}, },
onRoomName: function(room) {
// NB don't set state.room here.
//
// When peeking, this event lands *before* the timeline is correctly
// synced; if we set state.room here, the TimelinePanel will be
// instantiated, and it will initialise its scroll state, with *no
// events*. In short, the scroll state will be all messed up.
//
// There's no need to set state.room here anyway.
if (room.roomId == this.props.roomId) {
this.forceUpdate();
}
},
updateTint: function() { updateTint: function() {
var room = MatrixClientPeg.get().getRoom(this.props.roomId); var room = this.state.room;
if (!room) return; if (!room) return;
var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
@ -352,7 +347,8 @@ module.exports = React.createClass({
if (color_scheme_event) { if (color_scheme_event) {
color_scheme = color_scheme_event.getContent(); color_scheme = color_scheme_event.getContent();
// XXX: we should validate the event // XXX: we should validate the event
} }
console.log("Tinter.tint from updateTint");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
}, },
@ -361,34 +357,40 @@ module.exports = React.createClass({
if (event.getType === "org.matrix.room.color_scheme") { if (event.getType === "org.matrix.room.color_scheme") {
var color_scheme = event.getContent(); var color_scheme = event.getContent();
// XXX: we should validate the event // XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
} }
} }
}, },
onRoomStateMember: function(ev, state, member) { onRoomStateMember: function(ev, state, member) {
if (member.roomId === this.props.roomId) { // ignore if we don't have a room yet
// a member state changed in this room, refresh the tab complete list if (!this.state.room) {
this._updateTabCompleteList();
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) return;
var me = MatrixClientPeg.get().credentials.userId;
if (this.state.joining && room.hasMembershipState(me, "join")) {
this.setState({
joining: false
});
}
}
if (!this.props.ConferenceHandler) {
return; return;
} }
if (member.roomId !== this.props.roomId ||
member.userId !== this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) { // ignore members in other rooms
if (member.roomId !== this.state.room.roomId) {
return; return;
} }
this._updateConfCallNotification();
// a member state changed in this room, refresh the tab complete list
this._updateTabCompleteList();
// 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
// into.
var me = MatrixClientPeg.get().credentials.userId;
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
this.setState({
joining: false
});
}
if (this.props.ConferenceHandler &&
member.userId === this.props.ConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
this._updateConfCallNotification();
}
}, },
_hasUnsentMessages: function(room) { _hasUnsentMessages: function(room) {
@ -403,12 +405,12 @@ module.exports = React.createClass({
}, },
_updateConfCallNotification: function() { _updateConfCallNotification: function() {
var room = MatrixClientPeg.get().getRoom(this.props.roomId); var room = this.state.room;
if (!room || !this.props.ConferenceHandler) { if (!room || !this.props.ConferenceHandler) {
return; return;
} }
var confMember = room.getMember( var confMember = room.getMember(
this.props.ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId) this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId)
); );
if (!confMember) { if (!confMember) {
@ -427,7 +429,7 @@ module.exports = React.createClass({
}, },
componentDidMount: function() { componentDidMount: function() {
var call = CallHandler.getCallForRoom(this.props.roomId); var call = this._getCallForRoom();
var callState = call ? call.call_state : "ended"; var callState = call ? call.call_state : "ended";
this.setState({ this.setState({
callState: callState callState: callState
@ -559,25 +561,35 @@ module.exports = React.createClass({
display_name_promise.then(() => { display_name_promise.then(() => {
var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined; var sign_url = this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : undefined;
return MatrixClientPeg.get().joinRoom(this.props.roomAlias || this.props.roomId, return MatrixClientPeg.get().joinRoom(this.props.roomAddress,
{ inviteSignUrl: sign_url } ) { inviteSignUrl: sign_url } )
}).done(function() { }).then(function(resp) {
var roomId = resp.roomId;
// It is possible that there is no Room yet if state hasn't come down // It is possible that there is no Room yet if state hasn't come down
// from /sync - joinRoom will resolve when the HTTP request to join succeeds, // from /sync - joinRoom will resolve when the HTTP request to join succeeds,
// NOT when it comes down /sync. If there is no room, we'll keep the // NOT when it comes down /sync. If there is no room, we'll keep the
// joining flag set until we see it. Likewise, if our state is not // joining flag set until we see it.
// "join" we'll keep this flag set until it comes down /sync.
// We'll need to initialise the timeline when joining, but due to // We'll need to initialise the timeline when joining, but due to
// the above, we can't do it here: we do it in onRoom instead, // the above, we can't do it here: we do it in onRoom instead,
// once we have a useable room object. // once we have a useable room object.
var room = MatrixClientPeg.get().getRoom(self.props.roomId); var room = MatrixClientPeg.get().getRoom(roomId);
var me = MatrixClientPeg.get().credentials.userId; if (!room) {
self.setState({ // wait for the room to turn up in onRoom.
joining: room ? !room.hasMembershipState(me, "join") : true, self._joiningRoomId = roomId;
room: room } else {
}); // we've got a valid room, but that might also just mean that
}, function(error) { // it was peekable (so we had one before anyway). If we are
// not yet a member of the room, we will need to wait for that
// to happen, in onRoomStateMember.
var me = MatrixClientPeg.get().credentials.userId;
self.setState({
joining: !room.hasMembershipState(me, "join"),
room: room
});
}
}).catch(function(error) {
self.setState({ self.setState({
joining: false, joining: false,
joinError: error joinError: error
@ -612,7 +624,8 @@ module.exports = React.createClass({
description: msg description: msg
}); });
} }
}); }).done();
this.setState({ this.setState({
joining: true joining: true
}); });
@ -667,7 +680,7 @@ module.exports = React.createClass({
uploadFile: function(file) { uploadFile: function(file) {
var self = this; var self = this;
ContentMessages.sendContentToRoom( ContentMessages.sendContentToRoom(
file, this.props.roomId, MatrixClientPeg.get() file, this.state.room.roomId, MatrixClientPeg.get()
).done(undefined, function(error) { ).done(undefined, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
@ -702,7 +715,7 @@ module.exports = React.createClass({
filter = { filter = {
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
rooms: [ rooms: [
this.props.roomId this.state.room.roomId
] ]
}; };
} }
@ -860,6 +873,8 @@ module.exports = React.createClass({
}, },
onSettingsSaveClick: function() { onSettingsSaveClick: function() {
if (!this.refs.room_settings) return;
this.setState({ this.setState({
uploadingRoomSettings: true, uploadingRoomSettings: true,
}); });
@ -901,6 +916,7 @@ module.exports = React.createClass({
}, },
onCancelClick: function() { onCancelClick: function() {
console.log("updateTint from onCancelClick");
this.updateTint(); this.updateTint();
this.setState({editingRoomSettings: false}); this.setState({editingRoomSettings: false});
}, },
@ -908,12 +924,12 @@ module.exports = React.createClass({
onLeaveClick: function() { onLeaveClick: function() {
dis.dispatch({ dis.dispatch({
action: 'leave_room', action: 'leave_room',
room_id: this.props.roomId, room_id: this.state.room.roomId,
}); });
}, },
onForgetClick: function() { onForgetClick: function() {
MatrixClientPeg.get().forget(this.props.roomId).done(function() { MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
}, function(err) { }, function(err) {
var errCode = err.errcode || "unknown error code"; var errCode = err.errcode || "unknown error code";
@ -930,7 +946,7 @@ module.exports = React.createClass({
this.setState({ this.setState({
rejecting: true rejecting: true
}); });
MatrixClientPeg.get().leave(this.props.roomId).done(function() { MatrixClientPeg.get().leave(this.props.roomAddress).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
self.setState({ self.setState({
rejecting: false rejecting: false
@ -984,7 +1000,8 @@ module.exports = React.createClass({
}, },
// update the read marker to match the read-receipt // update the read marker to match the read-receipt
forgetReadMarker: function() { forgetReadMarker: function(ev) {
ev.stopPropagation();
this.refs.messagePanel.forgetReadMarker(); this.refs.messagePanel.forgetReadMarker();
}, },
@ -1087,7 +1104,7 @@ module.exports = React.createClass({
}, },
onMuteAudioClick: function() { onMuteAudioClick: function() {
var call = CallHandler.getCallForRoom(this.props.roomId); var call = this._getCallForRoom();
if (!call) { if (!call) {
return; return;
} }
@ -1099,7 +1116,7 @@ module.exports = React.createClass({
}, },
onMuteVideoClick: function() { onMuteVideoClick: function() {
var call = CallHandler.getCallForRoom(this.props.roomId); var call = this._getCallForRoom();
if (!call) { if (!call) {
return; return;
} }
@ -1139,11 +1156,35 @@ module.exports = React.createClass({
} }
}, },
/**
* Get the ID of the displayed room
*
* Returns null if the RoomView was instantiated on a room alias and
* we haven't yet joined the room.
*/
getRoomId: function() {
if (!this.state.room) {
return null;
}
return this.state.room.roomId;
},
/**
* get any current call for this room
*/
_getCallForRoom: function() {
if (!this.state.room) {
return null;
}
return CallHandler.getCallForRoom(this.state.room.roomId);
},
// this has to be a proper method rather than an unnamed function, // this has to be a proper method rather than an unnamed function,
// otherwise react calls it with null on each update. // otherwise react calls it with null on each update.
_gatherTimelinePanelRef: function(r) { _gatherTimelinePanelRef: function(r) {
this.refs.messagePanel = r; this.refs.messagePanel = r;
if(r) { if(r) {
console.log("updateTint from RoomView._gatherTimelinePanelRef");
this.updateTint(); this.updateTint();
} }
}, },
@ -1161,13 +1202,12 @@ module.exports = React.createClass({
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
if (!this.state.room) { if (!this.state.room) {
if (this.props.roomId) {
if (this.state.roomLoading) { if (this.state.roomLoading) {
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<Loader /> <Loader />
</div> </div>
); );
} }
else { else {
var inviterName = undefined; var inviterName = undefined;
@ -1183,7 +1223,11 @@ module.exports = React.createClass({
// We've got to this room by following a link, possibly a third party invite. // We've got to this room by following a link, possibly a third party invite.
return ( return (
<div className="mx_RoomView"> <div className="mx_RoomView">
<RoomHeader ref="header" room={this.state.room} oobData={this.props.oobData} /> <RoomHeader ref="header"
room={this.state.room}
oobData={this.props.oobData}
rightPanelCollapsed={ this.props.rightPanelCollapsed }
/>
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
onRejectClick={ this.onRejectThreepidInviteButtonClicked } onRejectClick={ this.onRejectThreepidInviteButtonClicked }
@ -1196,14 +1240,8 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_RoomView_messagePanel"></div> <div className="mx_RoomView_messagePanel"></div>
</div> </div>
); );
} }
}
else {
return (
<div />
);
}
} }
var myUserId = MatrixClientPeg.get().credentials.userId; var myUserId = MatrixClientPeg.get().credentials.userId;
@ -1233,7 +1271,7 @@ module.exports = React.createClass({
inviterName={ inviterName } inviterName={ inviterName }
canJoin={ true } canPreview={ false } canJoin={ true } canPreview={ false }
spinner={this.state.joining} spinner={this.state.joining}
room={this.state.room} room={this.state.room}
/> />
</div> </div>
<div className="mx_RoomView_messagePanel"></div> <div className="mx_RoomView_messagePanel"></div>
@ -1245,7 +1283,7 @@ module.exports = React.createClass({
// We have successfully loaded this room, and are not previewing. // We have successfully loaded this room, and are not previewing.
// Display the "normal" room view. // Display the "normal" room view.
var call = CallHandler.getCallForRoom(this.props.roomId); var call = this._getCallForRoom();
var inCall = false; var inCall = false;
if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) { if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
inCall = true; inCall = true;
@ -1257,13 +1295,6 @@ module.exports = React.createClass({
var statusBar; var statusBar;
// for testing UI...
// this.state.upload = {
// uploadedBytes: 123493,
// totalBytes: 347534,
// fileName: "testing_fooble.jpg",
// }
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar'); var UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} /> statusBar = <UploadBar room={this.state.room} />
@ -1314,7 +1345,7 @@ module.exports = React.createClass({
inviterName={inviterName} inviterName={inviterName}
invitedEmail={invitedEmail} invitedEmail={invitedEmail}
canPreview={this.state.canPeek} canPreview={this.state.canPeek}
room={this.state.room} room={this.state.room}
/> />
); );
} }
@ -1339,7 +1370,7 @@ module.exports = React.createClass({
messageComposer = messageComposer =
<MessageComposer <MessageComposer
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile} room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
callState={this.state.callState} tabComplete={this.tabComplete} /> callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>
} }
// TODO: Why aren't we storing the term/scope/count in this format // TODO: Why aren't we storing the term/scope/count in this format
@ -1394,8 +1425,12 @@ module.exports = React.createClass({
if (this.state.searchResults) { if (this.state.searchResults) {
searchResultsPanel = ( searchResultsPanel = (
<ScrollPanel ref="searchResultsPanel" className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel" <ScrollPanel ref="searchResultsPanel"
onFillRequest={ this.onSearchResultsFillRequest } onResize={ this.onSearchResultsResize }> className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
onFillRequest={ this.onSearchResultsFillRequest }
onResize={ this.onSearchResultsResize }
style={{ opacity: this.props.opacity }}
>
<li className={scrollheader_classes}></li> <li className={scrollheader_classes}></li>
{this.getSearchResultTiles()} {this.getSearchResultTiles()}
</ScrollPanel> </ScrollPanel>
@ -1412,13 +1447,14 @@ module.exports = React.createClass({
eventPixelOffset={this.props.eventPixelOffset} eventPixelOffset={this.props.eventPixelOffset}
onScroll={ this.onMessageListScroll } onScroll={ this.onMessageListScroll }
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
opacity={ this.props.opacity }
/>); />);
var topUnreadMessagesBar = null; var topUnreadMessagesBar = null;
if (this.state.showTopUnreadMessagesBar) { if (this.state.showTopUnreadMessagesBar) {
var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = ( topUnreadMessagesBar = (
<div className="mx_RoomView_topUnreadMessagesBar"> <div className="mx_RoomView_topUnreadMessagesBar mx_fadable" style={{ opacity: this.props.opacity }}>
<TopUnreadMessagesBar <TopUnreadMessagesBar
onScrollUpClick={this.jumpToReadMarker} onScrollUpClick={this.jumpToReadMarker}
onCloseClick={this.forgetReadMarker} onCloseClick={this.forgetReadMarker}
@ -1432,6 +1468,7 @@ module.exports = React.createClass({
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData} oobData={this.props.oobData}
editing={this.state.editingRoomSettings} editing={this.state.editingRoomSettings}
saving={this.state.uploadingRoomSettings}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onSaveClick={this.onSettingsSaveClick} onSaveClick={this.onSettingsSaveClick}
@ -1446,7 +1483,7 @@ module.exports = React.createClass({
{ topUnreadMessagesBar } { topUnreadMessagesBar }
{ messagePanel } { messagePanel }
{ searchResultsPanel } { searchResultsPanel }
<div className="mx_RoomView_statusArea"> <div className="mx_RoomView_statusArea mx_fadable" style={{ opacity: this.props.opacity }}>
<div className="mx_RoomView_statusAreaBox"> <div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div> <div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar } { statusBar }

View file

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

View file

@ -76,6 +76,9 @@ var TimelinePanel = React.createClass({
// callback which is called when the read-up-to mark is updated. // callback which is called when the read-up-to mark is updated.
onReadMarkerUpdated: React.PropTypes.func, onReadMarkerUpdated: React.PropTypes.func,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
}, },
statics: { statics: {
@ -172,8 +175,27 @@ var TimelinePanel = React.createClass({
}, },
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate: function(nextProps, nextState) {
return (!ObjectUtils.shallowEqual(this.props, nextProps) || if (!ObjectUtils.shallowEqual(this.props, nextProps)) {
!ObjectUtils.shallowEqual(this.state, nextState)); if (DEBUG) {
console.group("Timeline.shouldComponentUpdate: props change");
console.log("props before:", this.props);
console.log("props after:", nextProps);
console.groupEnd();
}
return true;
}
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
if (DEBUG) {
console.group("Timeline.shouldComponentUpdate: state change");
console.log("state before:", this.state);
console.log("state after:", nextState);
console.groupEnd();
}
return true;
}
return false;
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@ -222,8 +244,8 @@ var TimelinePanel = React.createClass({
this.setState({ this.setState({
[paginatingKey]: false, [paginatingKey]: false,
[canPaginateKey]: r, [canPaginateKey]: r,
events: this._getEvents(),
}); });
this._reloadEvents();
return r; return r;
}); });
}, },
@ -264,25 +286,14 @@ var TimelinePanel = React.createClass({
// updates from pagination will happen when the paginate completes. // updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return; if (toStartOfTimeline || !data || !data.liveEvent) return;
// even if we previously gave up forward-paginating, it's worth
// having another go now.
this.setState({canForwardPaginate: true});
if (!this.refs.messagePanel) return; if (!this.refs.messagePanel) return;
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) return; if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
// we won't load this event now, because we don't want to push any
// when a new event arrives when the user is not watching the window, but the // events off the other end of the timeline. But we need to note
// window is in its auto-scroll mode, make sure the read marker is visible. // that we can now paginate.
// this.setState({canForwardPaginate: true});
// We ignore events we have sent ourselves; we don't want to see the return;
// read-marker when a remote echo of an event we have just sent takes
// more than the timeout on userCurrentlyActive.
//
var myUserId = MatrixClientPeg.get().credentials.userId;
var sender = ev.sender ? ev.sender.userId : null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
this.setState({readMarkerVisible: true});
} }
// tell the timeline window to try to advance itself, but not to make // tell the timeline window to try to advance itself, but not to make
@ -291,11 +302,46 @@ var TimelinePanel = React.createClass({
// we deliberately avoid going via the ScrollPanel for this call - the // we deliberately avoid going via the ScrollPanel for this call - the
// ScrollPanel might already have an active pagination promise, which // ScrollPanel might already have an active pagination promise, which
// will fail, but would stop us passing the pagination request to the // will fail, but would stop us passing the pagination request to the
// timeline window. // timeline window.
// //
// see https://github.com/vector-im/vector-web/issues/1035 // see https://github.com/vector-im/vector-web/issues/1035
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false) this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
.done(this._reloadEvents); if (this.unmounted) { return; }
var events = this._timelineWindow.getEvents();
var lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(... this.props.room.getPendingEvents());
}
var updatedState = {events: events};
// when a new event arrives when the user is not watching the
// window, but the window is in its auto-scroll mode, make sure the
// read marker is visible.
//
// We ignore events we have sent ourselves; we don't want to see the
// read-marker when a remote echo of an event we have just sent takes
// more than the timeout on userCurrentlyActive.
//
var myUserId = MatrixClientPeg.get().credentials.userId;
var sender = ev.sender ? ev.sender.userId : null;
var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
updatedState.readMarkerVisible = true;
} else if(lastEv && this.getReadMarkerPosition() === 0) {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
this._setReadMarker(lastEv.getId(), lastEv.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastEv.getId();
callback = this.props.onReadMarkerUpdated;
}
this.setState(updatedState, callback);
});
}, },
onRoomTimelineReset: function(room) { onRoomTimelineReset: function(room) {
@ -717,6 +763,13 @@ var TimelinePanel = React.createClass({
// the results if so. // the results if so.
if (this.unmounted) return; if (this.unmounted) return;
this.setState({
events: this._getEvents(),
});
},
// get the list of events from the timeline window and the pending event list
_getEvents: function() {
var events = this._timelineWindow.getEvents(); var events = this._timelineWindow.getEvents();
// if we're at the end of the live timeline, append the pending events // if we're at the end of the live timeline, append the pending events
@ -724,9 +777,7 @@ var TimelinePanel = React.createClass({
events.push(... this.props.room.getPendingEvents()); events.push(... this.props.room.getPendingEvents());
} }
this.setState({ return events;
events: events,
});
}, },
_indexForEventId: function(evId) { _indexForEventId: function(evId) {
@ -792,7 +843,7 @@ var TimelinePanel = React.createClass({
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized); return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
}, },
_setReadMarker: function(eventId, eventTs) { _setReadMarker: function(eventId, eventTs, inhibitSetState) {
if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) { if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) {
// don't update the state (and cause a re-render) if there is // don't update the state (and cause a re-render) if there is
// no change to the RM. // no change to the RM.
@ -807,6 +858,10 @@ var TimelinePanel = React.createClass({
// above or below the visible timeline, we stash the timestamp. // above or below the visible timeline, we stash the timestamp.
TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs; TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs;
if (inhibitSetState) {
return;
}
// run the render cycle before calling the callback, so that // run the render cycle before calling the callback, so that
// getReadMarkerPosition() returns the right thing. // getReadMarkerPosition() returns the right thing.
this.setState({ this.setState({
@ -861,6 +916,7 @@ var TimelinePanel = React.createClass({
stickyBottom={ stickyBottom } stickyBottom={ stickyBottom }
onScroll={ this.onMessageListScroll } onScroll={ this.onMessageListScroll }
onFillRequest={ this.onMessageListFillRequest } onFillRequest={ this.onMessageListFillRequest }
opacity={ this.props.opacity }
/> />
); );
}, },

View file

@ -45,6 +45,17 @@ module.exports = React.createClass({displayName: 'UploadBar',
render: function() { render: function() {
var uploads = ContentMessages.getCurrentUploads(); var uploads = ContentMessages.getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView
//
// uploads = [{
// roomId: this.props.room.roomId,
// loaded: 123493,
// total: 347534,
// fileName: "testing_fooble.jpg",
// }];
if (uploads.length == 0) { if (uploads.length == 0) {
return <div /> return <div />
} }

View file

@ -51,7 +51,11 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
var self = this; dis.dispatch({
action: 'ui_opacity',
sideOpacity: 0.3,
middleOpacity: 0.3,
});
this._refreshFromServer(); this._refreshFromServer();
}, },
@ -61,6 +65,11 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
});
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
}, },
@ -321,7 +330,7 @@ module.exports = React.createClass({
var notification_area; var notification_area;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) { if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
notification_area = (<div> notification_area = (<div>
<h2>Notifications</h2> <h3>Notifications</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<Notifications threepids={this.state.threepids} /> <Notifications threepids={this.state.threepids} />
@ -331,11 +340,13 @@ module.exports = React.createClass({
return ( return (
<div className="mx_UserSettings"> <div className="mx_UserSettings">
<SimpleRoomHeader title="Settings"/> <SimpleRoomHeader title="Settings" onCancelClick={ this.props.onClose }/>
<GeminiScrollbar className="mx_UserSettings_body" autoshow={true}> <GeminiScrollbar className="mx_UserSettings_body"
relayoutOnUpdate={false}
autoshow={true}>
<h2>Profile</h2> <h3>Profile</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_profileTable"> <div className="mx_UserSettings_profileTable">
@ -366,10 +377,10 @@ module.exports = React.createClass({
</div> </div>
</div> </div>
<h2>Account</h2> <h3>Account</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}> <div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
Log out Log out
</div> </div>
@ -379,7 +390,7 @@ module.exports = React.createClass({
{notification_area} {notification_area}
<h2>Advanced</h2> <h3>Advanced</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced"> <div className="mx_UserSettings_advanced">

View file

@ -99,15 +99,36 @@ module.exports = React.createClass({
} }
}, },
_getInitialLetter: function() { /**
var name = this.props.name; * returns the first (non-sigil) character of 'name',
//For large characters (exceeding 2 bytes), this function will get the correct character. * converted to uppercase
//However, this does NOT get the second character correctly if a large character is before it. */
var initial = String.fromCodePoint(name.codePointAt(0)); _getInitialLetter: function(name) {
if ((initial === '@' || initial === '#') && name[1]) { if (name.length < 1) {
initial = String.fromCodePoint(name.codePointAt(1)); return undefined;
} }
return initial.toUpperCase();
var idx = 0;
var initial = name[0];
if ((initial === '@' || initial === '#') && name[1]) {
idx++;
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
var chars = 1;
var first = name.charCodeAt(idx);
// check if its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
var second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
var firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
}, },
render: function() { render: function() {
@ -116,7 +137,7 @@ module.exports = React.createClass({
var imageUrl = this.state.imageUrls[this.state.urlsIndex]; var imageUrl = this.state.imageUrls[this.state.urlsIndex];
if (imageUrl === this.state.defaultImageUrl) { if (imageUrl === this.state.defaultImageUrl) {
var initialLetter = this._getInitialLetter(); var initialLetter = this._getInitialLetter(this.props.name);
return ( return (
<span className="mx_BaseAvatar" {...this.props}> <span className="mx_BaseAvatar" {...this.props}>
<span className="mx_BaseAvatar_initial" aria-hidden="true" <span className="mx_BaseAvatar_initial" aria-hidden="true"

View file

@ -24,6 +24,7 @@ const KEY_WINDOWS = 91;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'EditableText', displayName: 'EditableText',
propTypes: { propTypes: {
onValueChanged: React.PropTypes.func, onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string, initialValue: React.PropTypes.string,

View file

@ -18,10 +18,9 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require("react-dom"); var ReactDOM = require("react-dom");
var dis = require("../../../dispatcher");
var Tinter = require("../../../Tinter"); var Tinter = require("../../../Tinter");
module.exports = React.createClass({ var TintableSvg = React.createClass({
displayName: 'TintableSvg', displayName: 'TintableSvg',
propTypes: { propTypes: {
@ -31,39 +30,48 @@ module.exports = React.createClass({
className: React.PropTypes.string, className: React.PropTypes.string,
}, },
statics: {
// list of currently mounted TintableSvgs
mounts: {},
idSequence: 0,
},
componentWillMount: function() { componentWillMount: function() {
this.fixups = []; this.fixups = [];
this.dispatcherRef = dis.register(this.onAction);
}, },
componentDidMount: function() { componentDidMount: function() {
// we can't use onLoad on object due to https://github.com/facebook/react/pull/5781 this.id = TintableSvg.idSequence++;
// so handle it with pure DOM instead TintableSvg.mounts[this.id] = this;
ReactDOM.findDOMNode(this).addEventListener('load', this.onLoad);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
ReactDOM.findDOMNode(this).removeEventListener('load', this.onLoad); delete TintableSvg.mounts[this.id];
dis.unregister(this.dispatcherRef);
}, },
onAction: function(payload) { tint: function() {
if (payload.action !== 'tint_update') return; // TODO: only bother running this if the global tint settings have changed
// since we loaded!
Tinter.applySvgFixups(this.fixups); Tinter.applySvgFixups(this.fixups);
}, },
onLoad: function(event) { onLoad: function(event) {
// console.log("TintableSvg.onLoad for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([event.target]); this.fixups = Tinter.calcSvgFixups([event.target]);
Tinter.applySvgFixups(this.fixups); Tinter.applySvgFixups(this.fixups);
}, },
render: function() { render: function() {
return ( return (
<object className={ "mx_TintableSvg " + this.props.className } <object className={ "mx_TintableSvg " + (this.props.className ? this.props.className : "") }
type="image/svg+xml" type="image/svg+xml"
data={ this.props.src } data={ this.props.src }
width={ this.props.width } width={ this.props.width }
height={ this.props.height }/> height={ this.props.height }
onLoad={ this.onLoad }
/>
); );
} }
}); });
module.exports = TintableSvg;

View file

@ -0,0 +1,50 @@
/*
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.
*/
'use strict';
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
export default class MAudioBody extends React.Component {
constructor(props) {
super(props);
this.state = {
playing: false
}
}
onPlayToggle() {
this.setState({
playing: !this.state.playing
});
}
render() {
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
return (
<span className="mx_MAudioBody">
<audio src={cli.mxcUrlToHttp(content.url)} controls />
<MFileBody {...this.props} />
</span>
);
}
}

View file

@ -43,7 +43,7 @@ module.exports = React.createClass({
}, },
getEventTileOps: function() { getEventTileOps: function() {
return this.refs.body ? this.refs.body.getEventTileOps() : null; return this.refs.body && this.refs.body.getEventTileOps ? this.refs.body.getEventTileOps() : null;
}, },
render: function() { render: function() {
@ -55,6 +55,7 @@ module.exports = React.createClass({
'm.emote': sdk.getComponent('messages.TextualBody'), 'm.emote': sdk.getComponent('messages.TextualBody'),
'm.image': sdk.getComponent('messages.MImageBody'), 'm.image': sdk.getComponent('messages.MImageBody'),
'm.file': sdk.getComponent('messages.MFileBody'), 'm.file': sdk.getComponent('messages.MFileBody'),
'm.audio': sdk.getComponent('messages.MAudioBody'),
'm.video': sdk.getComponent('messages.MVideoBody') 'm.video': sdk.getComponent('messages.MVideoBody')
}; };
@ -63,6 +64,9 @@ module.exports = React.createClass({
var BodyType = UnknownBody; var BodyType = UnknownBody;
if (msgtype && bodyTypes[msgtype]) { if (msgtype && bodyTypes[msgtype]) {
BodyType = bodyTypes[msgtype]; BodyType = bodyTypes[msgtype];
} else if (content.url) {
// Fallback to MFileBody if there's a content URL
BodyType = bodyTypes['m.file'];
} }
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights} return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}

View file

@ -84,7 +84,10 @@ module.exports = React.createClass({
findLink: function(nodes) { findLink: function(nodes) {
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]; var node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) { if (node.tagName === "A" && node.getAttribute("href") &&
(node.getAttribute("href").startsWith("http://") ||
node.getAttribute("href").startsWith("https://")))
{
return node; return node;
} }
else if (node.children && node.children.length) { else if (node.children && node.children.length) {

View file

@ -37,7 +37,8 @@ module.exports = React.createClass({
avatarJsx: React.PropTypes.any, // <BaseAvatar /> avatarJsx: React.PropTypes.any, // <BaseAvatar />
className: React.PropTypes.string, className: React.PropTypes.string,
presenceState: React.PropTypes.string, presenceState: React.PropTypes.string,
presenceActiveAgo: React.PropTypes.number, presenceLastActiveAgo: React.PropTypes.number,
presenceLastTs: React.PropTypes.number,
presenceCurrentlyActive: React.PropTypes.bool, presenceCurrentlyActive: React.PropTypes.bool,
showInviteButton: React.PropTypes.bool, showInviteButton: React.PropTypes.bool,
shouldComponentUpdate: React.PropTypes.func, shouldComponentUpdate: React.PropTypes.func,
@ -50,7 +51,8 @@ module.exports = React.createClass({
shouldComponentUpdate: function(nextProps, nextState) { return true; }, shouldComponentUpdate: function(nextProps, nextState) { return true; },
onClick: function() {}, onClick: function() {},
presenceState: "offline", presenceState: "offline",
presenceActiveAgo: -1, presenceLastActiveAgo: 0,
presenceLastTs: 0,
showInviteButton: false, showInviteButton: false,
suppressOnHover: false suppressOnHover: false
}; };
@ -82,13 +84,16 @@ module.exports = React.createClass({
var nameEl; var nameEl;
if (this.state.hover && !this.props.suppressOnHover) { if (this.state.hover && !this.props.suppressOnHover) {
var activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
mainClassName += " mx_EntityTile_hover"; mainClassName += " mx_EntityTile_hover";
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
nameEl = ( nameEl = (
<div className="mx_EntityTile_details"> <div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/> <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">{ this.props.name }</div>
<PresenceLabel activeAgo={this.props.presenceActiveAgo} <PresenceLabel activeAgo={ activeAgo }
currentlyActive={this.props.presenceCurrentlyActive} currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} /> presenceState={this.props.presenceState} />
</div> </div>

View file

@ -29,6 +29,8 @@ var Velociraptor = require('../../../Velociraptor');
require('../../../VelocityBounce'); require('../../../VelocityBounce');
var dispatcher = require("../../../dispatcher"); var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils');
var bounce = false; var bounce = false;
try { try {
if (global.localStorage) { if (global.localStorage) {
@ -107,12 +109,75 @@ module.exports = React.createClass({
/* callback called when dynamic content in events are loaded */ /* callback called when dynamic content in events are loaded */
onWidgetLoad: React.PropTypes.func, onWidgetLoad: React.PropTypes.func,
/* a list of Room Members whose read-receipts we should show */
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
/* the status of this event - ie, mxEvent.status. Denormalised to here so
* that we can tell when it changes. */
eventSendStatus: React.PropTypes.string,
}, },
getInitialState: function() { getInitialState: function() {
return {menu: false, allReadAvatars: false}; return {menu: false, allReadAvatars: false};
}, },
shouldComponentUpdate: function (nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
return true;
}
if (!this._propsEqual(this.props, nextProps)) {
return true;
}
return false;
},
_propsEqual: function(objA, objB) {
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (var i = 0; i < keysA.length; i++) {
var key = keysA[i];
if (!objB.hasOwnProperty(key)) {
return false;
}
// need to deep-compare readReceipts
if (key == 'readReceipts') {
var rA = objA[key];
var rB = objB[key];
if (rA === rB) {
continue;
}
if (!rA || !rB) {
return false;
}
if (rA.length !== rB.length) {
return false;
}
for (var j = 0; j < rA.length; j++) {
if (rA[j].userId !== rB[j].userId) {
return false;
}
}
} else {
if (objA[key] !== objB[key]) {
return false;
}
}
}
return true;
},
shouldHighlight: function() { shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent); var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; } if (!actions || !actions.tweaks) { return false; }
@ -137,7 +202,7 @@ module.exports = React.createClass({
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
left: x, left: x,
top: y, top: y,
eventTileOps: this.refs.tile ? this.refs.tile.getEventTileOps() : undefined, eventTileOps: this.refs.tile && this.refs.tile.getEventTileOps ? this.refs.tile.getEventTileOps() : undefined,
onFinished: function() { onFinished: function() {
self.setState({menu: false}); self.setState({menu: false});
} }
@ -153,20 +218,6 @@ module.exports = React.createClass({
getReadAvatars: function() { getReadAvatars: function() {
var avatars = []; var avatars = [];
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (!room) return [];
var myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) {
return r.type === "m.read" && r.userId != myUserId;
}).sort(function(r1, r2) {
return r2.data.ts - r1.data.ts;
});
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var left = 0; var left = 0;
@ -176,11 +227,9 @@ module.exports = React.createClass({
easing: 'easeOut' easing: 'easeOut'
}; };
var receipts = this.props.readReceipts || [];
for (var i = 0; i < receipts.length; ++i) { for (var i = 0; i < receipts.length; ++i) {
var member = room.getMember(receipts[i].userId); var member = receipts[i];
if (!member) {
continue;
}
// Using react refs here would mean both getting Velociraptor to expose // Using react refs here would mean both getting Velociraptor to expose
// them and making them scoped to the whole RoomView. Not impossible, but // them and making them scoped to the whole RoomView. Not impossible, but
@ -302,9 +351,9 @@ module.exports = React.createClass({
var classes = classNames({ var classes = classNames({
mx_EventTile: true, mx_EventTile: true,
mx_EventTile_sending: ['sending', 'queued'].indexOf( mx_EventTile_sending: ['sending', 'queued'].indexOf(
this.props.mxEvent.status this.props.eventSendStatus
) !== -1, ) !== -1,
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent', mx_EventTile_notSent: this.props.eventSendStatus == 'not_sent',
mx_EventTile_highlight: this.shouldHighlight(), mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.continuation, mx_EventTile_continuation: this.props.continuation,

View file

@ -45,14 +45,18 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
this.unmounted = false;
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{ MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
if (this.unmounted) {
return;
}
this.setState( this.setState(
{ preview: res }, { preview: res },
this.props.onWidgetLoad this.props.onWidgetLoad
); );
}, (error)=>{ }, (error)=>{
console.error("Failed to get preview for " + this.props.link + " " + error); console.error("Failed to get preview for " + this.props.link + " " + error);
}); }).done();
}, },
componentDidMount: function() { componentDidMount: function() {
@ -65,6 +69,10 @@ module.exports = React.createClass({
linkifyElement(this.refs.description, linkifyMatrix.options); linkifyElement(this.refs.description, linkifyMatrix.options);
}, },
componentWillUnmount: function() {
this.unmounted = true;
},
onImageClick: function(ev) { onImageClick: function(ev) {
var p = this.state.preview; var p = this.state.preview;
if (ev.button != 0 || ev.metaKey) return; if (ev.button != 0 || ev.metaKey) return;

View file

@ -57,7 +57,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId; var roomId = this.props.member.roomId;
var target = this.props.member.userId; var target = this.props.member.userId;
MatrixClientPeg.get().kick(roomId, target).done(function() { this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().kick(roomId, target).then(function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
console.log("Kick success"); console.log("Kick success");
@ -67,7 +68,9 @@ module.exports = React.createClass({
description: err.message description: err.message
}); });
} }
); ).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
this.props.onFinished(); this.props.onFinished();
}, },
@ -75,7 +78,8 @@ module.exports = React.createClass({
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId; var roomId = this.props.member.roomId;
var target = this.props.member.userId; var target = this.props.member.userId;
MatrixClientPeg.get().ban(roomId, target).done( this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().ban(roomId, target).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -86,7 +90,9 @@ module.exports = React.createClass({
description: err.message description: err.message
}); });
} }
); ).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
this.props.onFinished(); this.props.onFinished();
}, },
@ -122,7 +128,8 @@ module.exports = React.createClass({
level = parseInt(level); level = parseInt(level);
if (level !== NaN) { if (level !== NaN) {
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done( this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -133,9 +140,11 @@ module.exports = React.createClass({
description: err.message description: err.message
}); });
} }
); ).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
} }
this.props.onFinished(); this.props.onFinished();
}, },
onModToggle: function() { onModToggle: function() {
@ -164,7 +173,8 @@ module.exports = React.createClass({
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
// toggle the level // toggle the level
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel; var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).done( this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -183,12 +193,15 @@ module.exports = React.createClass({
}); });
} }
} }
); ).finally(()=>{
this.props.onFinished(); this.setState({ updating: this.state.updating - 1 });
});
this.props.onFinished();
}, },
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).done( this.setState({ updating: this.state.updating + 1 });
MatrixClientPeg.get().setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
function() { function() {
// NO-OP; rely on the m.room.member event coming down else we could // NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here! // get out of sync if we force setState here!
@ -199,7 +212,9 @@ module.exports = React.createClass({
description: err.message description: err.message
}); });
} }
); ).finally(()=>{
this.setState({ updating: this.state.updating - 1 });
});
this.props.onFinished(); this.props.onFinished();
}, },
@ -249,7 +264,7 @@ module.exports = React.createClass({
else { else {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent); this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
} }
}, },
onChatClick: function() { onChatClick: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -300,19 +315,17 @@ module.exports = React.createClass({
this.props.onFinished(); this.props.onFinished();
} }
else { else {
self.setState({ creatingRoom: true });
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
self.setState({ creatingRoom: false });
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, { Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register", title: "Please Register",
description: "Guest users can't create new rooms. Please register to create room and start a chat." description: "Guest users can't create new rooms. Please register to create room and start a chat."
}); });
self.props.onFinished(); self.props.onFinished();
return; return;
} }
self.setState({ updating: self.state.updating + 1 });
MatrixClientPeg.get().createRoom({ MatrixClientPeg.get().createRoom({
// XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat // XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat
invite: [this.props.member.userId], invite: [this.props.member.userId],
@ -328,24 +341,24 @@ module.exports = React.createClass({
type: 'm.room.guest_access', type: 'm.room.guest_access',
state_key: '', state_key: '',
} }
], ],
}).done( }).then(
function(res) { function(res) {
self.setState({ creatingRoom: false });
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: res.room_id room_id: res.room_id
}); });
self.props.onFinished(); self.props.onFinished();
}, function(err) { }, function(err) {
self.setState({ creatingRoom: false });
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
title: "Failure to start chat", title: "Failure to start chat",
description: err.message description: err.message
}); });
self.props.onFinished(); self.props.onFinished();
} }
); ).finally(()=>{
self.setState({ updating: self.state.updating - 1 });
});
} }
}, },
@ -367,7 +380,7 @@ module.exports = React.createClass({
}, },
muted: false, muted: false,
isTargetMod: false, isTargetMod: false,
creatingRoom: false updating: 0,
} }
}, },
@ -470,14 +483,14 @@ module.exports = React.createClass({
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/> startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
} }
if (this.state.creatingRoom) { if (this.state.updating) {
var Loader = sdk.getComponent("elements.Spinner"); var Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>; spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
} }
if (this.state.can.kick) { if (this.state.can.kick) {
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}> kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
Kick { this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
</div>; </div>;
} }
if (this.state.can.ban) { if (this.state.can.ban) {
@ -503,7 +516,7 @@ module.exports = React.createClass({
var adminTools; var adminTools;
if (kickButton || banButton || muteButton || giveModButton) { if (kickButton || banButton || muteButton || giveModButton) {
adminTools = adminTools =
<div> <div>
<h3>Admin tools</h3> <h3>Admin tools</h3>

View file

@ -64,15 +64,19 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomState.events", this.onRoomStateEvent); cli.on("RoomState.events", this.onRoomStateEvent);
cli.on("Room", this.onRoom); // invites cli.on("Room", this.onRoom); // invites
cli.on("User.presence", this.onUserPresence);
// cli.on("Room.timeline", this.onRoomTimeline);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
if (MatrixClientPeg.get()) { var cli = MatrixClientPeg.get();
MatrixClientPeg.get().removeListener("Room", this.onRoom); if (cli) {
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); cli.removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); cli.removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); cli.removeListener("RoomState.events", this.onRoomStateEvent);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvent); cli.removeListener("Room", this.onRoom);
cli.removeListener("User.presence", this.onUserPresence);
// cli.removeListener("Room.timeline", this.onRoomTimeline);
} }
}, },
@ -87,25 +91,45 @@ module.exports = React.createClass({
members: self.roomMembers() members: self.roomMembers()
}); });
}, 50); }, 50);
},
/*
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// 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;
// treat any activity from a user as implicit presence to update the
// ordering of the list whenever someone says something.
// Except right now we're not tiebreaking "active now" users in this way
// so don't bother for now.
if (ev.getSender()) {
// console.log("implicit presence from " + ev.getSender());
var tile = this.refs[ev.getSender()];
if (tile) {
// work around a race where you might have a room member object
// before the user object exists. XXX: why does this ever happen?
var all_members = room.currentState.members;
var userId = ev.getSender();
if (all_members[userId].user === null) {
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
}
this._updateList(); // reorder the membership list
}
}
},
*/
onUserPresence(event, user) {
// Attach a SINGLE listener for global presence changes then locate the // Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile // member tile and re-render it. This is more efficient than every tile
// evar attaching their own listener. // evar attaching their own listener.
function updateUserState(event, user) { // console.log("explicit presence from " + user.userId);
// XXX: evil hack to track the age of this presence info. var tile = this.refs[user.userId];
// this should be removed once syjs-28 is resolved in the JS SDK itself. if (tile) {
user.lastPresenceTs = Date.now(); this._updateList(); // reorder the membership list
var tile = self.refs[user.userId];
if (tile) {
self._updateList(); // reorder the membership list
}
} }
// FIXME: we should probably also reset 'lastActiveAgo' to zero whenever
// we see a typing notif from a user, as we don't get presence updates for those.
MatrixClientPeg.get().on("User.presence", updateUserState);
this.userPresenceFn = updateUserState;
}, },
onRoom: function(room) { onRoom: function(room) {
@ -133,6 +157,7 @@ module.exports = React.createClass({
}, },
_updateList: new rate_limited_func(function() { _updateList: new rate_limited_func(function() {
// console.log("Updating memberlist");
this.memberDict = this.getMemberDict(); this.memberDict = this.getMemberDict();
var self = this; var self = this;
@ -266,7 +291,6 @@ module.exports = React.createClass({
var all_members = room.currentState.members; var all_members = room.currentState.members;
// XXX: evil hack until SYJS-28 is fixed
Object.keys(all_members).map(function(userId) { Object.keys(all_members).map(function(userId) {
// work around a race where you might have a room member object // work around a race where you might have a room member object
// before the user object exists. This may or may not cause // before the user object exists. This may or may not cause
@ -275,9 +299,8 @@ module.exports = React.createClass({
all_members[userId].user = MatrixClientPeg.get().getUser(userId); all_members[userId].user = MatrixClientPeg.get().getUser(userId);
} }
if (all_members[userId].user && !all_members[userId].user.lastPresenceTs) { // XXX: this user may have no lastPresenceTs value!
all_members[userId].user.lastPresenceTs = Date.now(); // the right solution here is to fix the race rather than leave it as 0
}
}); });
return all_members; return all_members;
@ -288,7 +311,7 @@ module.exports = React.createClass({
var all_user_ids = Object.keys(all_members); var all_user_ids = Object.keys(all_members);
var ConferenceHandler = CallHandler.getConferenceHandler(); var ConferenceHandler = CallHandler.getConferenceHandler();
if (this.memberSort) all_user_ids.sort(this.memberSort); all_user_ids.sort(this.memberSort);
var to_display = []; var to_display = [];
var count = 0; var count = 0;
@ -325,27 +348,83 @@ module.exports = React.createClass({
}); });
}, },
memberSort: function(userIdA, userIdB) { memberString: function(member) {
var userA = this.memberDict[userIdA].user; if (!member) {
var userB = this.memberDict[userIdB].user; return "(null)";
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;
} }
else {
return "(" + member.name + ", " + member.powerLevel + ", " + member.user.lastActiveAgo + ", " + member.user.currentlyActive + ")";
}
},
var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastPresenceTs - userA.lastActiveAgo : 0; // returns negative if a comes before b,
var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastPresenceTs - userB.lastActiveAgo : 0; // returns 0 if a and b are equivalent in ordering
// returns positive if a comes after b.
memberSort: function(userIdA, userIdB) {
// order by last active, with "active now" first.
// ...and then by power
// ...and then alphabetically.
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
return lastActiveTsB - lastActiveTsA; var memberA = this.memberDict[userIdA];
var memberB = this.memberDict[userIdB];
var userA = memberA.user;
var userB = memberB.user;
// if (!userA || !userB) {
// console.log("comparing " + memberA.name + " user=" + memberA.user + " with " + memberB.name + " user=" + memberB.user);
// }
if (!userA && !userB) return 0;
if (userA && !userB) return -1;
if (!userA && userB) return 1;
// console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB));
if (userA.currentlyActive && userB.currentlyActive) {
// console.log(memberA.name + " and " + memberB.name + " are both active");
if (memberA.powerLevel === memberB.powerLevel) {
// console.log(memberA + " and " + memberB + " have same power level");
if (memberA.name && memberB.name) {
// console.log("comparing names: " + memberA.name + " and " + memberB.name);
return memberA.name.localeCompare(memberB.name);
}
else {
return 0;
}
}
else {
// console.log("comparing power: " + memberA.powerLevel + " and " + memberB.powerLevel);
return memberB.powerLevel - memberA.powerLevel;
}
}
if (userA.currentlyActive && !userB.currentlyActive) return -1;
if (!userA.currentlyActive && userB.currentlyActive) return 1;
// 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;
}, },
onSearchQueryChanged: function(input) { onSearchQueryChanged: function(input) {
@ -442,19 +521,16 @@ module.exports = React.createClass({
return ( return (
<div className="mx_MemberList"> <div className="mx_MemberList">
{inviteMemberListSection} {inviteMemberListSection}
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper"> <GeminiScrollbar autoshow={true}
relayoutOnUpdate={false}
className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt} <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}> createOverflowElement={this._createOverflowTile}>
{this.makeMemberTiles('join', this.state.searchQuery)} {this.makeMemberTiles('join', this.state.searchQuery)}
</TruncatedList> </TruncatedList>
{invitedSection} {invitedSection}
</GeminiScrollbar> </GeminiScrollbar>
<div className="mx_MemberList_bottom">
<div className="mx_MemberList_bottomRule">
</div>
</div>
</div> </div>
); );
} }
}); });

View file

@ -82,15 +82,13 @@ module.exports = React.createClass({
if (member.user) { if (member.user) {
this.user_last_modified_time = member.user.getLastModifiedTime(); this.user_last_modified_time = member.user.getLastModifiedTime();
// FIXME: make presence data update whenever User.presence changes...
active = member.user.lastActiveAgo ?
(Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) : -1;
} }
this.member_last_modified_time = member.getLastModifiedTime(); this.member_last_modified_time = member.getLastModifiedTime();
return ( return (
<EntityTile {...this.props} presenceActiveAgo={active} presenceState={presenceState} <EntityTile {...this.props} presenceState={presenceState}
presenceLastActiveAgo={ member.user ? member.user.lastActiveAgo : 0 }
presenceLastTs={ member.user ? member.user.lastPresenceTs : 0 }
presenceCurrentlyActive={ member.user ? member.user.currentlyActive : false } presenceCurrentlyActive={ member.user ? member.user.currentlyActive : false }
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick} avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerLevel={this.props.member.powerLevel} /> name={name} powerLevel={this.props.member.powerLevel} />

View file

@ -40,6 +40,9 @@ module.exports = React.createClass({
// callback when a file to upload is chosen // callback when a file to upload is chosen
uploadFile: React.PropTypes.func.isRequired, uploadFile: React.PropTypes.func.isRequired,
// opacity for dynamic UI fading effects
opacity: React.PropTypes.number,
}, },
onUploadClick: function(ev) { onUploadClick: function(ev) {
@ -55,7 +58,7 @@ module.exports = React.createClass({
var fileList = []; var fileList = [];
for(var i=0; i<files.length; i++) { for(var i=0; i<files.length; i++) {
fileList.push(<li> fileList.push(<li>
<TintableSvg src="img/files.svg" width="16" height="16" /> {files[i].name} <TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
</li>); </li>);
} }
@ -124,7 +127,7 @@ module.exports = React.createClass({
var controls = []; var controls = [];
controls.push( controls.push(
<div className="mx_MessageComposer_avatar"> <div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} /> <MemberAvatar member={me} width={24} height={24} />
</div> </div>
); );
@ -132,17 +135,17 @@ module.exports = React.createClass({
var callButton, videoCallButton, hangupButton; var callButton, videoCallButton, hangupButton;
if (this.props.callState && this.props.callState !== 'ended') { if (this.props.callState && this.props.callState !== 'ended') {
hangupButton = hangupButton =
<div className="mx_MessageComposer_hangup" onClick={this.onHangupClick}> <div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt="Hangup" title="Hangup" width="25" height="26"/> <img src="img/hangup.svg" alt="Hangup" title="Hangup" width="25" height="26"/>
</div>; </div>;
} }
else { else {
callButton = callButton =
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call"> <div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
<TintableSvg src="img/voice.svg" width="16" height="26"/> <TintableSvg src="img/voice.svg" width="16" height="26"/>
</div> </div>
videoCallButton = videoCallButton =
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call"> <div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
<TintableSvg src="img/call.svg" width="30" height="22"/> <TintableSvg src="img/call.svg" width="30" height="22"/>
</div> </div>
} }
@ -155,7 +158,7 @@ module.exports = React.createClass({
// check separately for whether we can call, but this is slightly // check separately for whether we can call, but this is slightly
// complex because of conference calls. // complex because of conference calls.
var uploadButton = ( var uploadButton = (
<div className="mx_MessageComposer_upload" <div key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title="Upload file"> onClick={this.onUploadClick} title="Upload file">
<TintableSvg src="img/upload.svg" width="19" height="24"/> <TintableSvg src="img/upload.svg" width="19" height="24"/>
<input ref="uploadInput" type="file" <input ref="uploadInput" type="file"
@ -166,7 +169,7 @@ module.exports = React.createClass({
); );
controls.push( controls.push(
<MessageComposerInput tabComplete={this.props.tabComplete} <MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
onResize={this.props.onResize} room={this.props.room} />, onResize={this.props.onResize} room={this.props.room} />,
uploadButton, uploadButton,
hangupButton, hangupButton,
@ -175,14 +178,14 @@ module.exports = React.createClass({
); );
} else { } else {
controls.push( controls.push(
<div className="mx_MessageComposer_noperm_error"> <div key="controls_error" className="mx_MessageComposer_noperm_error">
You do not have permission to post to this room You do not have permission to post to this room
</div> </div>
); );
} }
return ( return (
<div className="mx_MessageComposer"> <div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row"> <div className="mx_MessageComposer_row">
{controls} {controls}

View file

@ -76,6 +76,8 @@ module.exports = React.createClass({
render: function() { render: function() {
if (this.props.activeAgo >= 0) { if (this.props.activeAgo >= 0) {
var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago"); var ago = this.props.currentlyActive ? "now" : (this.getDuration(this.props.activeAgo) + " ago");
// var ago = this.getDuration(this.props.activeAgo) + " ago";
// if (this.props.currentlyActive) ago += " (now?)";
return ( return (
<div className="mx_PresenceLabel"> <div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { ago } { this.getPrettyPresence(this.props.presenceState) } { ago }

View file

@ -34,6 +34,8 @@ module.exports = React.createClass({
room: React.PropTypes.object, room: React.PropTypes.object,
oobData: React.PropTypes.object, oobData: React.PropTypes.object,
editing: React.PropTypes.bool, editing: React.PropTypes.bool,
saving: React.PropTypes.bool,
rightPanelCollapsed: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func, onSettingsClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func, onSaveClick: React.PropTypes.func,
onSearchClick: React.PropTypes.func, onSearchClick: React.PropTypes.func,
@ -51,6 +53,13 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
var cli = MatrixClientPeg.get(); var cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents); cli.on("RoomState.events", this._onRoomStateEvents);
// When a room name occurs, RoomState.events is fired *before*
// room.name is updated. So we have to listen to Room.name as well as
// RoomState.events.
if (this.props.room) {
this.props.room.on("Room.name", this._onRoomNameChange);
}
}, },
componentDidUpdate: function() { componentDidUpdate: function() {
@ -60,6 +69,9 @@ module.exports = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
if (this.props.room) {
this.props.room.removeListener("Room.name", this._onRoomNameChange);
}
var cli = MatrixClientPeg.get(); var cli = MatrixClientPeg.get();
if (cli) { if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents); cli.removeListener("RoomState.events", this._onRoomStateEvents);
@ -75,6 +87,10 @@ module.exports = React.createClass({
this.forceUpdate(); this.forceUpdate();
}, },
_onRoomNameChange: function(room) {
this.forceUpdate();
},
onAvatarPickerClick: function(ev) { onAvatarPickerClick: function(ev) {
if (this.refs.file_label) { if (this.refs.file_label) {
this.refs.file_label.click(); this.refs.file_label.click();
@ -96,7 +112,7 @@ module.exports = React.createClass({
description: "Failed to set avatar. " + errMsg description: "Failed to set avatar. " + errMsg
}); });
}).done(); }).done();
}, },
/** /**
* After editing the settings, get the new name for the room * After editing the settings, get the new name for the room
@ -134,6 +150,7 @@ module.exports = React.createClass({
var searchStatus = null; var searchStatus = null;
var topic_el = null; var topic_el = null;
var cancel_button = null; var cancel_button = null;
var spinner = null;
var save_button = null; var save_button = null;
var settings_button = null; var settings_button = null;
if (this.props.editing) { if (this.props.editing) {
@ -158,6 +175,11 @@ module.exports = React.createClass({
cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div> cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
} }
if (this.props.saving) {
var Spinner = sdk.getComponent("elements.Spinner");
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
}
if (can_set_room_name) { if (can_set_room_name) {
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor"); var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
name = <RoomNameEditor ref="nameEditor" room={this.props.room} /> name = <RoomNameEditor ref="nameEditor" room={this.props.room} />
@ -257,15 +279,21 @@ module.exports = React.createClass({
</div>; </div>;
} }
var rightPanel_buttons;
if (this.props.rightPanelCollapsed) {
// TODO: embed the RightPanel header in here if it's collapsed.
}
var right_row; var right_row;
if (!this.props.editing) { if (!this.props.editing) {
right_row = right_row =
<div className="mx_RoomHeader_rightRow"> <div className="mx_RoomHeader_rightRow">
{ forget_button } { forget_button }
{ leave_button } { leave_button }
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search"> <div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
<TintableSvg src="img/search.svg" width="21" height="19"/> <TintableSvg src="img/search.svg" width="21" height="19"/>
</div> </div>
{ rightPanel_buttons }
</div>; </div>;
} }
@ -280,6 +308,7 @@ module.exports = React.createClass({
{ topic_el } { topic_el }
</div> </div>
</div> </div>
{spinner}
{save_button} {save_button}
{cancel_button} {cancel_button}
{right_row} {right_row}

View file

@ -34,7 +34,8 @@ module.exports = React.createClass({
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool, collapsed: React.PropTypes.bool,
currentRoom: React.PropTypes.string currentRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string,
}, },
getInitialState: function() { getInitialState: function() {
@ -82,7 +83,7 @@ module.exports = React.createClass({
else { else {
this.setState({ this.setState({
incomingCall: null incomingCall: null
}); });
} }
break; break;
} }
@ -192,9 +193,9 @@ module.exports = React.createClass({
var me = room.getMember(MatrixClientPeg.get().credentials.userId); var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) return; if (!me) return;
// console.log("room = " + room.name + ", me.membership = " + me.membership + // console.log("room = " + room.name + ", me.membership = " + me.membership +
// ", sender = " + me.events.member.getSender() + // ", sender = " + me.events.member.getSender() +
// ", target = " + me.events.member.getStateKey() + // ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership); // ", prevMembership = " + me.events.member.getPrevContent().membership);
if (me.membership == "invite") { if (me.membership == "invite") {
@ -231,7 +232,7 @@ module.exports = React.createClass({
} }
} }
else { else {
s.lists["im.vector.fake.recent"].push(room); s.lists["im.vector.fake.recent"].push(room);
} }
} }
} }
@ -269,7 +270,7 @@ module.exports = React.createClass({
_repositionTooltip: function(e) { _repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) { if (this.tooltip && this.tooltip.parentElement) {
var scroll = ReactDOM.findDOMNode(this); var scroll = ReactDOM.findDOMNode(this);
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
} }
}, },
@ -312,12 +313,6 @@ module.exports = React.createClass({
} }
}, },
onShowClick: function() {
dis.dispatch({
action: 'show_left_panel',
});
},
onShowMoreRooms: function() { onShowMoreRooms: function() {
// kick gemini in the balls to get it to wake up // kick gemini in the balls to get it to wake up
// XXX: uuuuuuugh. // XXX: uuuuuuugh.
@ -325,18 +320,14 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var expandButton = this.props.collapsed ?
<img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
null;
var RoomSubList = sdk.getComponent('structures.RoomSubList'); var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this; var self = this;
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={ self._repositionTooltips } ref="gemscroll"> <GeminiScrollbar className="mx_RoomList_scrollbar"
relayoutOnUpdate={false}
autoshow={true} onScroll={ self._repositionTooltips } ref="gemscroll">
<div className="mx_RoomList"> <div className="mx_RoomList">
{ expandButton }
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] } <RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites" label="Invites"
editable={ false } editable={ false }
@ -344,7 +335,8 @@ module.exports = React.createClass({
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
onShowMoreRooms={ this.onShowMoreRooms } /> searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['m.favourite'] } <RoomSubList list={ self.state.lists['m.favourite'] }
label="Favourites" label="Favourites"
@ -355,7 +347,8 @@ module.exports = React.createClass({
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
onShowMoreRooms={ this.onShowMoreRooms } /> searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] } <RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label="Rooms" label="Rooms"
@ -365,7 +358,8 @@ module.exports = React.createClass({
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
onShowMoreRooms={ this.onShowMoreRooms } /> searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } />
{ Object.keys(self.state.lists).map(function(tagName) { { Object.keys(self.state.lists).map(function(tagName) {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|archived))$/)) { if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|archived))$/)) {
@ -379,6 +373,7 @@ module.exports = React.createClass({
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
} }
@ -393,7 +388,8 @@ module.exports = React.createClass({
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
onShowMoreRooms={ this.onShowMoreRooms } /> searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] } <RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label="Historical" label="Historical"
@ -406,7 +402,8 @@ module.exports = React.createClass({
showSpinner={ self.state.isLoadingLeftRooms } showSpinner={ self.state.isLoadingLeftRooms }
onHeaderClick= { self.onArchivedHeaderClick } onHeaderClick= { self.onArchivedHeaderClick }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
onShowMoreRooms={ this.onShowMoreRooms } /> searchFilter={ self.props.searchFilter }
onShowMoreRooms={ self.onShowMoreRooms } />
</div> </div>
</GeminiScrollbar> </GeminiScrollbar>
); );

View file

@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index'); var sdk = require('../../../index');
var Modal = require('../../../Modal'); var Modal = require('../../../Modal');
var ObjectUtils = require("../../../ObjectUtils"); var ObjectUtils = require("../../../ObjectUtils");
var dis = require("../../../dispatcher");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomSettings', displayName: 'RoomSettings',
@ -69,6 +70,20 @@ module.exports = React.createClass({
}, (err) => { }, (err) => {
console.error("Failed to get room visibility: " + err); console.error("Failed to get room visibility: " + err);
}); });
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 0.3,
middleOpacity: 0.3,
});
},
componentWillUnmount: function() {
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 1.0,
middleOpacity: 1.0,
});
}, },
setName: function(name) { setName: function(name) {

View file

@ -104,7 +104,10 @@ module.exports = React.createClass({
var label; var label;
if (!this.props.collapsed) { if (!this.props.collapsed) {
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
label = <div className={ className }>{name}</div>; if (this.props.selected) {
name = <span>{ name }</span>;
}
label = <div className={ className }>{ name }</div>;
} }
else if (this.state.hover) { else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");

View file

@ -162,9 +162,13 @@ var SearchableEntityList = React.createClass({
</div> </div>
); );
} }
list = <GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper"> list = (
{ list } <GeminiScrollbar autoshow={true}
</GeminiScrollbar>; relayoutOnUpdate={false}
className="mx_SearchableEntityList_listWrapper">
{ list }
</GeminiScrollbar>
);
} }
return ( return (

View file

@ -27,14 +27,21 @@ module.exports = React.createClass({
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string,
onCancelClick: React.PropTypes.func,
}, },
render: function() { render: function() {
var cancelButton;
if (this.props.onCancelClick) {
cancelButton = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
}
return ( return (
<div className="mx_RoomHeader" > <div className="mx_RoomHeader" >
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ this.props.title } { this.props.title }
{ cancelButton }
</div> </div>
</div> </div>
</div> </div>

View file

@ -20,6 +20,7 @@ var TestUtils = require('react-addons-test-utils');
var expect = require('expect'); var expect = require('expect');
var sdk = require('matrix-react-sdk'); var sdk = require('matrix-react-sdk');
var MatrixClientPeg = require('MatrixClientPeg');
var MessagePanel = sdk.getComponent('structures.MessagePanel'); var MessagePanel = sdk.getComponent('structures.MessagePanel');
@ -35,6 +36,9 @@ describe('MessagePanel', function () {
beforeEach(function() { beforeEach(function() {
test_utils.beforeEach(this); test_utils.beforeEach(this);
sandbox = test_utils.stubClient(sandbox); sandbox = test_utils.stubClient(sandbox);
var client = MatrixClientPeg.get();
client.credentials = {userId: '@me:here'};
}); });
afterEach(function () { afterEach(function () {
@ -93,7 +97,7 @@ describe('MessagePanel', function () {
// first render with the RM in one place // first render with the RM in one place
var mp = ReactDOM.render( var mp = ReactDOM.render(
<MessagePanel events={events} readMarkerEventId={events[4].getId()} <MessagePanel events={events} readMarkerEventId={events[4].getId()}
readMarkerVisible={true} readMarkerVisible={true}
/>, parentDiv); />, parentDiv);
var tiles = TestUtils.scryRenderedComponentsWithType( var tiles = TestUtils.scryRenderedComponentsWithType(
@ -109,13 +113,13 @@ describe('MessagePanel', function () {
// now move the RM // now move the RM
mp = ReactDOM.render( mp = ReactDOM.render(
<MessagePanel events={events} readMarkerEventId={events[6].getId()} <MessagePanel events={events} readMarkerEventId={events[6].getId()}
readMarkerVisible={true} readMarkerVisible={true}
/>, parentDiv); />, parentDiv);
// now there should be two RM containers // now there should be two RM containers
var found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container'); var found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container');
expect(found.length).toEqual(2); expect(found.length).toEqual(2);
// the first should be the ghost // the first should be the ghost
expect(found[0].previousSibling).toEqual(tileContainers[4]); expect(found[0].previousSibling).toEqual(tileContainers[4]);
var hr = found[0].children[0]; var hr = found[0].children[0];
@ -126,7 +130,7 @@ describe('MessagePanel', function () {
// advance the clock, and then let the browser run an animation frame, // advance the clock, and then let the browser run an animation frame,
// to let the animation start // to let the animation start
clock.tick(1500); clock.tick(1500);
realSetTimeout(() => { realSetTimeout(() => {
// then advance it again to let it complete // then advance it again to let it complete
clock.tick(1000); clock.tick(1000);

View file

@ -86,7 +86,8 @@ describe('TimelinePanel', function() {
// this is https://github.com/vector-im/vector-web/issues/1367 // this is https://github.com/vector-im/vector-web/issues/1367
// enough events to allow us to scroll back // enough events to allow us to scroll back
for (var i = 0; i < 40; i++) { var N_EVENTS = 20;
for (var i = 0; i < N_EVENTS; i++) {
timeline.addEvent(mkMessage()); timeline.addEvent(mkMessage());
} }
@ -119,7 +120,7 @@ describe('TimelinePanel', function() {
// happens // happens
awaitScroll().then(() => { awaitScroll().then(() => {
expect(panel.state.canBackPaginate).toBe(false); expect(panel.state.canBackPaginate).toBe(false);
expect(scryEventTiles(panel).length).toEqual(40); expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
// scroll up // scroll up
console.log("setting scrollTop = 0"); console.log("setting scrollTop = 0");
@ -145,12 +146,12 @@ describe('TimelinePanel', function() {
// that won't make much difference, because we don't paginate // that won't make much difference, because we don't paginate
// unless we're at the bottom of the timeline, but a scroll event // unless we're at the bottom of the timeline, but a scroll event
// should be enough to set off a pagination. // should be enough to set off a pagination.
expect(scryEventTiles(panel).length).toEqual(40); expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
scrollingDiv.scrollTop = 10; scrollingDiv.scrollTop = 10;
}).delay(0).then(awaitPaginationCompletion).then(() => { }).delay(0).then(awaitPaginationCompletion).then(() => {
expect(scryEventTiles(panel).length).toEqual(41); expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
}).done(done); }).done(done, done);
}); });
it('should not paginate forever if there are no events', function(done) { it('should not paginate forever if there are no events', function(done) {
@ -204,5 +205,3 @@ describe('TimelinePanel', function() {
}, 0); }, 0);
}); });
}); });