mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Merge remote-tracking branch 'origin/develop' into dbkr/email_notifs
This commit is contained in:
commit
3381e2b057
36 changed files with 908 additions and 357 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
@ -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)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.3.1...v0.4.0)
|
||||
|
|
|
@ -147,6 +147,11 @@ module.exports = function (config) {
|
|||
},
|
||||
resolve: {
|
||||
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'),
|
||||
'sinon': 'sinon/pkg/sinon.js',
|
||||
},
|
||||
|
|
20
package.json
20
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.1",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -17,7 +17,7 @@
|
|||
"build": "babel src -d lib --source-maps",
|
||||
"start": "babel src -w -d lib --source-maps",
|
||||
"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-multi": "karma start --single-run=false"
|
||||
},
|
||||
|
@ -30,20 +30,20 @@
|
|||
"highlight.js": "^8.9.1",
|
||||
"linkifyjs": "^2.0.0-beta.4",
|
||||
"marked": "^0.3.5",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-js-sdk": "^0.5.2",
|
||||
"optimist": "^0.6.1",
|
||||
"q": "^1.4.1",
|
||||
"react": "^0.14.2",
|
||||
"react-dom": "^0.14.2",
|
||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#869a86b",
|
||||
"react": "^15.0.1",
|
||||
"react-dom": "^15.0.1",
|
||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#c3d942e",
|
||||
"sanitize-html": "^1.11.1",
|
||||
"velocity-animate": "^1.2.3",
|
||||
"velocity-ui-pack": "^1.2.2"
|
||||
},
|
||||
"//babelversion": [
|
||||
"brief experiments with babel6 seems to show that it generates source ",
|
||||
"maps which confuse chrome and make setting breakpoints tricky. So ",
|
||||
"let's stick with v5 for now."
|
||||
"brief experiments with babel6 seems to show that it generates source ",
|
||||
"maps which confuse chrome and make setting breakpoints tricky. So ",
|
||||
"let's stick with v5 for now."
|
||||
],
|
||||
"devDependencies": {
|
||||
"babel": "^5.8.23",
|
||||
|
@ -61,7 +61,7 @@
|
|||
"karma-webpack": "^1.7.0",
|
||||
"mocha": "^2.4.5",
|
||||
"phantomjs-prebuilt": "^2.1.7",
|
||||
"react-addons-test-utils": "^0.14.7",
|
||||
"react-addons-test-utils": "^15.0.1",
|
||||
"require-json": "0.0.1",
|
||||
"rimraf": "^2.4.3",
|
||||
"sinon": "^1.17.3",
|
||||
|
|
|
@ -37,7 +37,7 @@ if (packageJson['matrix-react-parent']) {
|
|||
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) {
|
||||
var file = files[i].replace('.js', '');
|
||||
|
||||
|
|
|
@ -74,10 +74,13 @@ class ContentMessages {
|
|||
var def = q.defer();
|
||||
if (file.type.indexOf('image/') == 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(file).then(function(imageInfo) {
|
||||
infoForImageFile(file).then(function (imageInfo) {
|
||||
extend(content.info, imageInfo);
|
||||
def.resolve();
|
||||
});
|
||||
} else if (file.type.indexOf('audio/') == 0) {
|
||||
content.msgtype = 'm.audio';
|
||||
def.resolve();
|
||||
} else {
|
||||
content.msgtype = 'm.file';
|
||||
def.resolve();
|
||||
|
|
|
@ -15,11 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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
|
||||
// react-sdk and apps layered on top.
|
||||
|
||||
var DEBUG = 0;
|
||||
|
||||
// The colour keys to be replaced as referred to in SVGs
|
||||
var keyRgb = [
|
||||
"rgb(118, 207, 166)", // Vector Green
|
||||
|
@ -75,6 +78,7 @@ var svgAttrs = [
|
|||
var cached = false;
|
||||
|
||||
function calcCssFixups() {
|
||||
if (DEBUG) console.log("calcSvgFixups start");
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var ss = document.styleSheets[i];
|
||||
if (!ss) continue; // well done safari >:(
|
||||
|
@ -105,13 +109,16 @@ function calcCssFixups() {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
}
|
||||
|
||||
function applyCssFixups() {
|
||||
if (DEBUG) console.log("applyCssFixups start");
|
||||
for (var i = 0; i < cssFixups.length; i++) {
|
||||
var cssFixup = cssFixups[i];
|
||||
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
||||
}
|
||||
if (DEBUG) console.log("applyCssFixups end");
|
||||
}
|
||||
|
||||
function hexToRgb(color) {
|
||||
|
@ -135,6 +142,7 @@ function rgbToHex(rgb) {
|
|||
|
||||
module.exports = {
|
||||
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
||||
|
||||
if (!cached) {
|
||||
calcCssFixups();
|
||||
cached = true;
|
||||
|
@ -173,11 +181,19 @@ module.exports = {
|
|||
|
||||
colors = [primaryColor, secondaryColor, tertiaryColor];
|
||||
|
||||
if (DEBUG) console.log("Tinter.tint");
|
||||
|
||||
// go through manually fixing up the stylesheets.
|
||||
applyCssFixups();
|
||||
|
||||
// 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
|
||||
|
@ -189,6 +205,7 @@ module.exports = {
|
|||
// updated would be a PITA, so just brute-force search for the
|
||||
// key colour; cache the element and apply.
|
||||
|
||||
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
||||
var fixups = [];
|
||||
for (var i = 0; i < svgs.length; i++) {
|
||||
var svgDoc;
|
||||
|
@ -223,14 +240,17 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
|
||||
return fixups;
|
||||
},
|
||||
|
||||
applySvgFixups: function(fixups) {
|
||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
||||
for (var i = 0; i < fixups.length; i++) {
|
||||
var svgFixup = fixups[i];
|
||||
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
||||
}
|
||||
if (DEBUG) console.log("applySvgFixups end");
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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.MVideoBody'] = require('./components/views/messages/MVideoBody');
|
||||
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.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
||||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
||||
|
|
|
@ -67,6 +67,8 @@ module.exports = React.createClass({
|
|||
collapse_rhs: false,
|
||||
ready: false,
|
||||
width: 10000,
|
||||
sideOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
};
|
||||
if (s.logged_in) {
|
||||
if (MatrixClientPeg.get().getRooms().length) {
|
||||
|
@ -183,6 +185,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._stopMatrixClient();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
|
@ -258,12 +261,7 @@ module.exports = React.createClass({
|
|||
window.localStorage.setItem("mx_hs_url", hsUrl);
|
||||
window.localStorage.setItem("mx_is_url", isUrl);
|
||||
}
|
||||
Notifier.stop();
|
||||
UserActivity.stop();
|
||||
Presence.stop();
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().removeAllListeners();
|
||||
MatrixClientPeg.unset();
|
||||
this._stopMatrixClient();
|
||||
this.notifyNewScreen('login');
|
||||
this.replaceState({
|
||||
logged_in: false,
|
||||
|
@ -369,7 +367,7 @@ module.exports = React.createClass({
|
|||
onFinished: function(should_leave) {
|
||||
if (should_leave) {
|
||||
var d = MatrixClientPeg.get().leave(roomId);
|
||||
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
var modal = Modal.createDialog(Loader);
|
||||
|
@ -534,6 +532,12 @@ module.exports = React.createClass({
|
|||
collapse_rhs: false,
|
||||
});
|
||||
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);
|
||||
if (theAlias) presentedId = theAlias;
|
||||
|
||||
var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
||||
var color_scheme = {};
|
||||
if (color_scheme_event) {
|
||||
color_scheme = color_scheme_event.getContent();
|
||||
// XXX: we should validate the event
|
||||
}
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
// No need to do this given RoomView triggers it itself...
|
||||
// var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
||||
// var color_scheme = {};
|
||||
// if (color_scheme_event) {
|
||||
// color_scheme = color_scheme_event.getContent();
|
||||
// // XXX: we should validate the event
|
||||
// }
|
||||
// console.log("Tinter.tint from _viewRoom");
|
||||
// Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
}
|
||||
|
||||
if (eventId) {
|
||||
|
@ -624,10 +630,13 @@ module.exports = React.createClass({
|
|||
if (!this.refs.roomView) {
|
||||
return;
|
||||
}
|
||||
|
||||
var roomview = this.refs.roomView;
|
||||
var roomId = this.refs.roomView.getRoomId();
|
||||
if (!roomId) {
|
||||
return;
|
||||
}
|
||||
var state = roomview.getScrollState();
|
||||
this.scrollStateMap[roomview.props.roomId] = state;
|
||||
this.scrollStateMap[roomId] = state;
|
||||
},
|
||||
|
||||
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) {
|
||||
/*
|
||||
// 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({
|
||||
action: 'view_user',
|
||||
member: member,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutClick: function(event) {
|
||||
|
@ -1008,6 +1027,7 @@ module.exports = React.createClass({
|
|||
|
||||
onUserSettingsClose: function() {
|
||||
// XXX: use browser history instead to find the previous room?
|
||||
// or maintain a this.state.pageHistory in _setPage()?
|
||||
if (this.state.currentRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
|
@ -1034,7 +1054,7 @@ module.exports = React.createClass({
|
|||
var NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
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
|
||||
if (this.state.screen == 'post_registration') {
|
||||
|
@ -1052,29 +1072,29 @@ module.exports = React.createClass({
|
|||
page_element = (
|
||||
<RoomView
|
||||
ref="roomView"
|
||||
roomId={this.state.currentRoom}
|
||||
roomAlias={this.state.currentRoomAlias}
|
||||
roomAddress={this.state.currentRoom || this.state.currentRoomAlias}
|
||||
eventId={this.state.initialEventId}
|
||||
thirdPartyInvite={this.state.thirdPartyInvite}
|
||||
oobData={this.state.roomOobData}
|
||||
highlightedEventId={this.state.highlightedEventId}
|
||||
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||
key={this.state.currentRoom}
|
||||
opacity={this.state.middleOpacity}
|
||||
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;
|
||||
case this.PageTypes.UserSettings:
|
||||
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;
|
||||
case this.PageTypes.CreateRoom:
|
||||
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;
|
||||
case this.PageTypes.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;
|
||||
}
|
||||
|
||||
|
@ -1098,7 +1118,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_MatrixChat_wrapper">
|
||||
{topBar}
|
||||
<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">
|
||||
{page_element}
|
||||
</main>
|
||||
|
|
|
@ -19,6 +19,8 @@ var ReactDOM = require("react-dom");
|
|||
var dis = require("../../dispatcher");
|
||||
var sdk = require('../../index');
|
||||
|
||||
var MatrixClientPeg = require('../../MatrixClientPeg')
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
|
@ -65,6 +67,9 @@ module.exports = React.createClass({
|
|||
|
||||
// callback which is called when more content is needed.
|
||||
onFillRequest: React.PropTypes.func,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
|
@ -147,7 +152,7 @@ module.exports = React.createClass({
|
|||
this.refs.scrollPanel.scrollToBottom();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
|
@ -332,13 +337,17 @@ module.exports = React.createClass({
|
|||
// Local echos have a send "status".
|
||||
var scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
var readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||
|
||||
ret.push(
|
||||
<li key={eventId}
|
||||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-token={scrollToken}>
|
||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||
onWidgetLoad={this._onWidgetLoad}
|
||||
last={last} isSelectedEvent={highlight} />
|
||||
readReceipts={readReceipts}
|
||||
eventSendStatus={mxEv.status}
|
||||
last={last} isSelectedEvent={highlight}/>
|
||||
</li>
|
||||
);
|
||||
|
||||
|
@ -356,6 +365,30 @@ module.exports = React.createClass({
|
|||
!== 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) {
|
||||
var hr;
|
||||
if (visible) {
|
||||
|
@ -423,12 +456,15 @@ module.exports = React.createClass({
|
|||
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
|
||||
}
|
||||
|
||||
var style = this.props.hidden ? { display: 'none' } : {};
|
||||
style.opacity = this.props.opacity;
|
||||
|
||||
return (
|
||||
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel"
|
||||
onScroll={ this.props.onScroll }
|
||||
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel mx_fadable"
|
||||
onScroll={ this.props.onScroll }
|
||||
onResize={ this.onResize }
|
||||
onFillRequest={ this.props.onFillRequest }
|
||||
style={ this.props.hidden ? { display: 'none' } : {} }
|
||||
style={ style }
|
||||
stickyBottom={ this.props.stickyBottom }>
|
||||
{topSpinner}
|
||||
{this._getEventTiles()}
|
||||
|
|
|
@ -54,11 +54,15 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
|
||||
// if we are referring to this room by a given alias (e.g. in the URL), track it.
|
||||
// useful for joining rooms by alias correctly (and fixing https://github.com/vector-im/vector-web/issues/819)
|
||||
roomAlias: React.PropTypes.string,
|
||||
// the ID for this room (or, if we don't know it, an alias for it)
|
||||
//
|
||||
// XXX: if this is an alias, we will display a 'join' dialogue,
|
||||
// regardless of whether we are already a member, or if the room is
|
||||
// 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
|
||||
// Fields:
|
||||
|
@ -90,10 +94,13 @@ module.exports = React.createClass({
|
|||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
// Typically this will either be the same as 'eventId', or undefined.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
|
||||
// is the RightPanel collapsed?
|
||||
rightPanelCollapsed: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomAddress);
|
||||
return {
|
||||
room: room,
|
||||
roomLoading: !room,
|
||||
|
@ -123,7 +130,6 @@ module.exports = React.createClass({
|
|||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
// 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
|
||||
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||
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({
|
||||
room: room,
|
||||
roomLoading: false,
|
||||
|
@ -200,14 +206,15 @@ module.exports = React.createClass({
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -233,7 +240,7 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var call = CallHandler.getCallForRoom(payload.room_id);
|
||||
var call = this._getCallForRoom();
|
||||
var callState;
|
||||
|
||||
if (call) {
|
||||
|
@ -256,7 +263,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -270,7 +277,7 @@ module.exports = React.createClass({
|
|||
if (this.unmounted) return;
|
||||
|
||||
// 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:
|
||||
// updates from pagination will happen when the paginate completes.
|
||||
|
@ -286,7 +293,7 @@ module.exports = React.createClass({
|
|||
// no change
|
||||
}
|
||||
else {
|
||||
this.setState((state, props) => {
|
||||
this.setState((state, props) => {
|
||||
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)
|
||||
// This will happen if we start off viewing a room we're not joined,
|
||||
// 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({
|
||||
room: room
|
||||
room: room,
|
||||
joining: false,
|
||||
});
|
||||
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() {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var room = this.state.room;
|
||||
if (!room) return;
|
||||
|
||||
var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
|
||||
|
@ -352,7 +347,8 @@ module.exports = React.createClass({
|
|||
if (color_scheme_event) {
|
||||
color_scheme = color_scheme_event.getContent();
|
||||
// XXX: we should validate the event
|
||||
}
|
||||
}
|
||||
console.log("Tinter.tint from updateTint");
|
||||
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") {
|
||||
var color_scheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
if (member.roomId === this.props.roomId) {
|
||||
// a member state changed in this room, refresh the tab complete list
|
||||
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) {
|
||||
// ignore if we don't have a room yet
|
||||
if (!this.state.room) {
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
|
@ -403,12 +405,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_updateConfCallNotification: function() {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var room = this.state.room;
|
||||
if (!room || !this.props.ConferenceHandler) {
|
||||
return;
|
||||
}
|
||||
var confMember = room.getMember(
|
||||
this.props.ConferenceHandler.getConferenceUserIdForRoom(this.props.roomId)
|
||||
this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId)
|
||||
);
|
||||
|
||||
if (!confMember) {
|
||||
|
@ -427,7 +429,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var call = CallHandler.getCallForRoom(this.props.roomId);
|
||||
var call = this._getCallForRoom();
|
||||
var callState = call ? call.call_state : "ended";
|
||||
this.setState({
|
||||
callState: callState
|
||||
|
@ -559,25 +561,35 @@ module.exports = React.createClass({
|
|||
|
||||
display_name_promise.then(() => {
|
||||
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 } )
|
||||
}).done(function() {
|
||||
}).then(function(resp) {
|
||||
var roomId = resp.roomId;
|
||||
|
||||
// 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,
|
||||
// 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
|
||||
// "join" we'll keep this flag set until it comes down /sync.
|
||||
// joining flag set until we see it.
|
||||
|
||||
// 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,
|
||||
// once we have a useable room object.
|
||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||
var me = MatrixClientPeg.get().credentials.userId;
|
||||
self.setState({
|
||||
joining: room ? !room.hasMembershipState(me, "join") : true,
|
||||
room: room
|
||||
});
|
||||
}, function(error) {
|
||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) {
|
||||
// wait for the room to turn up in onRoom.
|
||||
self._joiningRoomId = roomId;
|
||||
} else {
|
||||
// we've got a valid room, but that might also just mean that
|
||||
// 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({
|
||||
joining: false,
|
||||
joinError: error
|
||||
|
@ -612,7 +624,8 @@ module.exports = React.createClass({
|
|||
description: msg
|
||||
});
|
||||
}
|
||||
});
|
||||
}).done();
|
||||
|
||||
this.setState({
|
||||
joining: true
|
||||
});
|
||||
|
@ -667,7 +680,7 @@ module.exports = React.createClass({
|
|||
uploadFile: function(file) {
|
||||
var self = this;
|
||||
ContentMessages.sendContentToRoom(
|
||||
file, this.props.roomId, MatrixClientPeg.get()
|
||||
file, this.state.room.roomId, MatrixClientPeg.get()
|
||||
).done(undefined, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
|
@ -702,7 +715,7 @@ module.exports = React.createClass({
|
|||
filter = {
|
||||
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
|
||||
rooms: [
|
||||
this.props.roomId
|
||||
this.state.room.roomId
|
||||
]
|
||||
};
|
||||
}
|
||||
|
@ -860,6 +873,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onSettingsSaveClick: function() {
|
||||
if (!this.refs.room_settings) return;
|
||||
|
||||
this.setState({
|
||||
uploadingRoomSettings: true,
|
||||
});
|
||||
|
@ -901,6 +916,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
this.setState({editingRoomSettings: false});
|
||||
},
|
||||
|
@ -908,12 +924,12 @@ module.exports = React.createClass({
|
|||
onLeaveClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.roomId,
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
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' });
|
||||
}, function(err) {
|
||||
var errCode = err.errcode || "unknown error code";
|
||||
|
@ -930,7 +946,7 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
rejecting: true
|
||||
});
|
||||
MatrixClientPeg.get().leave(this.props.roomId).done(function() {
|
||||
MatrixClientPeg.get().leave(this.props.roomAddress).done(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
self.setState({
|
||||
rejecting: false
|
||||
|
@ -984,7 +1000,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
// update the read marker to match the read-receipt
|
||||
forgetReadMarker: function() {
|
||||
forgetReadMarker: function(ev) {
|
||||
ev.stopPropagation();
|
||||
this.refs.messagePanel.forgetReadMarker();
|
||||
},
|
||||
|
||||
|
@ -1087,7 +1104,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onMuteAudioClick: function() {
|
||||
var call = CallHandler.getCallForRoom(this.props.roomId);
|
||||
var call = this._getCallForRoom();
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
@ -1099,7 +1116,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onMuteVideoClick: function() {
|
||||
var call = CallHandler.getCallForRoom(this.props.roomId);
|
||||
var call = this._getCallForRoom();
|
||||
if (!call) {
|
||||
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,
|
||||
// otherwise react calls it with null on each update.
|
||||
_gatherTimelinePanelRef: function(r) {
|
||||
this.refs.messagePanel = r;
|
||||
if(r) {
|
||||
console.log("updateTint from RoomView._gatherTimelinePanelRef");
|
||||
this.updateTint();
|
||||
}
|
||||
},
|
||||
|
@ -1161,13 +1202,12 @@ module.exports = React.createClass({
|
|||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.props.roomId) {
|
||||
if (this.state.roomLoading) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
else {
|
||||
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.
|
||||
return (
|
||||
<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">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
|
@ -1196,14 +1240,8 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
@ -1233,7 +1271,7 @@ module.exports = React.createClass({
|
|||
inviterName={ inviterName }
|
||||
canJoin={ true } canPreview={ false }
|
||||
spinner={this.state.joining}
|
||||
room={this.state.room}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</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.
|
||||
// Display the "normal" room view.
|
||||
|
||||
var call = CallHandler.getCallForRoom(this.props.roomId);
|
||||
var call = this._getCallForRoom();
|
||||
var inCall = false;
|
||||
if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
|
||||
inCall = true;
|
||||
|
@ -1257,13 +1295,6 @@ module.exports = React.createClass({
|
|||
|
||||
var statusBar;
|
||||
|
||||
// for testing UI...
|
||||
// this.state.upload = {
|
||||
// uploadedBytes: 123493,
|
||||
// totalBytes: 347534,
|
||||
// fileName: "testing_fooble.jpg",
|
||||
// }
|
||||
|
||||
if (ContentMessages.getCurrentUploads().length > 0) {
|
||||
var UploadBar = sdk.getComponent('structures.UploadBar');
|
||||
statusBar = <UploadBar room={this.state.room} />
|
||||
|
@ -1314,7 +1345,7 @@ module.exports = React.createClass({
|
|||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
canPreview={this.state.canPeek}
|
||||
room={this.state.room}
|
||||
room={this.state.room}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1339,7 +1370,7 @@ module.exports = React.createClass({
|
|||
messageComposer =
|
||||
<MessageComposer
|
||||
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
|
||||
|
@ -1394,8 +1425,12 @@ module.exports = React.createClass({
|
|||
|
||||
if (this.state.searchResults) {
|
||||
searchResultsPanel = (
|
||||
<ScrollPanel ref="searchResultsPanel" className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||
onFillRequest={ this.onSearchResultsFillRequest } onResize={ this.onSearchResultsResize }>
|
||||
<ScrollPanel ref="searchResultsPanel"
|
||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||
onFillRequest={ this.onSearchResultsFillRequest }
|
||||
onResize={ this.onSearchResultsResize }
|
||||
style={{ opacity: this.props.opacity }}
|
||||
>
|
||||
<li className={scrollheader_classes}></li>
|
||||
{this.getSearchResultTiles()}
|
||||
</ScrollPanel>
|
||||
|
@ -1412,13 +1447,14 @@ module.exports = React.createClass({
|
|||
eventPixelOffset={this.props.eventPixelOffset}
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
||||
opacity={ this.props.opacity }
|
||||
/>);
|
||||
|
||||
var topUnreadMessagesBar = null;
|
||||
if (this.state.showTopUnreadMessagesBar) {
|
||||
var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
|
||||
topUnreadMessagesBar = (
|
||||
<div className="mx_RoomView_topUnreadMessagesBar">
|
||||
<div className="mx_RoomView_topUnreadMessagesBar mx_fadable" style={{ opacity: this.props.opacity }}>
|
||||
<TopUnreadMessagesBar
|
||||
onScrollUpClick={this.jumpToReadMarker}
|
||||
onCloseClick={this.forgetReadMarker}
|
||||
|
@ -1432,6 +1468,7 @@ module.exports = React.createClass({
|
|||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
editing={this.state.editingRoomSettings}
|
||||
saving={this.state.uploadingRoomSettings}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onSaveClick={this.onSettingsSaveClick}
|
||||
|
@ -1446,7 +1483,7 @@ module.exports = React.createClass({
|
|||
{ topUnreadMessagesBar }
|
||||
{ messagePanel }
|
||||
{ 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_line"></div>
|
||||
{ statusBar }
|
||||
|
|
|
@ -540,6 +540,7 @@ module.exports = React.createClass({
|
|||
// it's not obvious why we have a separate div and ol anyway.
|
||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
relayoutOnUpdate={false}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
|
|
|
@ -76,6 +76,9 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: React.PropTypes.func,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -172,8 +175,27 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
||||
!ObjectUtils.shallowEqual(this.state, nextState));
|
||||
if (!ObjectUtils.shallowEqual(this.props, nextProps)) {
|
||||
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() {
|
||||
|
@ -222,8 +244,8 @@ var TimelinePanel = React.createClass({
|
|||
this.setState({
|
||||
[paginatingKey]: false,
|
||||
[canPaginateKey]: r,
|
||||
events: this._getEvents(),
|
||||
});
|
||||
this._reloadEvents();
|
||||
return r;
|
||||
});
|
||||
},
|
||||
|
@ -264,25 +286,14 @@ var TimelinePanel = React.createClass({
|
|||
// updates from pagination will happen when the paginate completes.
|
||||
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.getScrollState().stuckAtBottom) return;
|
||||
|
||||
// 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;
|
||||
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
||||
this.setState({readMarkerVisible: true});
|
||||
if (!this.refs.messagePanel.getScrollState().stuckAtBottom) {
|
||||
// we won't load this event now, because we don't want to push any
|
||||
// events off the other end of the timeline. But we need to note
|
||||
// that we can now paginate.
|
||||
this.setState({canForwardPaginate: true});
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
// ScrollPanel might already have an active pagination promise, which
|
||||
// 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
|
||||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false)
|
||||
.done(this._reloadEvents);
|
||||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
||||
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) {
|
||||
|
@ -717,6 +763,13 @@ var TimelinePanel = React.createClass({
|
|||
// the results if so.
|
||||
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();
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
this.setState({
|
||||
events: events,
|
||||
});
|
||||
return events;
|
||||
},
|
||||
|
||||
_indexForEventId: function(evId) {
|
||||
|
@ -792,7 +843,7 @@ var TimelinePanel = React.createClass({
|
|||
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||
},
|
||||
|
||||
_setReadMarker: function(eventId, eventTs) {
|
||||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||
if (TimelinePanel.roomReadMarkerMap[this.props.room.roomId] == eventId) {
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
|
@ -807,6 +858,10 @@ var TimelinePanel = React.createClass({
|
|||
// above or below the visible timeline, we stash the timestamp.
|
||||
TimelinePanel.roomReadMarkerTsMap[this.props.room.roomId] = eventTs;
|
||||
|
||||
if (inhibitSetState) {
|
||||
return;
|
||||
}
|
||||
|
||||
// run the render cycle before calling the callback, so that
|
||||
// getReadMarkerPosition() returns the right thing.
|
||||
this.setState({
|
||||
|
@ -861,6 +916,7 @@ var TimelinePanel = React.createClass({
|
|||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
opacity={ this.props.opacity }
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -45,6 +45,17 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
|||
|
||||
render: function() {
|
||||
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) {
|
||||
return <div />
|
||||
}
|
||||
|
|
|
@ -51,7 +51,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
var self = this;
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 0.3,
|
||||
middleOpacity: 0.3,
|
||||
});
|
||||
this._refreshFromServer();
|
||||
},
|
||||
|
||||
|
@ -61,6 +65,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
});
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
|
@ -321,7 +330,7 @@ module.exports = React.createClass({
|
|||
var notification_area;
|
||||
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
|
||||
notification_area = (<div>
|
||||
<h2>Notifications</h2>
|
||||
<h3>Notifications</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<Notifications threepids={this.state.threepids} />
|
||||
|
@ -331,11 +340,13 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<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_profileTable">
|
||||
|
@ -366,10 +377,10 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Account</h2>
|
||||
<h3>Account</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
|
||||
|
||||
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||
Log out
|
||||
</div>
|
||||
|
@ -379,7 +390,7 @@ module.exports = React.createClass({
|
|||
|
||||
{notification_area}
|
||||
|
||||
<h2>Advanced</h2>
|
||||
<h3>Advanced</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
|
|
|
@ -99,15 +99,36 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_getInitialLetter: function() {
|
||||
var name = this.props.name;
|
||||
//For large characters (exceeding 2 bytes), this function will get the correct character.
|
||||
//However, this does NOT get the second character correctly if a large character is before it.
|
||||
var initial = String.fromCodePoint(name.codePointAt(0));
|
||||
if ((initial === '@' || initial === '#') && name[1]) {
|
||||
initial = String.fromCodePoint(name.codePointAt(1));
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
*/
|
||||
_getInitialLetter: function(name) {
|
||||
if (name.length < 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 it’s 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() {
|
||||
|
@ -116,7 +137,7 @@ module.exports = React.createClass({
|
|||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
var initialLetter = this._getInitialLetter();
|
||||
var initialLetter = this._getInitialLetter(this.props.name);
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...this.props}>
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
|
|
|
@ -24,6 +24,7 @@ const KEY_WINDOWS = 91;
|
|||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
|
||||
propTypes: {
|
||||
onValueChanged: React.PropTypes.func,
|
||||
initialValue: React.PropTypes.string,
|
||||
|
|
|
@ -18,10 +18,9 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
var dis = require("../../../dispatcher");
|
||||
var Tinter = require("../../../Tinter");
|
||||
|
||||
module.exports = React.createClass({
|
||||
var TintableSvg = React.createClass({
|
||||
displayName: 'TintableSvg',
|
||||
|
||||
propTypes: {
|
||||
|
@ -31,39 +30,48 @@ module.exports = React.createClass({
|
|||
className: React.PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
// list of currently mounted TintableSvgs
|
||||
mounts: {},
|
||||
idSequence: 0,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.fixups = [];
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// we can't use onLoad on object due to https://github.com/facebook/react/pull/5781
|
||||
// so handle it with pure DOM instead
|
||||
ReactDOM.findDOMNode(this).addEventListener('load', this.onLoad);
|
||||
this.id = TintableSvg.idSequence++;
|
||||
TintableSvg.mounts[this.id] = this;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
ReactDOM.findDOMNode(this).removeEventListener('load', this.onLoad);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
delete TintableSvg.mounts[this.id];
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== 'tint_update') return;
|
||||
tint: function() {
|
||||
// TODO: only bother running this if the global tint settings have changed
|
||||
// since we loaded!
|
||||
Tinter.applySvgFixups(this.fixups);
|
||||
},
|
||||
|
||||
onLoad: function(event) {
|
||||
// console.log("TintableSvg.onLoad for " + this.props.src);
|
||||
this.fixups = Tinter.calcSvgFixups([event.target]);
|
||||
Tinter.applySvgFixups(this.fixups);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<object className={ "mx_TintableSvg " + this.props.className }
|
||||
<object className={ "mx_TintableSvg " + (this.props.className ? this.props.className : "") }
|
||||
type="image/svg+xml"
|
||||
data={ this.props.src }
|
||||
width={ this.props.width }
|
||||
height={ this.props.height }/>
|
||||
height={ this.props.height }
|
||||
onLoad={ this.onLoad }
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TintableSvg;
|
||||
|
|
50
src/components/views/messages/MAudioBody.js
Normal file
50
src/components/views/messages/MAudioBody.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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() {
|
||||
|
@ -55,6 +55,7 @@ module.exports = React.createClass({
|
|||
'm.emote': sdk.getComponent('messages.TextualBody'),
|
||||
'm.image': sdk.getComponent('messages.MImageBody'),
|
||||
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody')
|
||||
};
|
||||
|
||||
|
@ -63,6 +64,9 @@ module.exports = React.createClass({
|
|||
var BodyType = UnknownBody;
|
||||
if (msgtype && 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}
|
||||
|
|
|
@ -84,7 +84,10 @@ module.exports = React.createClass({
|
|||
findLink: function(nodes) {
|
||||
for (var i = 0; i < nodes.length; 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;
|
||||
}
|
||||
else if (node.children && node.children.length) {
|
||||
|
|
|
@ -37,7 +37,8 @@ module.exports = React.createClass({
|
|||
avatarJsx: React.PropTypes.any, // <BaseAvatar />
|
||||
className: React.PropTypes.string,
|
||||
presenceState: React.PropTypes.string,
|
||||
presenceActiveAgo: React.PropTypes.number,
|
||||
presenceLastActiveAgo: React.PropTypes.number,
|
||||
presenceLastTs: React.PropTypes.number,
|
||||
presenceCurrentlyActive: React.PropTypes.bool,
|
||||
showInviteButton: React.PropTypes.bool,
|
||||
shouldComponentUpdate: React.PropTypes.func,
|
||||
|
@ -50,7 +51,8 @@ module.exports = React.createClass({
|
|||
shouldComponentUpdate: function(nextProps, nextState) { return true; },
|
||||
onClick: function() {},
|
||||
presenceState: "offline",
|
||||
presenceActiveAgo: -1,
|
||||
presenceLastActiveAgo: 0,
|
||||
presenceLastTs: 0,
|
||||
showInviteButton: false,
|
||||
suppressOnHover: false
|
||||
};
|
||||
|
@ -82,13 +84,16 @@ module.exports = React.createClass({
|
|||
var nameEl;
|
||||
|
||||
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";
|
||||
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
|
||||
<PresenceLabel activeAgo={this.props.presenceActiveAgo}
|
||||
<PresenceLabel activeAgo={ activeAgo }
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />
|
||||
</div>
|
||||
|
|
|
@ -29,6 +29,8 @@ var Velociraptor = require('../../../Velociraptor');
|
|||
require('../../../VelocityBounce');
|
||||
var dispatcher = require("../../../dispatcher");
|
||||
|
||||
var ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
var bounce = false;
|
||||
try {
|
||||
if (global.localStorage) {
|
||||
|
@ -107,12 +109,75 @@ module.exports = React.createClass({
|
|||
|
||||
/* callback called when dynamic content in events are loaded */
|
||||
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() {
|
||||
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() {
|
||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
@ -137,7 +202,7 @@ module.exports = React.createClass({
|
|||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
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() {
|
||||
self.setState({menu: false});
|
||||
}
|
||||
|
@ -153,20 +218,6 @@ module.exports = React.createClass({
|
|||
|
||||
getReadAvatars: function() {
|
||||
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 left = 0;
|
||||
|
@ -176,11 +227,9 @@ module.exports = React.createClass({
|
|||
easing: 'easeOut'
|
||||
};
|
||||
|
||||
var receipts = this.props.readReceipts || [];
|
||||
for (var i = 0; i < receipts.length; ++i) {
|
||||
var member = room.getMember(receipts[i].userId);
|
||||
if (!member) {
|
||||
continue;
|
||||
}
|
||||
var member = receipts[i];
|
||||
|
||||
// Using react refs here would mean both getting Velociraptor to expose
|
||||
// them and making them scoped to the whole RoomView. Not impossible, but
|
||||
|
@ -302,9 +351,9 @@ module.exports = React.createClass({
|
|||
var classes = classNames({
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_sending: ['sending', 'queued'].indexOf(
|
||||
this.props.mxEvent.status
|
||||
this.props.eventSendStatus
|
||||
) !== -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_selected: this.props.isSelectedEvent,
|
||||
mx_EventTile_continuation: this.props.continuation,
|
||||
|
|
|
@ -45,14 +45,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState(
|
||||
{ preview: res },
|
||||
this.props.onWidgetLoad
|
||||
);
|
||||
}, (error)=>{
|
||||
console.error("Failed to get preview for " + this.props.link + " " + error);
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -65,6 +69,10 @@ module.exports = React.createClass({
|
|||
linkifyElement(this.refs.description, linkifyMatrix.options);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.unmounted = true;
|
||||
},
|
||||
|
||||
onImageClick: function(ev) {
|
||||
var p = this.state.preview;
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
|
|
|
@ -57,7 +57,8 @@ module.exports = React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
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
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Kick success");
|
||||
|
@ -67,7 +68,9 @@ module.exports = React.createClass({
|
|||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
).finally(()=>{
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -75,7 +78,8 @@ module.exports = React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
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() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -86,7 +90,9 @@ module.exports = React.createClass({
|
|||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
).finally(()=>{
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -122,7 +128,8 @@ module.exports = React.createClass({
|
|||
level = parseInt(level);
|
||||
|
||||
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() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -133,9 +140,11 @@ module.exports = React.createClass({
|
|||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
).finally(()=>{
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
});
|
||||
}
|
||||
this.props.onFinished();
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
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
|
||||
// toggle the level
|
||||
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() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -183,12 +193,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
).finally(()=>{
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
_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() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -199,7 +212,9 @@ module.exports = React.createClass({
|
|||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
).finally(()=>{
|
||||
this.setState({ updating: this.state.updating - 1 });
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -249,7 +264,7 @@ module.exports = React.createClass({
|
|||
else {
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
onChatClick: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -300,19 +315,17 @@ module.exports = React.createClass({
|
|||
this.props.onFinished();
|
||||
}
|
||||
else {
|
||||
self.setState({ creatingRoom: true });
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
self.setState({ creatingRoom: false });
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't create new rooms. Please register to create room and start a chat."
|
||||
});
|
||||
self.props.onFinished();
|
||||
self.props.onFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
self.setState({ updating: self.state.updating + 1 });
|
||||
MatrixClientPeg.get().createRoom({
|
||||
// XXX: FIXME: deduplicate this with "view_create_room" in MatrixChat
|
||||
invite: [this.props.member.userId],
|
||||
|
@ -328,24 +341,24 @@ module.exports = React.createClass({
|
|||
type: 'm.room.guest_access',
|
||||
state_key: '',
|
||||
}
|
||||
],
|
||||
}).done(
|
||||
],
|
||||
}).then(
|
||||
function(res) {
|
||||
self.setState({ creatingRoom: false });
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: res.room_id
|
||||
});
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.setState({ creatingRoom: false });
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failure to start chat",
|
||||
description: err.message
|
||||
});
|
||||
self.props.onFinished();
|
||||
}
|
||||
);
|
||||
).finally(()=>{
|
||||
self.setState({ updating: self.state.updating - 1 });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -367,7 +380,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
muted: 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 }/>
|
||||
}
|
||||
|
||||
if (this.state.creatingRoom) {
|
||||
if (this.state.updating) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
|
||||
}
|
||||
|
||||
if (this.state.can.kick) {
|
||||
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
|
||||
Kick
|
||||
{ this.props.member.membership === "invite" ? "Disinvite" : "Kick" }
|
||||
</div>;
|
||||
}
|
||||
if (this.state.can.ban) {
|
||||
|
@ -503,7 +516,7 @@ module.exports = React.createClass({
|
|||
|
||||
var adminTools;
|
||||
if (kickButton || banButton || muteButton || giveModButton) {
|
||||
adminTools =
|
||||
adminTools =
|
||||
<div>
|
||||
<h3>Admin tools</h3>
|
||||
|
||||
|
|
|
@ -64,15 +64,19 @@ module.exports = React.createClass({
|
|||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("RoomState.events", this.onRoomStateEvent);
|
||||
cli.on("Room", this.onRoom); // invites
|
||||
cli.on("User.presence", this.onUserPresence);
|
||||
// cli.on("Room.timeline", this.onRoomTimeline);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
cli.removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvent);
|
||||
cli.removeListener("Room", this.onRoom);
|
||||
cli.removeListener("User.presence", this.onUserPresence);
|
||||
// cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -87,25 +91,45 @@ module.exports = React.createClass({
|
|||
members: self.roomMembers()
|
||||
});
|
||||
}, 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
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// evar attaching their own listener.
|
||||
function updateUserState(event, user) {
|
||||
// XXX: evil hack to track the age of this presence info.
|
||||
// this should be removed once syjs-28 is resolved in the JS SDK itself.
|
||||
user.lastPresenceTs = Date.now();
|
||||
|
||||
var tile = self.refs[user.userId];
|
||||
|
||||
if (tile) {
|
||||
self._updateList(); // reorder the membership list
|
||||
}
|
||||
// console.log("explicit presence from " + user.userId);
|
||||
var tile = this.refs[user.userId];
|
||||
if (tile) {
|
||||
this._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) {
|
||||
|
@ -133,6 +157,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_updateList: new rate_limited_func(function() {
|
||||
// console.log("Updating memberlist");
|
||||
this.memberDict = this.getMemberDict();
|
||||
|
||||
var self = this;
|
||||
|
@ -266,7 +291,6 @@ module.exports = React.createClass({
|
|||
|
||||
var all_members = room.currentState.members;
|
||||
|
||||
// XXX: evil hack until SYJS-28 is fixed
|
||||
Object.keys(all_members).map(function(userId) {
|
||||
// work around a race where you might have a room member object
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (all_members[userId].user && !all_members[userId].user.lastPresenceTs) {
|
||||
all_members[userId].user.lastPresenceTs = Date.now();
|
||||
}
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
});
|
||||
|
||||
return all_members;
|
||||
|
@ -288,7 +311,7 @@ module.exports = React.createClass({
|
|||
var all_user_ids = Object.keys(all_members);
|
||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
|
||||
if (this.memberSort) all_user_ids.sort(this.memberSort);
|
||||
all_user_ids.sort(this.memberSort);
|
||||
|
||||
var to_display = [];
|
||||
var count = 0;
|
||||
|
@ -325,27 +348,83 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
memberSort: function(userIdA, userIdB) {
|
||||
var userA = this.memberDict[userIdA].user;
|
||||
var userB = this.memberDict[userIdB].user;
|
||||
|
||||
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;
|
||||
memberString: function(member) {
|
||||
if (!member) {
|
||||
return "(null)";
|
||||
}
|
||||
else {
|
||||
return "(" + member.name + ", " + member.powerLevel + ", " + member.user.lastActiveAgo + ", " + member.user.currentlyActive + ")";
|
||||
}
|
||||
},
|
||||
|
||||
var lastActiveTsA = userA && userA.lastActiveAgo ? userA.lastPresenceTs - userA.lastActiveAgo : 0;
|
||||
var lastActiveTsB = userB && userB.lastActiveAgo ? userB.lastPresenceTs - userB.lastActiveAgo : 0;
|
||||
// returns negative if a comes before b,
|
||||
// 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) {
|
||||
|
@ -442,19 +521,16 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberList">
|
||||
{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}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{this.makeMemberTiles('join', this.state.searchQuery)}
|
||||
</TruncatedList>
|
||||
{invitedSection}
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_MemberList_bottom">
|
||||
<div className="mx_MemberList_bottomRule">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -82,15 +82,13 @@ module.exports = React.createClass({
|
|||
|
||||
if (member.user) {
|
||||
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();
|
||||
|
||||
|
||||
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 }
|
||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||
name={name} powerLevel={this.props.member.powerLevel} />
|
||||
|
|
|
@ -40,6 +40,9 @@ module.exports = React.createClass({
|
|||
|
||||
// callback when a file to upload is chosen
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
},
|
||||
|
||||
onUploadClick: function(ev) {
|
||||
|
@ -55,7 +58,7 @@ module.exports = React.createClass({
|
|||
var fileList = [];
|
||||
for(var i=0; i<files.length; i++) {
|
||||
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>);
|
||||
}
|
||||
|
||||
|
@ -124,7 +127,7 @@ module.exports = React.createClass({
|
|||
var controls = [];
|
||||
|
||||
controls.push(
|
||||
<div className="mx_MessageComposer_avatar">
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
</div>
|
||||
);
|
||||
|
@ -132,17 +135,17 @@ module.exports = React.createClass({
|
|||
var callButton, videoCallButton, hangupButton;
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
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"/>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
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"/>
|
||||
</div>
|
||||
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"/>
|
||||
</div>
|
||||
}
|
||||
|
@ -155,7 +158,7 @@ module.exports = React.createClass({
|
|||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
var uploadButton = (
|
||||
<div className="mx_MessageComposer_upload"
|
||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick} title="Upload file">
|
||||
<TintableSvg src="img/upload.svg" width="19" height="24"/>
|
||||
<input ref="uploadInput" type="file"
|
||||
|
@ -166,7 +169,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput tabComplete={this.props.tabComplete}
|
||||
<MessageComposerInput key="controls_input" tabComplete={this.props.tabComplete}
|
||||
onResize={this.props.onResize} room={this.props.room} />,
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
|
@ -175,14 +178,14 @@ module.exports = React.createClass({
|
|||
);
|
||||
} else {
|
||||
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
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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_row">
|
||||
{controls}
|
||||
|
|
|
@ -76,6 +76,8 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
if (this.props.activeAgo >= 0) {
|
||||
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 (
|
||||
<div className="mx_PresenceLabel">
|
||||
{ this.getPrettyPresence(this.props.presenceState) } { ago }
|
||||
|
|
|
@ -34,6 +34,8 @@ module.exports = React.createClass({
|
|||
room: React.PropTypes.object,
|
||||
oobData: React.PropTypes.object,
|
||||
editing: React.PropTypes.bool,
|
||||
saving: React.PropTypes.bool,
|
||||
rightPanelCollapsed: React.PropTypes.bool,
|
||||
onSettingsClick: React.PropTypes.func,
|
||||
onSaveClick: React.PropTypes.func,
|
||||
onSearchClick: React.PropTypes.func,
|
||||
|
@ -51,6 +53,13 @@ module.exports = React.createClass({
|
|||
componentDidMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
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() {
|
||||
|
@ -60,6 +69,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this.props.room) {
|
||||
this.props.room.removeListener("Room.name", this._onRoomNameChange);
|
||||
}
|
||||
var cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
|
@ -75,6 +87,10 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
},
|
||||
|
||||
_onRoomNameChange: function(room) {
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
onAvatarPickerClick: function(ev) {
|
||||
if (this.refs.file_label) {
|
||||
this.refs.file_label.click();
|
||||
|
@ -96,7 +112,7 @@ module.exports = React.createClass({
|
|||
description: "Failed to set avatar. " + errMsg
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* After editing the settings, get the new name for the room
|
||||
|
@ -134,6 +150,7 @@ module.exports = React.createClass({
|
|||
var searchStatus = null;
|
||||
var topic_el = null;
|
||||
var cancel_button = null;
|
||||
var spinner = null;
|
||||
var save_button = null;
|
||||
var settings_button = null;
|
||||
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>
|
||||
}
|
||||
|
||||
if (this.props.saving) {
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
|
||||
}
|
||||
|
||||
if (can_set_room_name) {
|
||||
var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor");
|
||||
name = <RoomNameEditor ref="nameEditor" room={this.props.room} />
|
||||
|
@ -257,15 +279,21 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
var rightPanel_buttons;
|
||||
if (this.props.rightPanelCollapsed) {
|
||||
// TODO: embed the RightPanel header in here if it's collapsed.
|
||||
}
|
||||
|
||||
var right_row;
|
||||
if (!this.props.editing) {
|
||||
right_row =
|
||||
right_row =
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||
</div>
|
||||
{ rightPanel_buttons }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -280,6 +308,7 @@ module.exports = React.createClass({
|
|||
{ topic_el }
|
||||
</div>
|
||||
</div>
|
||||
{spinner}
|
||||
{save_button}
|
||||
{cancel_button}
|
||||
{right_row}
|
||||
|
|
|
@ -34,7 +34,8 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
collapsed: React.PropTypes.bool,
|
||||
currentRoom: React.PropTypes.string
|
||||
currentRoom: React.PropTypes.string,
|
||||
searchFilter: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -82,7 +83,7 @@ module.exports = React.createClass({
|
|||
else {
|
||||
this.setState({
|
||||
incomingCall: null
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -192,9 +193,9 @@ module.exports = React.createClass({
|
|||
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
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() +
|
||||
// ", target = " + me.events.member.getStateKey() +
|
||||
// ", target = " + me.events.member.getStateKey() +
|
||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||
|
||||
if (me.membership == "invite") {
|
||||
|
@ -231,7 +232,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
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) {
|
||||
if (this.tooltip && this.tooltip.parentElement) {
|
||||
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() {
|
||||
// kick gemini in the balls to get it to wake up
|
||||
// XXX: uuuuuuugh.
|
||||
|
@ -325,18 +320,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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 self = this;
|
||||
|
||||
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">
|
||||
{ expandButton }
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
label="Invites"
|
||||
editable={ false }
|
||||
|
@ -344,7 +335,8 @@ module.exports = React.createClass({
|
|||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
onShowMoreRooms={ this.onShowMoreRooms } />
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
label="Favourites"
|
||||
|
@ -355,7 +347,8 @@ module.exports = React.createClass({
|
|||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
onShowMoreRooms={ this.onShowMoreRooms } />
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||
label="Rooms"
|
||||
|
@ -365,7 +358,8 @@ module.exports = React.createClass({
|
|||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
onShowMoreRooms={ this.onShowMoreRooms } />
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|archived))$/)) {
|
||||
|
@ -379,6 +373,7 @@ module.exports = React.createClass({
|
|||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
}
|
||||
|
@ -393,7 +388,8 @@ module.exports = React.createClass({
|
|||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed }
|
||||
onShowMoreRooms={ this.onShowMoreRooms } />
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||
label="Historical"
|
||||
|
@ -406,7 +402,8 @@ module.exports = React.createClass({
|
|||
showSpinner={ self.state.isLoadingLeftRooms }
|
||||
onHeaderClick= { self.onArchivedHeaderClick }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
onShowMoreRooms={ this.onShowMoreRooms } />
|
||||
searchFilter={ self.props.searchFilter }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ var MatrixClientPeg = require('../../../MatrixClientPeg');
|
|||
var sdk = require('../../../index');
|
||||
var Modal = require('../../../Modal');
|
||||
var ObjectUtils = require("../../../ObjectUtils");
|
||||
var dis = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomSettings',
|
||||
|
@ -69,6 +70,20 @@ module.exports = React.createClass({
|
|||
}, (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) {
|
||||
|
|
|
@ -104,7 +104,10 @@ module.exports = React.createClass({
|
|||
var label;
|
||||
if (!this.props.collapsed) {
|
||||
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) {
|
||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
|
|
|
@ -162,9 +162,13 @@ var SearchableEntityList = React.createClass({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
list = <GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper">
|
||||
{ list }
|
||||
</GeminiScrollbar>;
|
||||
list = (
|
||||
<GeminiScrollbar autoshow={true}
|
||||
relayoutOnUpdate={false}
|
||||
className="mx_SearchableEntityList_listWrapper">
|
||||
{ list }
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -27,14 +27,21 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
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 (
|
||||
<div className="mx_RoomHeader" >
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ this.props.title }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ var TestUtils = require('react-addons-test-utils');
|
|||
var expect = require('expect');
|
||||
|
||||
var sdk = require('matrix-react-sdk');
|
||||
var MatrixClientPeg = require('MatrixClientPeg');
|
||||
|
||||
var MessagePanel = sdk.getComponent('structures.MessagePanel');
|
||||
|
||||
|
@ -35,6 +36,9 @@ describe('MessagePanel', function () {
|
|||
beforeEach(function() {
|
||||
test_utils.beforeEach(this);
|
||||
sandbox = test_utils.stubClient(sandbox);
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
client.credentials = {userId: '@me:here'};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
@ -93,7 +97,7 @@ describe('MessagePanel', function () {
|
|||
// first render with the RM in one place
|
||||
var mp = ReactDOM.render(
|
||||
<MessagePanel events={events} readMarkerEventId={events[4].getId()}
|
||||
readMarkerVisible={true}
|
||||
readMarkerVisible={true}
|
||||
/>, parentDiv);
|
||||
|
||||
var tiles = TestUtils.scryRenderedComponentsWithType(
|
||||
|
@ -109,13 +113,13 @@ describe('MessagePanel', function () {
|
|||
// now move the RM
|
||||
mp = ReactDOM.render(
|
||||
<MessagePanel events={events} readMarkerEventId={events[6].getId()}
|
||||
readMarkerVisible={true}
|
||||
readMarkerVisible={true}
|
||||
/>, parentDiv);
|
||||
|
||||
// now there should be two RM containers
|
||||
var found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container');
|
||||
expect(found.length).toEqual(2);
|
||||
|
||||
|
||||
// the first should be the ghost
|
||||
expect(found[0].previousSibling).toEqual(tileContainers[4]);
|
||||
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,
|
||||
// to let the animation start
|
||||
clock.tick(1500);
|
||||
|
||||
|
||||
realSetTimeout(() => {
|
||||
// then advance it again to let it complete
|
||||
clock.tick(1000);
|
||||
|
|
|
@ -86,7 +86,8 @@ describe('TimelinePanel', function() {
|
|||
// this is https://github.com/vector-im/vector-web/issues/1367
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
|
@ -119,7 +120,7 @@ describe('TimelinePanel', function() {
|
|||
// happens
|
||||
awaitScroll().then(() => {
|
||||
expect(panel.state.canBackPaginate).toBe(false);
|
||||
expect(scryEventTiles(panel).length).toEqual(40);
|
||||
expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
|
||||
|
||||
// scroll up
|
||||
console.log("setting scrollTop = 0");
|
||||
|
@ -145,12 +146,12 @@ describe('TimelinePanel', function() {
|
|||
// that won't make much difference, because we don't paginate
|
||||
// unless we're at the bottom of the timeline, but a scroll event
|
||||
// should be enough to set off a pagination.
|
||||
expect(scryEventTiles(panel).length).toEqual(40);
|
||||
expect(scryEventTiles(panel).length).toEqual(N_EVENTS);
|
||||
|
||||
scrollingDiv.scrollTop = 10;
|
||||
}).delay(0).then(awaitPaginationCompletion).then(() => {
|
||||
expect(scryEventTiles(panel).length).toEqual(41);
|
||||
}).done(done);
|
||||
expect(scryEventTiles(panel).length).toEqual(N_EVENTS+1);
|
||||
}).done(done, done);
|
||||
});
|
||||
|
||||
it('should not paginate forever if there are no events', function(done) {
|
||||
|
@ -204,5 +205,3 @@ describe('TimelinePanel', function() {
|
|||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue