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

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