mirror of
https://github.com/element-hq/element-web
synced 2024-10-26 12:45:53 +03:00
Merge remote-tracking branch 'origin/develop' into dbkr/no_auto_join
This commit is contained in:
commit
11df2fc285
6 changed files with 495 additions and 239 deletions
|
@ -39,7 +39,8 @@ function createClient(hs_url, is_url, user_id, access_token, guestAccess) {
|
||||||
baseUrl: hs_url,
|
baseUrl: hs_url,
|
||||||
idBaseUrl: is_url,
|
idBaseUrl: is_url,
|
||||||
accessToken: access_token,
|
accessToken: access_token,
|
||||||
userId: user_id
|
userId: user_id,
|
||||||
|
timelineSupport: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
|
|
|
@ -316,7 +316,10 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
this._viewRoom(payload.room_id, payload.show_settings);
|
// by default we autoPeek rooms, unless we were called explicitly with
|
||||||
|
// autoPeek=false by something like RoomDirectory who has already peeked
|
||||||
|
this.setState({ autoPeek : payload.auto_peek === false ? false : true });
|
||||||
|
this._viewRoom(payload.room_id, payload.show_settings, payload.event_id);
|
||||||
break;
|
break;
|
||||||
case 'view_prev_room':
|
case 'view_prev_room':
|
||||||
roomIndexDelta = -1;
|
roomIndexDelta = -1;
|
||||||
|
@ -351,7 +354,8 @@ module.exports = React.createClass({
|
||||||
if (foundRoom) {
|
if (foundRoom) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: foundRoom.roomId
|
room_id: foundRoom.roomId,
|
||||||
|
event_id: payload.event_id,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -360,7 +364,8 @@ module.exports = React.createClass({
|
||||||
function(result) {
|
function(result) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: result.room_id
|
room_id: result.room_id,
|
||||||
|
event_id: payload.event_id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -432,15 +437,34 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_viewRoom: function(roomId, showSettings) {
|
// switch view to the given room
|
||||||
|
//
|
||||||
|
// eventId is optional and will cause a switch to the context of that
|
||||||
|
// particular event.
|
||||||
|
_viewRoom: function(roomId, showSettings, eventId) {
|
||||||
// before we switch room, record the scroll state of the current room
|
// before we switch room, record the scroll state of the current room
|
||||||
this._updateScrollMap();
|
this._updateScrollMap();
|
||||||
|
|
||||||
this.focusComposer = true;
|
this.focusComposer = true;
|
||||||
|
|
||||||
var newState = {
|
var newState = {
|
||||||
currentRoom: roomId,
|
currentRoom: roomId,
|
||||||
|
initialEventId: eventId,
|
||||||
|
highlightedEventId: eventId,
|
||||||
|
initialEventPixelOffset: undefined,
|
||||||
page_type: this.PageTypes.RoomView,
|
page_type: this.PageTypes.RoomView,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if we aren't given an explicit event id, look for one in the
|
||||||
|
// scrollStateMap.
|
||||||
|
if (!eventId) {
|
||||||
|
var scrollState = this.scrollStateMap[roomId];
|
||||||
|
if (scrollState) {
|
||||||
|
newState.initialEventId = scrollState.focussedEvent;
|
||||||
|
newState.initialEventPixelOffset = scrollState.pixelOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.sdkReady) {
|
if (this.sdkReady) {
|
||||||
// if the SDK is not ready yet, remember what room
|
// if the SDK is not ready yet, remember what room
|
||||||
// we're supposed to be on but don't notify about
|
// we're supposed to be on but don't notify about
|
||||||
|
@ -462,14 +486,14 @@ module.exports = React.createClass({
|
||||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventId) {
|
||||||
|
presentedId += "/"+eventId;
|
||||||
|
}
|
||||||
this.notifyNewScreen('room/'+presentedId);
|
this.notifyNewScreen('room/'+presentedId);
|
||||||
newState.ready = true;
|
newState.ready = true;
|
||||||
}
|
}
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
if (this.scrollStateMap[roomId]) {
|
|
||||||
var scrollState = this.scrollStateMap[roomId];
|
|
||||||
this.refs.roomView.restoreScrollState(scrollState);
|
|
||||||
}
|
|
||||||
if (this.refs.roomView && showSettings) {
|
if (this.refs.roomView && showSettings) {
|
||||||
this.refs.roomView.showSettings(true);
|
this.refs.roomView.showSettings(true);
|
||||||
}
|
}
|
||||||
|
@ -517,9 +541,11 @@ module.exports = React.createClass({
|
||||||
if (self.starting_room_alias) {
|
if (self.starting_room_alias) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room_alias',
|
action: 'view_room_alias',
|
||||||
room_alias: self.starting_room_alias
|
room_alias: self.starting_room_alias,
|
||||||
|
event_id: self.starting_event_id,
|
||||||
});
|
});
|
||||||
delete self.starting_room_alias;
|
delete self.starting_room_alias;
|
||||||
|
delete self.starting_event_id;
|
||||||
} else if (!self.state.page_type) {
|
} else if (!self.state.page_type) {
|
||||||
if (!self.state.currentRoom) {
|
if (!self.state.currentRoom) {
|
||||||
var firstRoom = null;
|
var firstRoom = null;
|
||||||
|
@ -645,23 +671,28 @@ module.exports = React.createClass({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomString = screen.split('/')[1];
|
var segments = screen.substring(5).split('/');
|
||||||
|
var roomString = segments[0];
|
||||||
|
var eventId = segments[1]; // undefined if no event id given
|
||||||
if (roomString[0] == '#') {
|
if (roomString[0] == '#') {
|
||||||
if (this.state.logged_in) {
|
if (this.state.logged_in) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room_alias',
|
action: 'view_room_alias',
|
||||||
room_alias: roomString
|
room_alias: roomString,
|
||||||
|
event_id: eventId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Okay, we'll take you here soon...
|
// Okay, we'll take you here soon...
|
||||||
this.starting_room_alias = roomString;
|
this.starting_room_alias = roomString;
|
||||||
|
this.starting_event_id = eventId;
|
||||||
// ...but you're still going to have to log in.
|
// ...but you're still going to have to log in.
|
||||||
this.notifyNewScreen('login');
|
this.notifyNewScreen('login');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: roomString
|
room_id: roomString,
|
||||||
|
event_id: eventId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -839,6 +870,10 @@ module.exports = React.createClass({
|
||||||
<RoomView
|
<RoomView
|
||||||
ref="roomView"
|
ref="roomView"
|
||||||
roomId={this.state.currentRoom}
|
roomId={this.state.currentRoom}
|
||||||
|
eventId={this.state.initialEventId}
|
||||||
|
highlightedEventId={this.state.highlightedEventId}
|
||||||
|
eventPixelOffset={this.state.initialEventPixelOffset}
|
||||||
|
autoPeek={this.state.autoPeek}
|
||||||
key={this.state.currentRoom}
|
key={this.state.currentRoom}
|
||||||
ConferenceHandler={this.props.ConferenceHandler} />
|
ConferenceHandler={this.props.ConferenceHandler} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -26,6 +26,7 @@ var ReactDOM = require("react-dom");
|
||||||
var q = require("q");
|
var q = require("q");
|
||||||
var classNames = require("classnames");
|
var classNames = require("classnames");
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
var EventTimeline = Matrix.EventTimeline;
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var ContentMessages = require("../../ContentMessages");
|
var ContentMessages = require("../../ContentMessages");
|
||||||
|
@ -44,6 +45,7 @@ var Tinter = require("../../Tinter");
|
||||||
var PAGINATE_SIZE = 20;
|
var PAGINATE_SIZE = 20;
|
||||||
var INITIAL_SIZE = 20;
|
var INITIAL_SIZE = 20;
|
||||||
var SEND_READ_RECEIPT_DELAY = 2000;
|
var SEND_READ_RECEIPT_DELAY = 2000;
|
||||||
|
var TIMELINE_CAP = 1000; // the most events to show in a timeline
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
|
@ -60,6 +62,22 @@ module.exports = React.createClass({
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: React.PropTypes.any,
|
||||||
roomId: React.PropTypes.string,
|
roomId: React.PropTypes.string,
|
||||||
autoPeek: React.PropTypes.bool, // Now unused, left here temporarily to avoid merge conflicts with @richvdh's branch.
|
autoPeek: React.PropTypes.bool, // Now unused, left here temporarily to avoid merge conflicts with @richvdh's branch.
|
||||||
|
|
||||||
|
roomId: React.PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// id of an event to jump to. If not given, will use the read-up-to-marker.
|
||||||
|
eventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// where to position the event given by eventId, in pixels from the
|
||||||
|
// bottom of the viewport. If not given, will try to put the event in the
|
||||||
|
// middle of the viewprt.
|
||||||
|
eventPixelOffset: React.PropTypes.number,
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
autoPeek: React.PropTypes.bool, // should we try to peek the room on mount, or has whoever invoked us already initiated a peek?
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -76,7 +94,9 @@ module.exports = React.createClass({
|
||||||
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
|
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
|
||||||
return {
|
return {
|
||||||
room: room,
|
room: room,
|
||||||
messageCap: INITIAL_SIZE,
|
events: [],
|
||||||
|
canBackPaginate: true,
|
||||||
|
paginating: room != null,
|
||||||
editingRoomSettings: false,
|
editingRoomSettings: false,
|
||||||
uploadingRoomSettings: false,
|
uploadingRoomSettings: false,
|
||||||
numUnreadMessages: 0,
|
numUnreadMessages: 0,
|
||||||
|
@ -86,19 +106,22 @@ module.exports = React.createClass({
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||||
callState: null,
|
callState: null,
|
||||||
autoPeekDone: false, // track whether our autoPeek (if any) has completed)
|
timelineLoading: true, // track whether our room timeline is loading
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null,
|
readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null,
|
||||||
readMarkerGhostEventId: undefined,
|
readMarkerGhostEventId: undefined,
|
||||||
atBottom: true,
|
|
||||||
|
// this is true if we are fully scrolled-down, and are looking at
|
||||||
|
// the end of the live timeline. It has the effect of hiding the
|
||||||
|
// 'scroll to bottom' knob, among a couple of other things.
|
||||||
|
atEndOfLiveTimeline: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.last_rr_sent_event_id = undefined;
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room", this.onNewRoom);
|
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||||
|
@ -116,6 +139,15 @@ module.exports = React.createClass({
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// to make the timeline load work correctly, build up a chain of promises which
|
||||||
|
// take us through the necessary steps.
|
||||||
|
|
||||||
|
// First of all, we may need to load the room. Construct a promise
|
||||||
|
// which resolves to the Room object.
|
||||||
|
var roomProm;
|
||||||
|
|
||||||
// if this is an unknown room then we're in one of three states:
|
// if this is an unknown room then we're in one of three states:
|
||||||
// - This is a room we can peek into (search engine) (we can /peek)
|
// - This is a room we can peek into (search engine) (we can /peek)
|
||||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||||
|
@ -124,22 +156,92 @@ module.exports = React.createClass({
|
||||||
// We can /peek though. If it fails then we present the join UI. If it
|
// We can /peek though. If it fails then we present the join UI. If it
|
||||||
// succeeds then great, show the preview (but we still may be able to /join!).
|
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
if (this.props.autoPeek) {
|
if (!this.props.autoPeek) {
|
||||||
|
console.log("No room loaded, and autopeek disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Attempting to peek into room %s", this.props.roomId);
|
console.log("Attempting to peek into room %s", this.props.roomId);
|
||||||
MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => {
|
|
||||||
|
roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => {
|
||||||
console.error("Failed to peek into room: %s", err);
|
console.error("Failed to peek into room: %s", err);
|
||||||
}).finally(() => {
|
throw err;
|
||||||
// we don't need to do anything - JS SDK will emit Room events
|
}).then((room) => {
|
||||||
// which will update the UI.
|
|
||||||
this.setState({
|
this.setState({
|
||||||
autoPeekDone: true
|
room: room
|
||||||
|
});
|
||||||
|
return room;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
roomProm = q(this.state.room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, load the timeline.
|
||||||
|
roomProm.then((room) => {
|
||||||
|
this._calculatePeekRules(room);
|
||||||
|
return this._initTimeline(this.props);
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
_initTimeline: function(props) {
|
||||||
|
var initialEvent = props.eventId;
|
||||||
|
if (!initialEvent) {
|
||||||
|
// go to the 'read-up-to' mark if no explicit event given
|
||||||
|
initialEvent = this.state.readMarkerEventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixelOffset = props.eventPixelOffset;
|
||||||
|
return this._loadTimeline(initialEvent, pixelOffset);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (re)-load the event timeline, and initialise the scroll state, centered
|
||||||
|
* around the given event.
|
||||||
|
*
|
||||||
|
* @param {string?} eventId the event to focus on. If undefined, will
|
||||||
|
* scroll to the bottom of the room.
|
||||||
|
*
|
||||||
|
* @param {number?} pixelOffset offset to position the given event at
|
||||||
|
* (pixels from the bottom of the view). If undefined, will put the
|
||||||
|
* event in the middle of the view.
|
||||||
|
*
|
||||||
|
* returns a promise which will resolve when the load completes.
|
||||||
|
*/
|
||||||
|
_loadTimeline: function(eventId, pixelOffset) {
|
||||||
|
// TODO: we could optimise this, by not resetting the window if the
|
||||||
|
// event is in the current window (though it's not obvious how we can
|
||||||
|
// tell if the current window is on the live event stream)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
events: [],
|
||||||
|
timelineLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._timelineWindow = new Matrix.TimelineWindow(
|
||||||
|
MatrixClientPeg.get(), this.state.room,
|
||||||
|
{windowLimit: TIMELINE_CAP});
|
||||||
|
|
||||||
|
return this._timelineWindow.load(eventId, INITIAL_SIZE).then(() => {
|
||||||
|
debuglog("RoomView: timeline loaded");
|
||||||
|
this._onTimelineUpdated(true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({
|
||||||
|
timelineLoading: false,
|
||||||
|
}, () => {
|
||||||
|
// initialise the scroll state of the message panel
|
||||||
|
if (!this.refs.messagePanel) {
|
||||||
|
// this shouldn't happen.
|
||||||
|
console.log("can't initialise scroll state because " +
|
||||||
|
"messagePanel didn't load");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (eventId) {
|
||||||
|
this.refs.messagePanel.scrollToToken(eventId, pixelOffset);
|
||||||
|
} else {
|
||||||
|
this.refs.messagePanel.scrollToBottom();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this._calculatePeekRules(this.state.room);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -162,7 +264,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onNewRoom);
|
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||||
|
@ -208,10 +309,6 @@ module.exports = React.createClass({
|
||||||
var callState;
|
var callState;
|
||||||
|
|
||||||
if (call) {
|
if (call) {
|
||||||
// Call state has changed so we may be loading video elements
|
|
||||||
// which will obscure the message log.
|
|
||||||
// scroll to bottom
|
|
||||||
this.scrollToBottom();
|
|
||||||
callState = call.call_state;
|
callState = call.call_state;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -248,52 +345,52 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// MatrixRoom still showing the messages from the old room?
|
componentWillReceiveProps: function(newProps) {
|
||||||
// Set the key to the room_id. Sadly you can no longer get at
|
if (newProps.roomId != this.props.roomId) {
|
||||||
// the key from inside the component, or we'd check this in code.
|
throw new Error("changing room on a RoomView is not supported");
|
||||||
/*componentWillReceiveProps: function(props) {
|
}
|
||||||
},*/
|
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
if (newProps.eventId != this.props.eventId) {
|
||||||
|
console.log("RoomView switching to eventId " + newProps.eventId +
|
||||||
|
" (was " + this.props.eventId + ")");
|
||||||
|
return this._initTimeline(newProps);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
// ignore anything that comes in whilst paginating: we get one
|
// ignore events for other rooms
|
||||||
// event for each new matrix event so this would cause a huge
|
if (room.roomId != this.props.roomId) return;
|
||||||
// number of UI updates. Just update the UI when the paginate
|
|
||||||
// call returns.
|
// ignore anything but real-time updates at the end of the room:
|
||||||
if (this.state.paginating) return;
|
// updates from pagination will happen when the paginate completes.
|
||||||
|
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||||
|
|
||||||
// no point handling anything while we're waiting for the join to finish:
|
// no point handling anything while we're waiting for the join to finish:
|
||||||
// we'll only be showing a spinner.
|
// we'll only be showing a spinner.
|
||||||
if (this.state.joining) return;
|
if (this.state.joining) return;
|
||||||
if (room.roomId != this.props.roomId) return;
|
|
||||||
|
|
||||||
var currentUnread = this.state.numUnreadMessages;
|
if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) {
|
||||||
if (!toStartOfTimeline &&
|
|
||||||
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
|
|
||||||
// update unread count when scrolled up
|
// update unread count when scrolled up
|
||||||
if (!this.state.searchResults && this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
|
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||||
currentUnread = 0;
|
// no change
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
currentUnread += 1;
|
this.setState((state, props) => {
|
||||||
}
|
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
room: MatrixClientPeg.get().getRoom(this.props.roomId),
|
|
||||||
numUnreadMessages: currentUnread
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onNewRoom: function(room) {
|
|
||||||
if (room.roomId == this.props.roomId) {
|
|
||||||
this.setState({
|
|
||||||
room: room
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._calculatePeekRules(room);
|
// tell the messagepanel to go paginate itself. This in turn will cause
|
||||||
|
// onMessageListFillRequest to be called, which will call
|
||||||
|
// _onTimelineUpdated, which will update the state with the new event -
|
||||||
|
// so there is no need update the state here.
|
||||||
|
//
|
||||||
|
if (this.refs.messagePanel) {
|
||||||
|
this.refs.messagePanel.checkFillState();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculatePeekRules: function(room) {
|
_calculatePeekRules: function(room) {
|
||||||
|
@ -355,14 +452,14 @@ module.exports = React.createClass({
|
||||||
// if the event after the one referenced in the read receipt if sent by us, do nothing since
|
// if the event after the one referenced in the read receipt if sent by us, do nothing since
|
||||||
// this is a temporary period before the synthesized receipt for our own message arrives
|
// this is a temporary period before the synthesized receipt for our own message arrives
|
||||||
var readMarkerGhostEventIndex;
|
var readMarkerGhostEventIndex;
|
||||||
for (var i = 0; i < room.timeline.length; ++i) {
|
for (var i = 0; i < this.state.events.length; ++i) {
|
||||||
if (room.timeline[i].getId() == readMarkerGhostEventId) {
|
if (this.state.events[i].getId() == readMarkerGhostEventId) {
|
||||||
readMarkerGhostEventIndex = i;
|
readMarkerGhostEventIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (readMarkerGhostEventIndex + 1 < room.timeline.length) {
|
if (readMarkerGhostEventIndex + 1 < this.state.events.length) {
|
||||||
var nextEvent = room.timeline[readMarkerGhostEventIndex + 1];
|
var nextEvent = this.state.events[readMarkerGhostEventIndex + 1];
|
||||||
if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
readMarkerGhostEventId = undefined;
|
readMarkerGhostEventId = undefined;
|
||||||
}
|
}
|
||||||
|
@ -372,6 +469,20 @@ module.exports = React.createClass({
|
||||||
readMarkerEventId: readMarkerEventId,
|
readMarkerEventId: readMarkerEventId,
|
||||||
readMarkerGhostEventId: readMarkerGhostEventId,
|
readMarkerGhostEventId: readMarkerGhostEventId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// if the scrollpanel is following the timeline, attempt to scroll
|
||||||
|
// it to bring the read message up to the middle of the panel. This
|
||||||
|
// will have no immediate effect (since we are already at the
|
||||||
|
// bottom), but will ensure that if there is no further user
|
||||||
|
// activity, but room activity continues, the read message will
|
||||||
|
// scroll up to the middle of the window, but no further.
|
||||||
|
//
|
||||||
|
// we do this here as well as in sendReadReceipt to deal with
|
||||||
|
// people using two clients at once.
|
||||||
|
if (this.refs.messagePanel && this.state.atEndOfLiveTimeline) {
|
||||||
|
this.refs.messagePanel.scrollToToken(readMarkerEventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -494,7 +605,6 @@ module.exports = React.createClass({
|
||||||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
||||||
this.refs.messagePanel.initialised = true;
|
this.refs.messagePanel.initialised = true;
|
||||||
|
|
||||||
this.scrollToBottom();
|
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
|
|
||||||
this.updateTint();
|
this.updateTint();
|
||||||
|
@ -511,17 +621,21 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_paginateCompleted: function() {
|
_onTimelineUpdated: function(gotResults) {
|
||||||
debuglog("paginate complete");
|
// we might have switched rooms since the load started - just bin
|
||||||
|
|
||||||
// we might have switched rooms since the paginate started - just bin
|
|
||||||
// the results if so.
|
// the results if so.
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
room: MatrixClientPeg.get().getRoom(this.props.roomId),
|
|
||||||
paginating: false,
|
paginating: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (gotResults) {
|
||||||
|
this.setState({
|
||||||
|
events: this._timelineWindow.getEvents(),
|
||||||
|
canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
|
@ -541,30 +655,19 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// set off a pagination request.
|
// set off a pagination request.
|
||||||
onMessageListFillRequest: function(backwards) {
|
onMessageListFillRequest: function(backwards) {
|
||||||
if (!backwards)
|
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
|
if(!this._timelineWindow.canPaginate(dir)) {
|
||||||
|
debuglog("RoomView: can't paginate at this time; backwards:"+backwards);
|
||||||
return q(false);
|
return q(false);
|
||||||
|
|
||||||
// Either wind back the message cap (if there are enough events in the
|
|
||||||
// timeline to do so), or fire off a pagination request.
|
|
||||||
|
|
||||||
if (this.state.messageCap < this.state.room.timeline.length) {
|
|
||||||
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
|
||||||
debuglog("winding back message cap to", cap);
|
|
||||||
this.setState({messageCap: cap});
|
|
||||||
return q(true);
|
|
||||||
} else if(this.state.room.oldState.paginationToken) {
|
|
||||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
|
||||||
debuglog("starting paginate to cap", cap);
|
|
||||||
this.setState({messageCap: cap, paginating: true});
|
|
||||||
return MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).
|
|
||||||
finally(this._paginateCompleted).then(true);
|
|
||||||
}
|
}
|
||||||
},
|
this.setState({paginating: true});
|
||||||
|
|
||||||
// return true if there's more messages in the backlog which we aren't displaying
|
debuglog("RoomView: Initiating paginate; backwards:"+backwards);
|
||||||
_canPaginate: function() {
|
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => {
|
||||||
return (this.state.messageCap < this.state.room.timeline.length) ||
|
debuglog("RoomView: paginate complete backwards:"+backwards+"; success:"+r);
|
||||||
this.state.room.oldState.paginationToken;
|
this._onTimelineUpdated(r);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onResendAllClick: function() {
|
onResendAllClick: function() {
|
||||||
|
@ -606,17 +709,18 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function(ev) {
|
onMessageListScroll: function(ev) {
|
||||||
if (this.refs.messagePanel.isAtBottom()) {
|
if (this.refs.messagePanel.isAtBottom() &&
|
||||||
|
!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
if (this.state.numUnreadMessages != 0) {
|
if (this.state.numUnreadMessages != 0) {
|
||||||
this.setState({ numUnreadMessages: 0 });
|
this.setState({ numUnreadMessages: 0 });
|
||||||
}
|
}
|
||||||
if (!this.state.atBottom) {
|
if (!this.state.atEndOfLiveTimeline) {
|
||||||
this.setState({ atBottom: true });
|
this.setState({ atEndOfLiveTimeline: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.state.atBottom) {
|
if (this.state.atEndOfLiveTimeline) {
|
||||||
this.setState({ atBottom: false });
|
this.setState({ atEndOfLiveTimeline: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -836,11 +940,10 @@ module.exports = React.createClass({
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
|
||||||
var prevEvent = null; // the last event we showed
|
var prevEvent = null; // the last event we showed
|
||||||
var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap);
|
|
||||||
var readMarkerIndex;
|
|
||||||
var ghostIndex;
|
var ghostIndex;
|
||||||
for (var i = startIdx; i < this.state.room.timeline.length; i++) {
|
var readMarkerIndex;
|
||||||
var mxEv = this.state.room.timeline[i];
|
for (var i = 0; i < this.state.events.length; i++) {
|
||||||
|
var mxEv = this.state.events[i];
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -886,7 +989,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// do we need a date separator since the last event?
|
// do we need a date separator since the last event?
|
||||||
var ts1 = mxEv.getTs();
|
var ts1 = mxEv.getTs();
|
||||||
if ((prevEvent == null && !this._canPaginate()) ||
|
if ((prevEvent == null && !this.state.canBackPaginate) ||
|
||||||
(prevEvent != null &&
|
(prevEvent != null &&
|
||||||
new Date(prevEvent.getTs()).toDateString() !== new Date(ts1).toDateString())) {
|
new Date(prevEvent.getTs()).toDateString() !== new Date(ts1).toDateString())) {
|
||||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
||||||
|
@ -895,15 +998,17 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
var last = false;
|
var last = false;
|
||||||
if (i == this.state.room.timeline.length - 1) {
|
if (i == this.state.events.length - 1) {
|
||||||
// XXX: we might not show a tile for the last event.
|
// XXX: we might not show a tile for the last event.
|
||||||
last = true;
|
last = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventId = mxEv.getId();
|
var eventId = mxEv.getId();
|
||||||
|
var highlight = (eventId == this.props.highlightedEventId);
|
||||||
ret.push(
|
ret.push(
|
||||||
<li key={eventId} ref={this._collectEventNode.bind(this, eventId)} data-scroll-token={eventId}>
|
<li key={eventId} ref={this._collectEventNode.bind(this, eventId)} data-scroll-token={eventId}>
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation} last={last}/>
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
|
last={last} isSelectedEvent={highlight}/>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -922,7 +1027,7 @@ module.exports = React.createClass({
|
||||||
var hr;
|
var hr;
|
||||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
||||||
Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
|
Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
|
||||||
self.setState({readMarkerGhostEventId: undefined});
|
if (!self.unmounted) self.setState({readMarkerGhostEventId: undefined});
|
||||||
}});
|
}});
|
||||||
}} />);
|
}} />);
|
||||||
ret.splice(ghostIndex, 0, (
|
ret.splice(ghostIndex, 0, (
|
||||||
|
@ -1178,8 +1283,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_indexForEventId(evId) {
|
_indexForEventId(evId) {
|
||||||
for (var i = 0; i < this.state.room.timeline.length; ++i) {
|
for (var i = 0; i < this.state.events.length; ++i) {
|
||||||
if (evId == this.state.room.timeline[i].getId()) {
|
if (evId == this.state.events[i].getId()) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1188,13 +1293,33 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
sendReadReceipt: function() {
|
sendReadReceipt: function() {
|
||||||
if (!this.state.room) return;
|
if (!this.state.room) return;
|
||||||
|
if (!this.refs.messagePanel) return;
|
||||||
|
|
||||||
var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
||||||
|
|
||||||
|
// We want to avoid sending out read receipts when we are looking at
|
||||||
|
// events in the past which are before the latest RR.
|
||||||
|
//
|
||||||
|
// For now, let's apply a heuristic: if (a) the event corresponding to
|
||||||
|
// the latest RR (either from the server, or sent by ourselves) doesn't
|
||||||
|
// appear in our timeline, and (b) we could forward-paginate the event
|
||||||
|
// timeline, then don't send any more RRs.
|
||||||
|
//
|
||||||
|
// This isn't watertight, as we could be looking at a section of
|
||||||
|
// timeline which is *after* the latest RR (so we should actually send
|
||||||
|
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
||||||
|
// the user eventually hits the live timeline.
|
||||||
|
//
|
||||||
|
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
|
||||||
|
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
||||||
if (lastReadEventIndex === null) return;
|
if (lastReadEventIndex === null) return;
|
||||||
|
|
||||||
var lastReadEvent = this.state.room.timeline[lastReadEventIndex];
|
var lastReadEvent = this.state.events[lastReadEventIndex];
|
||||||
|
|
||||||
// we also remember the last read receipt we sent to avoid spamming the same one at the server repeatedly
|
// we also remember the last read receipt we sent to avoid spamming the same one at the server repeatedly
|
||||||
if (lastReadEventIndex > currentReadUpToEventIndex && this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
if (lastReadEventIndex > currentReadUpToEventIndex && this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
||||||
|
@ -1203,6 +1328,19 @@ module.exports = React.createClass({
|
||||||
// it failed, so allow retries next time the user is active
|
// it failed, so allow retries next time the user is active
|
||||||
this.last_rr_sent_event_id = undefined;
|
this.last_rr_sent_event_id = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// if the scrollpanel is following the timeline, attempt to scroll
|
||||||
|
// it to bring the read message up to the middle of the panel. This
|
||||||
|
// will have no immediate effect (since we are already at the
|
||||||
|
// bottom), but will ensure that if there is no further user
|
||||||
|
// activity, but room activity continues, the read message will
|
||||||
|
// scroll up to the middle of the window, but no further.
|
||||||
|
//
|
||||||
|
// we do this here as well as in onRoomReceipt to cater for guest users
|
||||||
|
// (which do not send out read receipts).
|
||||||
|
if (this.state.atEndOfLiveTimeline) {
|
||||||
|
this.refs.messagePanel.scrollToToken(lastReadEvent.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1213,8 +1351,8 @@ module.exports = React.createClass({
|
||||||
if (messageWrapper === undefined) return null;
|
if (messageWrapper === undefined) return null;
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||||
|
|
||||||
for (var i = this.state.room.timeline.length-1; i >= 0; --i) {
|
for (var i = this.state.events.length-1; i >= 0; --i) {
|
||||||
var ev = this.state.room.timeline[i];
|
var ev = this.state.events[i];
|
||||||
|
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1327,78 +1465,58 @@ module.exports = React.createClass({
|
||||||
return this.state.numUnreadMessages + " new message" + (this.state.numUnreadMessages > 1 ? "s" : "");
|
return this.state.numUnreadMessages + " new message" + (this.state.numUnreadMessages > 1 ? "s" : "");
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom: function() {
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
var messagePanel = this.refs.messagePanel;
|
jumpToLiveTimeline: function() {
|
||||||
if (!messagePanel) return;
|
// if we can't forward-paginate the existing timeline, then there
|
||||||
messagePanel.scrollToBottom();
|
// is no point reloading it - just jump straight to the bottom.
|
||||||
},
|
|
||||||
|
|
||||||
// scroll the event view to put the given event at the bottom.
|
|
||||||
//
|
//
|
||||||
// pixel_offset gives the number of pixels between the bottom of the event
|
// Otherwise, reload the timeline rather than trying to paginate
|
||||||
// and the bottom of the container.
|
// through all of space-time.
|
||||||
scrollToEvent: function(eventId, pixelOffset) {
|
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
var messagePanel = this.refs.messagePanel;
|
this._loadTimeline();
|
||||||
if (!messagePanel) return;
|
} else {
|
||||||
|
if (this.refs.messagePanel) {
|
||||||
var idx = this._indexForEventId(eventId);
|
this.refs.messagePanel.scrollToBottom();
|
||||||
if (idx === null) {
|
|
||||||
// we don't seem to have this event in our timeline. Presumably
|
|
||||||
// it's fallen out of scrollback. We ought to backfill until we
|
|
||||||
// find it, but we'd have to be careful we didn't backfill forever
|
|
||||||
// looking for a non-existent event.
|
|
||||||
//
|
|
||||||
// for now, just scroll to the top of the buffer.
|
|
||||||
console.log("Refusing to scroll to unknown event "+eventId);
|
|
||||||
messagePanel.scrollToTop();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we might need to roll back the messagecap (to generate tiles for
|
|
||||||
// older messages). This just means telling getEventTiles to create
|
|
||||||
// tiles for events we already have in our timeline (we already know
|
|
||||||
// the event in question is in our timeline, so we shouldn't need to
|
|
||||||
// backfill).
|
|
||||||
//
|
|
||||||
// we actually wind back slightly further than the event in question,
|
|
||||||
// because we want the event to be at the *bottom* of the container.
|
|
||||||
// Don't roll it back past the timeline we have, though.
|
|
||||||
var minCap = this.state.room.timeline.length - Math.min(idx - INITIAL_SIZE, 0);
|
|
||||||
if (minCap > this.state.messageCap) {
|
|
||||||
this.setState({messageCap: minCap});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the scrollTokens on our DOM nodes are the event IDs, so we can pass
|
|
||||||
// eventId directly into _scrollToToken.
|
|
||||||
messagePanel.scrollToToken(eventId, pixelOffset);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// get the current scroll position of the room, so that it can be
|
// get the current scroll position of the room, so that it can be
|
||||||
// restored when we switch back to it
|
// restored when we switch back to it.
|
||||||
|
//
|
||||||
|
// This returns an object with the following properties:
|
||||||
|
//
|
||||||
|
// focussedEvent: the ID of the 'focussed' event. Typically this is the
|
||||||
|
// last event fully visible in the viewport, though if we have done
|
||||||
|
// an explicit scroll to an explicit event, it will be that event.
|
||||||
|
//
|
||||||
|
// pixelOffset: the number of pixels the window is scrolled down from
|
||||||
|
// the focussedEvent.
|
||||||
|
//
|
||||||
|
// If there are no visible events, returns null.
|
||||||
|
//
|
||||||
getScrollState: function() {
|
getScrollState: function() {
|
||||||
var messagePanel = this.refs.messagePanel;
|
var messagePanel = this.refs.messagePanel;
|
||||||
if (!messagePanel) return null;
|
if (!messagePanel) return null;
|
||||||
|
|
||||||
return messagePanel.getScrollState();
|
var scrollState = messagePanel.getScrollState();
|
||||||
},
|
|
||||||
|
|
||||||
restoreScrollState: function(scrollState) {
|
if (scrollState.stuckAtBottom) {
|
||||||
var messagePanel = this.refs.messagePanel;
|
// we don't really expect to be in this state, but it will
|
||||||
if (!messagePanel) return null;
|
// occasionally happen when no scroll state has been set on the
|
||||||
|
// messagePanel (ie, we didn't have an initial event (so it's
|
||||||
if(scrollState.atBottom) {
|
// probably a new room), there has been no user-initiated scroll, and
|
||||||
// we were at the bottom before. Ideally we'd scroll to the
|
// no read-receipts have arrived to update the scroll position).
|
||||||
// 'read-up-to' mark here.
|
//
|
||||||
messagePanel.scrollToBottom();
|
// Return null, which will cause us to scroll to last unread on
|
||||||
|
// reload.
|
||||||
} else if (scrollState.lastDisplayedScrollToken) {
|
return null;
|
||||||
// we might need to backfill, so we call scrollToEvent rather than
|
|
||||||
// scrollToToken here. The scrollTokens on our DOM nodes are the
|
|
||||||
// event IDs, so lastDisplayedScrollToken will be the event ID we need,
|
|
||||||
// and we can pass it directly into scrollToEvent.
|
|
||||||
this.scrollToEvent(scrollState.lastDisplayedScrollToken,
|
|
||||||
scrollState.pixelOffset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
focussedEvent: scrollState.trackedScrollToken,
|
||||||
|
pixelOffset: scrollState.pixelOffset,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onResize: function(e) {
|
onResize: function(e) {
|
||||||
|
@ -1492,11 +1610,11 @@ module.exports = React.createClass({
|
||||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
||||||
|
|
||||||
if (!this.state.room) {
|
|
||||||
if (this.props.roomId) {
|
|
||||||
if (this.props.autoPeek && !this.state.autoPeekDone) {
|
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
if (!this._timelineWindow) {
|
||||||
|
if (this.props.roomId) {
|
||||||
|
if (this.state.timelineLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
@ -1529,7 +1647,6 @@ module.exports = React.createClass({
|
||||||
var myMember = this.state.room.getMember(myUserId);
|
var myMember = this.state.room.getMember(myUserId);
|
||||||
if (myMember && myMember.membership == 'invite') {
|
if (myMember && myMember.membership == 'invite') {
|
||||||
if (this.state.joining || this.state.rejecting) {
|
if (this.state.joining || this.state.rejecting) {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
@ -1640,7 +1757,7 @@ module.exports = React.createClass({
|
||||||
// set when you've scrolled up
|
// set when you've scrolled up
|
||||||
else if (unreadMsgs) {
|
else if (unreadMsgs) {
|
||||||
statusBar = (
|
statusBar = (
|
||||||
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.scrollToBottom }>
|
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.jumpToLiveTimeline }>
|
||||||
<img src="img/newmessages.svg" width="24" height="24" alt=""/>
|
<img src="img/newmessages.svg" width="24" height="24" alt=""/>
|
||||||
{unreadMsgs}
|
{unreadMsgs}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1654,9 +1771,9 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else if (!this.state.atBottom) {
|
else if (!this.state.atEndOfLiveTimeline) {
|
||||||
statusBar = (
|
statusBar = (
|
||||||
<div className="mx_RoomView_scrollToBottomBar" onClick={ this.scrollToBottom }>
|
<div className="mx_RoomView_scrollToBottomBar" onClick={ this.jumpToLiveTimeline }>
|
||||||
<img src="img/scrolldown.svg" width="24" height="24" alt="Scroll to bottom of page" title="Scroll to bottom of page"/>
|
<img src="img/scrolldown.svg" width="24" height="24" alt="Scroll to bottom of page" title="Scroll to bottom of page"/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1668,7 +1785,6 @@ module.exports = React.createClass({
|
||||||
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
|
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
|
||||||
}
|
}
|
||||||
else if (this.state.uploadingRoomSettings) {
|
else if (this.state.uploadingRoomSettings) {
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
aux = <Loader/>;
|
aux = <Loader/>;
|
||||||
}
|
}
|
||||||
else if (this.state.searching) {
|
else if (this.state.searching) {
|
||||||
|
@ -1794,15 +1910,40 @@ module.exports = React.createClass({
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var messagePanel = (
|
var messagePanel;
|
||||||
|
|
||||||
|
// just show a spinner while the timeline loads.
|
||||||
|
//
|
||||||
|
// put it in a div of the right class (mx_RoomView_messagePanel) so
|
||||||
|
// that the order in the roomview flexbox is correct, and
|
||||||
|
// mx_RoomView_messageListWrapper to position the inner div in the
|
||||||
|
// right place.
|
||||||
|
//
|
||||||
|
// Note that the click-on-search-result functionality relies on the
|
||||||
|
// fact that the messagePanel is hidden while the timeline reloads,
|
||||||
|
// but that the RoomHeader (complete with search term) continues to
|
||||||
|
// exist.
|
||||||
|
if (this.state.timelineLoading) {
|
||||||
|
messagePanel = (
|
||||||
|
<div className="mx_RoomView_messagePanel mx_RoomView_messageListWrapper">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// it's important that stickyBottom = false on this, otherwise if somebody hits the
|
||||||
|
// bottom of the loaded events when viewing historical messages, we get stuck in a
|
||||||
|
// loop of paginating our way through the entire history of the room.
|
||||||
|
messagePanel = (
|
||||||
<ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel"
|
<ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel"
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
onFillRequest={ this.onMessageListFillRequest }
|
||||||
style={ hideMessagePanel ? { display: 'none' } : {} } >
|
style={ hideMessagePanel ? { display: 'none' } : {} }
|
||||||
|
stickyBottom={ false }>
|
||||||
<li className={scrollheader_classes}></li>
|
<li className={scrollheader_classes}></li>
|
||||||
{this.getEventTiles()}
|
{this.getEventTiles()}
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||||
|
|
|
@ -37,14 +37,32 @@ if (DEBUG_SCROLL) {
|
||||||
* It also provides a hook which allows parents to provide more list elements
|
* It also provides a hook which allows parents to provide more list elements
|
||||||
* when we get close to the start or end of the list.
|
* when we get close to the start or end of the list.
|
||||||
*
|
*
|
||||||
* We don't save the absolute scroll offset, because that would be affected by
|
|
||||||
* window width, zoom level, amount of scrollback, etc. Instead we save an
|
|
||||||
* identifier for the last fully-visible message, and the number of pixels the
|
|
||||||
* window was scrolled below it - which is hopefully be near enough.
|
|
||||||
*
|
|
||||||
* Each child element should have a 'data-scroll-token'. This token is used to
|
* Each child element should have a 'data-scroll-token'. This token is used to
|
||||||
* serialise the scroll state, and returned as the 'lastDisplayedScrollToken'
|
* serialise the scroll state, and returned as the 'trackedScrollToken'
|
||||||
* attribute by getScrollState().
|
* attribute by getScrollState().
|
||||||
|
*
|
||||||
|
* Some notes about the implementation:
|
||||||
|
*
|
||||||
|
* The saved 'scrollState' can exist in one of two states:
|
||||||
|
*
|
||||||
|
* - stuckAtBottom: (the default, and restored by resetScrollState): the
|
||||||
|
* viewport is scrolled down as far as it can be. When the children are
|
||||||
|
* updated, the scroll position will be updated to ensure it is still at
|
||||||
|
* the bottom.
|
||||||
|
*
|
||||||
|
* - fixed, in which the viewport is conceptually tied at a specific scroll
|
||||||
|
* offset. We don't save the absolute scroll offset, because that would be
|
||||||
|
* affected by window width, zoom level, amount of scrollback, etc. Instead
|
||||||
|
* we save an identifier for the last fully-visible message, and the number
|
||||||
|
* of pixels the window was scrolled below it - which is hopefully be near
|
||||||
|
* enough.
|
||||||
|
*
|
||||||
|
* The 'stickyBottom' property controls the behaviour when we reach the bottom
|
||||||
|
* of the window (either through a user-initiated scroll, or by calling
|
||||||
|
* scrollToBottom). If stickyBottom is enabled, the scrollState will enter
|
||||||
|
* 'stuckAtBottom' state - ensuring that new additions cause the window to
|
||||||
|
* scroll down further. If stickyBottom is disabled, we just save the scroll
|
||||||
|
* offset as normal.
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'ScrollPanel',
|
displayName: 'ScrollPanel',
|
||||||
|
@ -145,8 +163,15 @@ module.exports = React.createClass({
|
||||||
this.recentEventScroll = undefined;
|
this.recentEventScroll = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollState = this._calculateScrollState();
|
// If there weren't enough children to fill the viewport, the scroll we
|
||||||
debuglog("Saved scroll state", this.scrollState);
|
// got might be different to the scroll we wanted; we don't want to
|
||||||
|
// forget what we wanted, so don't overwrite the saved state unless
|
||||||
|
// this appears to be a user-initiated scroll.
|
||||||
|
if (sn.scrollTop != this._lastSetScroll) {
|
||||||
|
this._saveScrollState();
|
||||||
|
} else {
|
||||||
|
debuglog("Ignoring scroll echo");
|
||||||
|
}
|
||||||
|
|
||||||
this.props.onScroll(ev);
|
this.props.onScroll(ev);
|
||||||
|
|
||||||
|
@ -155,13 +180,19 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// return true if the content is fully scrolled down right now; else false.
|
// return true if the content is fully scrolled down right now; else false.
|
||||||
//
|
//
|
||||||
// Note that if the content hasn't yet been fully populated, this may
|
// note that this is independent of the 'stuckAtBottom' state - it is simply
|
||||||
// spuriously return true even if the user wanted to be looking at earlier
|
// about whether the the content is scrolled down right now, irrespective of
|
||||||
// content. So don't call it in render() cycles.
|
// whether it will stay that way when the children update.
|
||||||
isAtBottom: function() {
|
isAtBottom: function() {
|
||||||
var sn = this._getScrollNode();
|
var sn = this._getScrollNode();
|
||||||
// + 2 here to avoid fractional pixel rounding errors
|
|
||||||
return sn.scrollHeight - sn.scrollTop <= sn.clientHeight + 2;
|
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
|
||||||
|
// understanding of the box model, wherein the scrollNode ends up 2
|
||||||
|
// pixels higher than the available space, even when there are less
|
||||||
|
// than a screenful of messages. + 3 is a fudge factor to pretend
|
||||||
|
// that we're at the bottom when we're still a few pixels off.
|
||||||
|
|
||||||
|
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
|
||||||
},
|
},
|
||||||
|
|
||||||
// check the scroll state and send out backfill requests if necessary.
|
// check the scroll state and send out backfill requests if necessary.
|
||||||
|
@ -230,9 +261,9 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
q.finally(fillPromise, () => {
|
q.finally(fillPromise, () => {
|
||||||
debuglog("ScrollPanel: "+dir+" fill complete");
|
|
||||||
this._pendingFillRequests[dir] = false;
|
this._pendingFillRequests[dir] = false;
|
||||||
}).then((hasMoreResults) => {
|
}).then((hasMoreResults) => {
|
||||||
|
debuglog("ScrollPanel: "+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||||
if (hasMoreResults) {
|
if (hasMoreResults) {
|
||||||
// further pagination requests have been disabled until now, so
|
// further pagination requests have been disabled until now, so
|
||||||
// it's time to check the fill state again in case the pagination
|
// it's time to check the fill state again in case the pagination
|
||||||
|
@ -242,42 +273,83 @@ module.exports = React.createClass({
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
// get the current scroll position of the room, so that it can be
|
/* get the current scroll state. This returns an object with the following
|
||||||
// restored later
|
* properties:
|
||||||
|
*
|
||||||
|
* boolean stuckAtBottom: true if we are tracking the bottom of the
|
||||||
|
* scroll. false if we are tracking a particular child.
|
||||||
|
*
|
||||||
|
* string trackedScrollToken: undefined if stuckAtBottom is true; if it is
|
||||||
|
* false, the data-scroll-token of the child which we are tracking.
|
||||||
|
*
|
||||||
|
* number pixelOffset: undefined if stuckAtBottom is true; if it is false,
|
||||||
|
* the number of pixels the bottom of the tracked child is above the
|
||||||
|
* bottom of the scroll panel.
|
||||||
|
*/
|
||||||
getScrollState: function() {
|
getScrollState: function() {
|
||||||
return this.scrollState;
|
return this.scrollState;
|
||||||
},
|
},
|
||||||
|
|
||||||
/* reset the saved scroll state.
|
/* reset the saved scroll state.
|
||||||
*
|
|
||||||
* This will cause the scroll to be reinitialised on the next update of the
|
|
||||||
* child list.
|
|
||||||
*
|
*
|
||||||
* This is useful if the list is being replaced, and you don't want to
|
* This is useful if the list is being replaced, and you don't want to
|
||||||
* preserve scroll even if new children happen to have the same scroll
|
* preserve scroll even if new children happen to have the same scroll
|
||||||
* tokens as old ones.
|
* tokens as old ones.
|
||||||
|
*
|
||||||
|
* This will cause the viewport to be scrolled down to the bottom on the
|
||||||
|
* next update of the child list. This is different to scrollToBottom(),
|
||||||
|
* which would save the current bottom-most child as the active one (so is
|
||||||
|
* no use if no children exist yet, or if you are about to replace the
|
||||||
|
* child list.)
|
||||||
*/
|
*/
|
||||||
resetScrollState: function() {
|
resetScrollState: function() {
|
||||||
this.scrollState = null;
|
this.scrollState = {stuckAtBottom: true};
|
||||||
},
|
|
||||||
|
|
||||||
scrollToTop: function() {
|
|
||||||
this._getScrollNode().scrollTop = 0;
|
|
||||||
debuglog("Scrolled to top");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom: function() {
|
scrollToBottom: function() {
|
||||||
|
// the easiest way to make sure that the scroll state is correctly
|
||||||
|
// saved is to do the scroll, then save the updated state. (Calculating
|
||||||
|
// it ourselves is hard, and we can't rely on an onScroll callback
|
||||||
|
// happening, since there may be no user-visible change here).
|
||||||
var scrollNode = this._getScrollNode();
|
var scrollNode = this._getScrollNode();
|
||||||
|
|
||||||
scrollNode.scrollTop = scrollNode.scrollHeight;
|
scrollNode.scrollTop = scrollNode.scrollHeight;
|
||||||
debuglog("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
debuglog("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
||||||
|
this._lastSetScroll = scrollNode.scrollTop;
|
||||||
|
|
||||||
|
this._saveScrollState();
|
||||||
},
|
},
|
||||||
|
|
||||||
// scroll the message list to the node with the given scrollToken. See
|
// pixelOffset gives the number of pixels between the bottom of the node
|
||||||
// notes in _calculateScrollState on how this works.
|
// and the bottom of the container. If undefined, it will put the node
|
||||||
//
|
// in the middle of the container.
|
||||||
// pixel_offset gives the number of pixels between the bottom of the node
|
|
||||||
// and the bottom of the container.
|
|
||||||
scrollToToken: function(scrollToken, pixelOffset) {
|
scrollToToken: function(scrollToken, pixelOffset) {
|
||||||
|
var scrollNode = this._getScrollNode();
|
||||||
|
|
||||||
|
// default to the middle
|
||||||
|
if (pixelOffset === undefined) {
|
||||||
|
pixelOffset = scrollNode.clientHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the desired scroll state. It's important we do this here rather
|
||||||
|
// than as a result of the scroll event, because (a) we might not *get*
|
||||||
|
// a scroll event, and (b) it might not currently be possible to set
|
||||||
|
// the requested scroll state (eg, because we hit the end of the
|
||||||
|
// timeline and need to do more pagination); we want to save the
|
||||||
|
// *desired* scroll state rather than what we end up achieving.
|
||||||
|
this.scrollState = {
|
||||||
|
stuckAtBottom: false,
|
||||||
|
trackedScrollToken: scrollToken,
|
||||||
|
pixelOffset: pixelOffset
|
||||||
|
};
|
||||||
|
|
||||||
|
// ... then make it so.
|
||||||
|
this._restoreSavedScrollState();
|
||||||
|
},
|
||||||
|
|
||||||
|
// set the scrollTop attribute appropriately to position the given child at the
|
||||||
|
// given offset in the window. A helper for _restoreSavedScrollState.
|
||||||
|
_scrollToToken: function(scrollToken, pixelOffset) {
|
||||||
/* find the dom node with the right scrolltoken */
|
/* find the dom node with the right scrolltoken */
|
||||||
var node;
|
var node;
|
||||||
var messages = this.refs.itemlist.children;
|
var messages = this.refs.itemlist.children;
|
||||||
|
@ -291,7 +363,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
console.error("No node with scrollToken '"+scrollToken+"'");
|
debuglog("ScrollPanel: No node with scrollToken '"+scrollToken+"'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,15 +384,12 @@ module.exports = React.createClass({
|
||||||
debuglog("recentEventScroll now "+this.recentEventScroll);
|
debuglog("recentEventScroll now "+this.recentEventScroll);
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculateScrollState: function() {
|
_saveScrollState: function() {
|
||||||
// Our scroll implementation is agnostic of the precise contents of the
|
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||||
// message list (since it needs to work with both search results and
|
this.scrollState = { stuckAtBottom: true };
|
||||||
// timelines). 'refs.messageList' is expected to be a DOM node with a
|
debuglog("Saved scroll state", this.scrollState);
|
||||||
// number of children, each of which may have a 'data-scroll-token'
|
return;
|
||||||
// attribute. It is this token which is stored as the
|
}
|
||||||
// 'lastDisplayedScrollToken'.
|
|
||||||
|
|
||||||
var atBottom = this.isAtBottom();
|
|
||||||
|
|
||||||
var itemlist = this.refs.itemlist;
|
var itemlist = this.refs.itemlist;
|
||||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||||
|
@ -332,28 +401,34 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
var boundingRect = node.getBoundingClientRect();
|
||||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||||
return {
|
this.scrollState = {
|
||||||
atBottom: atBottom,
|
stuckAtBottom: false,
|
||||||
lastDisplayedScrollToken: node.dataset.scrollToken,
|
trackedScrollToken: node.dataset.scrollToken,
|
||||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||||
}
|
}
|
||||||
|
debuglog("Saved scroll state", this.scrollState);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// apparently the entire timeline is below the viewport. Give up.
|
debuglog("Unable to save scroll state: found no children in the viewport");
|
||||||
return { atBottom: true };
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreSavedScrollState: function() {
|
_restoreSavedScrollState: function() {
|
||||||
var scrollState = this.scrollState;
|
var scrollState = this.scrollState;
|
||||||
if (!scrollState || (this.props.stickyBottom && scrollState.atBottom)) {
|
var scrollNode = this._getScrollNode();
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (scrollState.lastDisplayedScrollToken) {
|
if (scrollState.stuckAtBottom) {
|
||||||
this.scrollToToken(scrollState.lastDisplayedScrollToken,
|
scrollNode.scrollTop = scrollNode.scrollHeight;
|
||||||
|
debuglog("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
||||||
|
} else if (scrollState.trackedScrollToken) {
|
||||||
|
this._scrollToToken(scrollState.trackedScrollToken,
|
||||||
scrollState.pixelOffset);
|
scrollState.pixelOffset);
|
||||||
}
|
}
|
||||||
|
this._lastSetScroll = scrollNode.scrollTop;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/* get the DOM node which has the scrollTop property we care about for our
|
/* get the DOM node which has the scrollTop property we care about for our
|
||||||
* message panel.
|
* message panel.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -58,7 +58,7 @@ module.exports = React.createClass({
|
||||||
onHomeserverChanged: function(ev) {
|
onHomeserverChanged: function(ev) {
|
||||||
this.setState({hs_url: ev.target.value}, function() {
|
this.setState({hs_url: ev.target.value}, function() {
|
||||||
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||||
this.props.onHsUrlChanged(this.state.hs_url);
|
this.props.onHsUrlChanged(this.state.hs_url.replace(/\/$/, ""));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -66,7 +66,7 @@ module.exports = React.createClass({
|
||||||
onIdentityServerChanged: function(ev) {
|
onIdentityServerChanged: function(ev) {
|
||||||
this.setState({is_url: ev.target.value}, function() {
|
this.setState({is_url: ev.target.value}, function() {
|
||||||
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
||||||
this.props.onIsUrlChanged(this.state.is_url);
|
this.props.onIsUrlChanged(this.state.is_url.replace(/\/$/, ""));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -98,6 +98,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
/* a function to be called when the highlight is clicked */
|
/* a function to be called when the highlight is clicked */
|
||||||
onHighlightClick: React.PropTypes.func,
|
onHighlightClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
/* is this the focussed event */
|
||||||
|
isSelectedEvent: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -273,6 +276,7 @@ module.exports = React.createClass({
|
||||||
) !== -1,
|
) !== -1,
|
||||||
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent',
|
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent',
|
||||||
mx_EventTile_highlight: this.shouldHighlight(),
|
mx_EventTile_highlight: this.shouldHighlight(),
|
||||||
|
mx_EventTile_selected: this.props.isSelectedEvent,
|
||||||
mx_EventTile_continuation: this.props.continuation,
|
mx_EventTile_continuation: this.props.continuation,
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
|
|
Loading…
Reference in a new issue