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

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

View file

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

View file

@ -147,6 +147,11 @@ module.exports = function (config) {
},
resolve: {
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',
},

View file

@ -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",

View file

@ -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', '');

View file

@ -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();

View file

@ -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");
},
};

View file

@ -68,6 +68,7 @@ module.exports.components['views.messages.MFileBody'] = require('./components/vi
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
module.exports.components['views.messages.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');

View file

@ -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>

View file

@ -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()}

View file

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

View file

@ -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">

View file

@ -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 }
/>
);
},

View file

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

View file

@ -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">

View file

@ -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 its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
var second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
var firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
},
render: function() {
@ -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"

View file

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

View file

@ -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;

View file

@ -0,0 +1,50 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
export default class MAudioBody extends React.Component {
constructor(props) {
super(props);
this.state = {
playing: false
}
}
onPlayToggle() {
this.setState({
playing: !this.state.playing
});
}
render() {
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
return (
<span className="mx_MAudioBody">
<audio src={cli.mxcUrlToHttp(content.url)} controls />
<MFileBody {...this.props} />
</span>
);
}
}

View file

@ -43,7 +43,7 @@ module.exports = React.createClass({
},
getEventTileOps: function() {
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}

View file

@ -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) {

View file

@ -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>

View file

@ -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,

View file

@ -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;

View file

@ -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>

View file

@ -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>
);
}
});

View file

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

View file

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

View file

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

View file

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

View file

@ -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>
);

View file

@ -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) {

View file

@ -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");

View file

@ -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 (

View file

@ -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>

View file

@ -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);

View file

@ -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);
});
});