Control currently viewied event via RoomViewStore
Fix for https://github.com/vector-im/riot-web/issues/4224 Due to the way `MatrixChat` does a state update when the `view_room` dispatch fires and a second update when `RoomViewStore` sends an update, the current event ID and room ID were becoming out of sync. The solution devised was to have the event ID managed by the `RoomViewStore` itself and do any defaulting there (for when we revisit a room that we saved scroll state for previously). This required a few changes: - The addition of `update_scroll_state` in `RoomViewStore` allows the `RoomView` to save scroll state for a room before swapping to another one. Previously the caching of scroll state was done in `RoomView`. - The `view_room` dispatch now accepts an `event_id`, which dictates which event is supposed to be scrolled to in the `MessagePanel` when a new room is viewed. It also accepts `event_offset`, but currently, this isn't passed in by a dispatch in the app, but it is clobbered when loading the default position when an `event_id` isn't specified. Finally, `highlighted` was added to distinguish whether the initial event being scrolled to is also highlighted. This flag is also used by `viewRoom` in `MatrixChat` in order to decide whether to `notifyNewScreen` with the specified `event_id`.
This commit is contained in:
parent
ea02d8841c
commit
d3cf78ff5a
5 changed files with 109 additions and 78 deletions
|
@ -223,10 +223,8 @@ export default React.createClass({
|
||||||
ref='roomView'
|
ref='roomView'
|
||||||
autoJoin={this.props.autoJoin}
|
autoJoin={this.props.autoJoin}
|
||||||
onRegistered={this.props.onRegistered}
|
onRegistered={this.props.onRegistered}
|
||||||
eventId={this.props.initialEventId}
|
|
||||||
thirdPartyInvite={this.props.thirdPartyInvite}
|
thirdPartyInvite={this.props.thirdPartyInvite}
|
||||||
oobData={this.props.roomOobData}
|
oobData={this.props.roomOobData}
|
||||||
highlightedEventId={this.props.highlightedEventId}
|
|
||||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||||
key={this.props.currentRoomId || 'roomview'}
|
key={this.props.currentRoomId || 'roomview'}
|
||||||
opacity={this.props.middleOpacity}
|
opacity={this.props.middleOpacity}
|
||||||
|
|
|
@ -618,9 +618,6 @@ module.exports = React.createClass({
|
||||||
this.focusComposer = true;
|
this.focusComposer = true;
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
initialEventId: roomInfo.event_id,
|
|
||||||
highlightedEventId: roomInfo.event_id,
|
|
||||||
initialEventPixelOffset: undefined,
|
|
||||||
page_type: PageTypes.RoomView,
|
page_type: PageTypes.RoomView,
|
||||||
thirdPartyInvite: roomInfo.third_party_invite,
|
thirdPartyInvite: roomInfo.third_party_invite,
|
||||||
roomOobData: roomInfo.oob_data,
|
roomOobData: roomInfo.oob_data,
|
||||||
|
@ -632,18 +629,6 @@ module.exports = React.createClass({
|
||||||
newState.currentRoomId = roomInfo.room_id;
|
newState.currentRoomId = roomInfo.room_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we aren't given an explicit event id, look for one in the
|
|
||||||
// scrollStateMap.
|
|
||||||
//
|
|
||||||
// TODO: do this in RoomView rather than here
|
|
||||||
if (!roomInfo.event_id && this.refs.loggedInView) {
|
|
||||||
const scrollState = this.refs.loggedInView.getScrollStateForRoom(roomInfo.room_id);
|
|
||||||
if (scrollState) {
|
|
||||||
newState.initialEventId = scrollState.focussedEvent;
|
|
||||||
newState.initialEventPixelOffset = scrollState.pixelOffset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the first sync to complete so that if a room does have an alias,
|
// Wait for the first sync to complete so that if a room does have an alias,
|
||||||
// it would have been retrieved.
|
// it would have been retrieved.
|
||||||
let waitFor = q(null);
|
let waitFor = q(null);
|
||||||
|
@ -669,7 +654,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roomInfo.event_id) {
|
if (roomInfo.event_id && roomInfo.highlighted) {
|
||||||
presentedId += "/" + roomInfo.event_id;
|
presentedId += "/" + roomInfo.event_id;
|
||||||
}
|
}
|
||||||
this.notifyNewScreen('room/' + presentedId);
|
this.notifyNewScreen('room/' + presentedId);
|
||||||
|
@ -1124,8 +1109,10 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
id: '#mylovelyid',
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
event_id: eventId,
|
event_id: eventId,
|
||||||
|
highlighted: Boolean(eventId),
|
||||||
third_party_invite: thirdPartyInvite,
|
third_party_invite: thirdPartyInvite,
|
||||||
oob_data: oobData,
|
oob_data: oobData,
|
||||||
};
|
};
|
||||||
|
|
|
@ -83,36 +83,8 @@ module.exports = React.createClass({
|
||||||
// * invited us tovthe room
|
// * invited us tovthe room
|
||||||
oobData: React.PropTypes.object,
|
oobData: React.PropTypes.object,
|
||||||
|
|
||||||
// id of an event to jump to. If not given, will go to the end of the
|
|
||||||
// live timeline.
|
|
||||||
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
|
|
||||||
// 1/3 of the way down the viewport.
|
|
||||||
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,
|
|
||||||
|
|
||||||
// is the RightPanel collapsed?
|
// is the RightPanel collapsed?
|
||||||
collapsedRhs: React.PropTypes.bool,
|
collapsedRhs: React.PropTypes.bool,
|
||||||
|
|
||||||
// a map from room id to scroll state, which will be updated on unmount.
|
|
||||||
//
|
|
||||||
// If there is no special scroll state (ie, we are following the live
|
|
||||||
// timeline), the scroll state is null. Otherwise, it is 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.
|
|
||||||
scrollStateMap: React.PropTypes.object,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -180,13 +152,28 @@ module.exports = React.createClass({
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
const newState = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
roomAlias: RoomViewStore.getRoomAlias(),
|
roomAlias: RoomViewStore.getRoomAlias(),
|
||||||
roomLoading: RoomViewStore.isRoomLoading(),
|
roomLoading: RoomViewStore.isRoomLoading(),
|
||||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||||
joining: RoomViewStore.isJoining(),
|
joining: RoomViewStore.isJoining(),
|
||||||
}, () => {
|
eventId: RoomViewStore.getEventId(),
|
||||||
|
eventPixelOffset: RoomViewStore.getEventPixelOffset(),
|
||||||
|
isEventHighlighted: RoomViewStore.isEventHighlighted(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.state.eventId !== newState.eventId) {
|
||||||
|
newState.searchResults = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the scroll state for the previous room so that we can return to this
|
||||||
|
// position when viewing this room in future.
|
||||||
|
if (this.state.roomId !== newState.roomId) {
|
||||||
|
this._updateScrollMap(this.state.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(newState, () => {
|
||||||
this._onHaveRoom();
|
this._onHaveRoom();
|
||||||
this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId));
|
this.onRoom(MatrixClientPeg.get().getRoom(this.state.roomId));
|
||||||
});
|
});
|
||||||
|
@ -287,13 +274,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
|
||||||
if (newProps.eventId != this.props.eventId) {
|
|
||||||
// when we change focussed event id, hide the search results.
|
|
||||||
this.setState({searchResults: null});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
||||||
!ObjectUtils.shallowEqual(this.state, nextState));
|
!ObjectUtils.shallowEqual(this.state, nextState));
|
||||||
|
@ -319,7 +299,7 @@ module.exports = React.createClass({
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
// update the scroll map before we get unmounted
|
// update the scroll map before we get unmounted
|
||||||
this._updateScrollMap();
|
this._updateScrollMap(this.state.roomId);
|
||||||
|
|
||||||
if (this.refs.roomView) {
|
if (this.refs.roomView) {
|
||||||
// disconnect the D&D event listeners from the room view. This
|
// disconnect the D&D event listeners from the room view. This
|
||||||
|
@ -598,6 +578,14 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_updateScrollMap(roomId) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'update_scroll_state',
|
||||||
|
room_id: roomId,
|
||||||
|
scroll_state: this._getScrollState(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
if (!room || room.roomId !== this.state.roomId) {
|
if (!room || room.roomId !== this.state.roomId) {
|
||||||
return;
|
return;
|
||||||
|
@ -1240,21 +1228,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// update scrollStateMap on unmount
|
|
||||||
_updateScrollMap: function() {
|
|
||||||
if (!this.state.room) {
|
|
||||||
// we were instantiated on a room alias and haven't yet joined the room.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.props.scrollStateMap) return;
|
|
||||||
|
|
||||||
var roomId = this.state.room.roomId;
|
|
||||||
|
|
||||||
var state = this._getScrollState();
|
|
||||||
this.props.scrollStateMap[roomId] = state;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
// 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.
|
||||||
//
|
//
|
||||||
|
@ -1677,6 +1650,14 @@ module.exports = React.createClass({
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldHighlight = this.state.isEventHighlighted;
|
||||||
|
let highlightedEventId = null;
|
||||||
|
if (this.state.forwardingEvent) {
|
||||||
|
highlightedEventId = this.state.forwardingEvent.getId();
|
||||||
|
} else if (shouldHighlight) {
|
||||||
|
highlightedEventId = this.state.eventId;
|
||||||
|
}
|
||||||
|
|
||||||
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||||
var messagePanel = (
|
var messagePanel = (
|
||||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||||
|
@ -1684,9 +1665,9 @@ module.exports = React.createClass({
|
||||||
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||||
manageReadMarkers={true}
|
manageReadMarkers={true}
|
||||||
hidden={hideMessagePanel}
|
hidden={hideMessagePanel}
|
||||||
highlightedEventId={this.state.forwardingEvent ? this.state.forwardingEvent.getId() : this.props.highlightedEventId}
|
highlightedEventId={highlightedEventId}
|
||||||
eventId={this.props.eventId}
|
eventId={this.state.eventId}
|
||||||
eventPixelOffset={this.props.eventPixelOffset}
|
eventPixelOffset={this.state.eventPixelOffset}
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
||||||
showUrlPreview = { this.state.showUrlPreview }
|
showUrlPreview = { this.state.showUrlPreview }
|
||||||
|
|
|
@ -381,6 +381,7 @@ module.exports = WithMatrixClient(React.createClass({
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
event_id: this.props.mxEvent.getId(),
|
event_id: this.props.mxEvent.getId(),
|
||||||
|
highlighted: true,
|
||||||
room_id: this.props.mxEvent.getRoomId(),
|
room_id: this.props.mxEvent.getRoomId(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,12 +27,32 @@ const INITIAL_STATE = {
|
||||||
joinError: null,
|
joinError: null,
|
||||||
// The room ID of the room
|
// The room ID of the room
|
||||||
roomId: null,
|
roomId: null,
|
||||||
|
// The event being viewed
|
||||||
|
eventId: null,
|
||||||
|
// The offset to display the event at (see scrollStateMap)
|
||||||
|
eventPixelOffset: null,
|
||||||
|
// Whether to highlight the event
|
||||||
|
isEventHighlighted: false,
|
||||||
// The room alias of the room (or null if not originally specified in view_room)
|
// The room alias of the room (or null if not originally specified in view_room)
|
||||||
roomAlias: null,
|
roomAlias: null,
|
||||||
// Whether the current room is loading
|
// Whether the current room is loading
|
||||||
roomLoading: false,
|
roomLoading: false,
|
||||||
// Any error that has occurred during loading
|
// Any error that has occurred during loading
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
|
// A map from room id to scroll state.
|
||||||
|
//
|
||||||
|
// If there is no special scroll state (ie, we are following the live
|
||||||
|
// timeline), the scroll state is null. Otherwise, it is 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.
|
||||||
|
scrollStateMap: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,8 +76,11 @@ class RoomViewStore extends Store {
|
||||||
__onDispatch(payload) {
|
__onDispatch(payload) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// view_room:
|
// view_room:
|
||||||
// - room_alias: '#somealias:matrix.org'
|
// - room_alias: '#somealias:matrix.org'
|
||||||
// - room_id: '!roomid123:matrix.org'
|
// - room_id: '!roomid123:matrix.org'
|
||||||
|
// - event_id: '$213456782:matrix.org'
|
||||||
|
// - event_offset: 100
|
||||||
|
// - highlighted: true
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
this._viewRoom(payload);
|
this._viewRoom(payload);
|
||||||
break;
|
break;
|
||||||
|
@ -88,20 +111,39 @@ class RoomViewStore extends Store {
|
||||||
case 'on_logged_out':
|
case 'on_logged_out':
|
||||||
this.reset();
|
this.reset();
|
||||||
break;
|
break;
|
||||||
|
case 'update_scroll_state':
|
||||||
|
this._updateScrollState(payload);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_viewRoom(payload) {
|
_viewRoom(payload) {
|
||||||
// Always set the room ID if present
|
|
||||||
if (payload.room_id) {
|
if (payload.room_id) {
|
||||||
|
// If an event ID wasn't specified, default to the one saved for this room
|
||||||
|
// via update_scroll_state. Also assume event_offset should be set.
|
||||||
|
if (!payload.event_id) {
|
||||||
|
const roomScrollState = this._state.scrollStateMap[payload.room_id];
|
||||||
|
if (roomScrollState) {
|
||||||
|
payload.event_id = roomScrollState.focussedEvent;
|
||||||
|
payload.event_offset = roomScrollState.pixelOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._setState({
|
this._setState({
|
||||||
roomId: payload.room_id,
|
roomId: payload.room_id,
|
||||||
|
eventId: payload.event_id,
|
||||||
|
eventPixelOffset: payload.event_offset,
|
||||||
|
isEventHighlighted: payload.highlighted,
|
||||||
roomLoading: false,
|
roomLoading: false,
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
});
|
});
|
||||||
} else if (payload.room_alias) {
|
} else if (payload.room_alias) {
|
||||||
|
// Resolve the alias and then do a second dispatch with the room ID acquired
|
||||||
this._setState({
|
this._setState({
|
||||||
roomId: null,
|
roomId: null,
|
||||||
|
eventId: null,
|
||||||
|
eventPixelOffset: null,
|
||||||
|
isEventHighlighted: null,
|
||||||
roomAlias: payload.room_alias,
|
roomAlias: payload.room_alias,
|
||||||
roomLoading: true,
|
roomLoading: true,
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
|
@ -111,6 +153,8 @@ class RoomViewStore extends Store {
|
||||||
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,
|
||||||
|
highlighted: payload.highlighted,
|
||||||
room_alias: payload.room_alias,
|
room_alias: payload.room_alias,
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
|
@ -168,6 +212,15 @@ class RoomViewStore extends Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateScrollState(payload) {
|
||||||
|
// Clobber existing scroll state for the given room ID
|
||||||
|
const newScrollStateMap = this._state.scrollStateMap;
|
||||||
|
newScrollStateMap[payload.room_id] = payload.scroll_state;
|
||||||
|
this._setState({
|
||||||
|
scrollStateMap: newScrollStateMap,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this._state = Object.assign({}, INITIAL_STATE);
|
this._state = Object.assign({}, INITIAL_STATE);
|
||||||
}
|
}
|
||||||
|
@ -176,6 +229,18 @@ class RoomViewStore extends Store {
|
||||||
return this._state.roomId;
|
return this._state.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEventId() {
|
||||||
|
return this._state.eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventPixelOffset() {
|
||||||
|
return this._state.eventPixelOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEventHighlighted() {
|
||||||
|
return this._state.isEventHighlighted;
|
||||||
|
}
|
||||||
|
|
||||||
getRoomAlias() {
|
getRoomAlias() {
|
||||||
return this._state.roomAlias;
|
return this._state.roomAlias;
|
||||||
}
|
}
|
||||||
|
@ -195,7 +260,6 @@ class RoomViewStore extends Store {
|
||||||
getJoinError() {
|
getJoinError() {
|
||||||
return this._state.joinError;
|
return this._state.joinError;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let singletonRoomViewStore = null;
|
let singletonRoomViewStore = null;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue