Merge remote-tracking branch 'origin/t3chguy/on_copy_tooltip' into t3chguy/on_copy_tooltip

This commit is contained in:
Michael Telatynski 2017-09-13 14:18:56 +01:00
commit fea7af11b4
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
16 changed files with 342 additions and 339 deletions

View file

@ -1,3 +1,53 @@
Changes in [0.10.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3) (2017-09-06)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.2...v0.10.3)
* No changes
Changes in [0.10.3-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.2) (2017-09-05)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.3-rc.1...v0.10.3-rc.2)
* Fix plurals in translations
[\#1358](https://github.com/matrix-org/matrix-react-sdk/pull/1358)
* Fix typo
[\#1357](https://github.com/matrix-org/matrix-react-sdk/pull/1357)
* Update from Weblate.
[\#1356](https://github.com/matrix-org/matrix-react-sdk/pull/1356)
Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.3-rc.1) (2017-09-01)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.2...v0.10.3-rc.1)
* Fix room change sometimes being very slow
[\#1354](https://github.com/matrix-org/matrix-react-sdk/pull/1354)
* apply shouldHideEvent fn to onRoomTimeline for RoomStatusBar
[\#1346](https://github.com/matrix-org/matrix-react-sdk/pull/1346)
* text4event widget modified, used to show widget added each time.
[\#1345](https://github.com/matrix-org/matrix-react-sdk/pull/1345)
* separate concepts of showing and managing RRs to fix regression
[\#1352](https://github.com/matrix-org/matrix-react-sdk/pull/1352)
* Make staging widgets work with live and vice versa.
[\#1350](https://github.com/matrix-org/matrix-react-sdk/pull/1350)
* Avoid breaking /sync with uncaught exceptions
[\#1349](https://github.com/matrix-org/matrix-react-sdk/pull/1349)
* we need to pass whether it is an invite RoomSubList explicitly (i18n)
[\#1343](https://github.com/matrix-org/matrix-react-sdk/pull/1343)
* Percent encoding isn't a valid thing within _t
[\#1348](https://github.com/matrix-org/matrix-react-sdk/pull/1348)
* Fix spurious notifications
[\#1339](https://github.com/matrix-org/matrix-react-sdk/pull/1339)
* Unbreak password reset with a non-default HS
[\#1347](https://github.com/matrix-org/matrix-react-sdk/pull/1347)
* Remove unnecessary 'load' on notif audio element
[\#1341](https://github.com/matrix-org/matrix-react-sdk/pull/1341)
* _tJsx returns a React Object, the sub fn must return a React Object
[\#1340](https://github.com/matrix-org/matrix-react-sdk/pull/1340)
* Fix deprecation warning about promise.defer()
[\#1292](https://github.com/matrix-org/matrix-react-sdk/pull/1292)
* Fix click to insert completion
[\#1331](https://github.com/matrix-org/matrix-react-sdk/pull/1331)
Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24) Changes in [0.10.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.2) (2017-08-24)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.1...v0.10.2)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.10.2", "version": "0.10.3",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -73,7 +73,6 @@
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"react-sticky": "^6.0.1",
"sanitize-html": "^1.14.1", "sanitize-html": "^1.14.1",
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0", "url": "^0.11.0",

View file

@ -30,18 +30,6 @@ function getDaysArray() {
]; ];
} }
function getLongDaysArray() {
return [
_t('Sunday'),
_t('Monday'),
_t('Tuesday'),
_t('Wednesday'),
_t('Thursday'),
_t('Friday'),
_t('Saturday'),
];
}
function getMonthsArray() { function getMonthsArray() {
return [ return [
_t('Jan'), _t('Jan'),
@ -108,38 +96,6 @@ module.exports = {
}); });
}, },
formatDateSeparator: function(date) {
const days = getDaysArray();
const longDays = getLongDaysArray();
const months = getMonthsArray();
const today = new Date();
const yesterday = new Date();
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return _t('Today');
} else if (date.toDateString() === yesterday.toDateString()) {
return _t('Yesterday');
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return longDays[date.getDay()];
} else if (today.getTime() - date.getTime() < 365 * 24 * 60 * 60 * 1000) {
return _t('%(weekDayName)s, %(monthName)s %(day)s', {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
});
} else {
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
weekDayName: days[date.getDay()],
monthName: months[date.getMonth()],
day: date.getDate(),
fullYear: date.getFullYear(),
});
}
},
formatTime: function(date, showTwelveHour=false) { formatTime: function(date, showTwelveHour=false) {
if (showTwelveHour) { if (showTwelveHour) {
return twelveHourTime(date); return twelveHourTime(date);

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -31,13 +32,28 @@ emojione.imagePathPNG = 'emojione/png/';
// Use SVGs for emojis // Use SVGs for emojis
emojione.imageType = 'svg'; emojione.imageType = 'svg';
const SIMPLE_EMOJI_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
/*
* Return true if the given string contains emoji
* Uses a much, much simpler regex than emojione's so will give false
* positives, but useful for fast-path testing strings to see if they
* need emojification.
* unicodeToImage uses this function.
*/
export function containsEmoji(str) {
return SIMPLE_EMOJI_PATTERN.test(str);
}
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js /* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
* because we want to include emoji shortnames in title text * because we want to include emoji shortnames in title text
*/ */
export function unicodeToImage(str) { export function unicodeToImage(str) {
// fast path
if (!containsEmoji(str)) return str;
let replaceWith, unicode, alt, short, fname; let replaceWith, unicode, alt, short, fname;
const mappedUnicode = emojione.mapUnicodeToShort(); const mappedUnicode = emojione.mapUnicodeToShort();
@ -393,7 +409,6 @@ export function bodyToHtml(content, highlights, opts) {
} }
safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = sanitizeHtml(body, sanitizeHtmlParams);
safeBody = unicodeToImage(safeBody); safeBody = unicodeToImage(safeBody);
safeBody = addCodeCopyButton(safeBody);
} }
finally { finally {
delete sanitizeHtmlParams.textFilter; delete sanitizeHtmlParams.textFilter;
@ -412,23 +427,6 @@ export function bodyToHtml(content, highlights, opts) {
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />; return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
} }
function addCodeCopyButton(safeBody) {
// Adds 'copy' buttons to pre blocks
// Note that this only manipulates the markup to add the buttons:
// we need to add the event handlers once the nodes are in the DOM
// since we can't save functions in the markup.
// This is done in TextualBody
const el = document.createElement("div");
el.innerHTML = safeBody;
const codeBlocks = Array.from(el.getElementsByTagName("pre"));
codeBlocks.forEach(p => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
p.appendChild(button);
});
return el.innerHTML;
}
export function emojifyText(text) { export function emojifyText(text) {
return { return {
__html: unicodeToImage(escape(text)), __html: unicodeToImage(escape(text)),

View file

@ -84,6 +84,9 @@ class Skinner {
// behaviour with multiple copies of files etc. is erratic at best. // behaviour with multiple copies of files etc. is erratic at best.
// XXX: We can still end up with the same file twice in the resulting // XXX: We can still end up with the same file twice in the resulting
// JS bundle which is nonideal. // JS bundle which is nonideal.
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
// ("Modules are cached based on their resolved filename")
if (global.mxSkinner === undefined) { if (global.mxSkinner === undefined) {
global.mxSkinner = new Skinner(); global.mxSkinner = new Skinner();
} }

View file

@ -81,10 +81,6 @@ export default React.createClass({
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
// _scrollStateMap is a map from room id to the scroll state returned by
// RoomView.getScrollState()
this._scrollStateMap = {};
CallMediaHandler.loadDevices(); CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onKeyDown); document.addEventListener('keydown', this._onKeyDown);
@ -116,10 +112,6 @@ export default React.createClass({
return Boolean(MatrixClientPeg.get()); return Boolean(MatrixClientPeg.get());
}, },
getScrollStateForRoom: function(roomId) {
return this._scrollStateMap[roomId];
},
canResetTimelineInRoom: function(roomId) { canResetTimelineInRoom: function(roomId) {
if (!this.refs.roomView) { if (!this.refs.roomView) {
return true; return true;
@ -248,7 +240,6 @@ export default React.createClass({
opacity={this.props.middleOpacity} opacity={this.props.middleOpacity}
collapsedRhs={this.props.collapse_rhs} collapsedRhs={this.props.collapse_rhs}
ConferenceHandler={this.props.ConferenceHandler} ConferenceHandler={this.props.ConferenceHandler}
scrollStateMap={this._scrollStateMap}
/>; />;
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />; if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
break; break;

View file

@ -38,7 +38,6 @@ import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle'; import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions // LifecycleStore is not used but does listen to and dispatch actions
require('../../stores/LifecycleStore'); require('../../stores/LifecycleStore');
import RoomViewStore from '../../stores/RoomViewStore';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
@ -214,9 +213,6 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
this._onRoomViewStoreUpdated();
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
// Used by _viewRoom before getting state from sync // Used by _viewRoom before getting state from sync
@ -353,7 +349,6 @@ module.exports = React.createClass({
UDEHandler.stopListening(); UDEHandler.stopListening();
window.removeEventListener("focus", this.onFocus); window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
this._roomViewStoreToken.remove();
}, },
componentDidUpdate: function() { componentDidUpdate: function() {
@ -587,10 +582,6 @@ module.exports = React.createClass({
} }
}, },
_onRoomViewStoreUpdated: function() {
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
},
_setPage: function(pageType) { _setPage: function(pageType) {
this.setState({ this.setState({
page_type: pageType, page_type: pageType,
@ -677,6 +668,7 @@ module.exports = React.createClass({
this.focusComposer = true; this.focusComposer = true;
const newState = { const newState = {
currentRoomId: roomInfo.room_id || null,
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,

View file

@ -61,6 +61,9 @@ module.exports = React.createClass({
// for pending messages. // for pending messages.
ourUserId: React.PropTypes.string, ourUserId: React.PropTypes.string,
// true to suppress the date at the start of the timeline
suppressFirstDateSeparator: React.PropTypes.bool,
// whether to show read receipts // whether to show read receipts
showReadReceipts: React.PropTypes.bool, showReadReceipts: React.PropTypes.bool,
@ -514,10 +517,10 @@ module.exports = React.createClass({
_wantsDateSeparator: function(prevEvent, nextEventDate) { _wantsDateSeparator: function(prevEvent, nextEventDate) {
if (prevEvent == null) { if (prevEvent == null) {
// First event in the panel always wants a DateSeparator // first event in the panel: depends if we could back-paginate from
return true; // here.
return !this.props.suppressFirstDateSeparator;
} }
const prevEventDate = prevEvent.getDate(); const prevEventDate = prevEvent.getDate();
if (!nextEventDate || !prevEventDate) { if (!nextEventDate || !prevEventDate) {
return false; return false;

View file

@ -47,6 +47,7 @@ import KeyCode from '../../KeyCode';
import UserProvider from '../../autocomplete/UserProvider'; import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
let DEBUG = false; let DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
@ -163,7 +164,6 @@ module.exports = React.createClass({
roomLoadError: RoomViewStore.getRoomLoadError(), roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(), joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(), forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(), shouldPeek: RoomViewStore.shouldPeek(),
@ -189,6 +189,25 @@ module.exports = React.createClass({
// the RoomView instance // the RoomView instance
if (initial) { if (initial) {
newState.room = MatrixClientPeg.get().getRoom(newState.roomId); newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
if (newState.room) {
newState.unsentMessageError = this._getUnsentMessageError(newState.room);
newState.showApps = this._shouldShowApps(newState.room);
this._onRoomLoaded(newState.room);
}
}
if (this.state.roomId === null && newState.roomId !== null) {
// Get the scroll state for the new room
// If an event ID wasn't specified, default to the one saved for this room
// in the scroll state store. Assume initialEventPixelOffset should be set.
if (!newState.initialEventId) {
const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId);
if (roomScrollState) {
newState.initialEventId = roomScrollState.focussedEvent;
newState.initialEventPixelOffset = roomScrollState.pixelOffset;
}
}
} }
// Clear the search results when clicking a search result (which changes the // Clear the search results when clicking a search result (which changes the
@ -197,22 +216,20 @@ module.exports = React.createClass({
newState.searchResults = null; newState.searchResults = null;
} }
// Store the scroll state for the previous room so that we can return to this this.setState(newState);
// position when viewing this room in future. // At this point, newState.roomId could be null (e.g. the alias might not
if (this.state.roomId !== newState.roomId) { // have been resolved yet) so anything called here must handle this case.
this._updateScrollMap(this.state.roomId);
}
this.setState(newState, () => { // We pass the new state into this function for it to read: it needs to
// At this point, this.state.roomId could be null (e.g. the alias might not // observe the new state but we don't want to put it in the setState
// have been resolved yet) so anything called here must handle this case. // callback because this would prevent the setStates from being batched,
if (initial) { // ie. cause it to render RoomView twice rather than the once that is necessary.
this._onHaveRoom(); if (initial) {
} this._setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek);
}); }
}, },
_onHaveRoom: function() { _setupRoom: function(room, roomId, joining, shouldPeek) {
// 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)
@ -228,23 +245,15 @@ module.exports = React.createClass({
// about it). We don't peek in the historical case where we were joined but are // about it). We don't peek in the historical case where we were joined but are
// now not joined because the js-sdk peeking API will clobber our historical room, // now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room. // making it impossible to indicate a newly joined room.
const room = this.state.room; if (!joining && roomId) {
if (room) {
this.setState({
unsentMessageError: this._getUnsentMessageError(room),
showApps: this._shouldShowApps(room),
});
this._onRoomLoaded(room);
}
if (!this.state.joining && this.state.roomId) {
if (this.props.autoJoin) { if (this.props.autoJoin) {
this.onJoinButtonClicked(); this.onJoinButtonClicked();
} else if (!room && this.state.shouldPeek) { } else if (!room && shouldPeek) {
console.log("Attempting to peek into room %s", this.state.roomId); console.log("Attempting to peek into room %s", roomId);
this.setState({ this.setState({
peekLoading: true, peekLoading: true,
}); });
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => { MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
this.setState({ this.setState({
room: room, room: room,
peekLoading: false, peekLoading: false,
@ -340,7 +349,9 @@ 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.state.roomId); if (this.state.roomId) {
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
}
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
@ -617,18 +628,6 @@ module.exports = React.createClass({
}); });
}, },
_updateScrollMap(roomId) {
// No point updating scroll state if the room ID hasn't been resolved yet
if (!roomId) {
return;
}
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;

View file

@ -17,7 +17,6 @@ limitations under the License.
var React = require("react"); var React = require("react");
var ReactDOM = require("react-dom"); var ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar'); var GeminiScrollbar = require('react-gemini-scrollbar');
import { StickyContainer } from 'react-sticky';
import Promise from 'bluebird'; import Promise from 'bluebird';
var KeyCode = require('../../KeyCode'); var KeyCode = require('../../KeyCode');
@ -78,52 +77,111 @@ if (DEBUG_SCROLL) {
* scroll down further. If stickyBottom is disabled, we just save the scroll * scroll down further. If stickyBottom is disabled, we just save the scroll
* offset as normal. * offset as normal.
*/ */
export default class ScrollPanel extends StickyContainer { module.exports = React.createClass({
displayName: 'ScrollPanel',
constructor() { propTypes: {
super(); /* stickyBottom: if set to true, then once the user hits the bottom of
this.onResize = this.onResize.bind(this); * the list, any new children added to the list will cause the list to
this.onScroll = this.onScroll.bind(this); * scroll down to show the new element, rather than preserving the
} * existing view.
*/
stickyBottom: React.PropTypes.bool,
componentWillMount() { /* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom.
* XXX: It's likley this is unecessary and can be derived from
* stickyBottom, but I'm adding an extra parameter to ensure
* behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line.
*/
startAtBottom: React.PropTypes.bool,
/* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards =
* false) of the list.
*
* This should return a promise; no more calls will be made until the
* promise completes.
*
* The promise should resolve to true if there is more data to be
* retrieved in this direction (in which case onFillRequest may be
* called again immediately), or false if there is no more data in this
* directon (at this time) - which will stop the pagination cycle until
* the user scrolls again.
*/
onFillRequest: React.PropTypes.func,
/* onUnfillRequest(backwards): a callback which is called on scroll when
* there are children elements that are far out of view and could be removed
* without causing pagination to occur.
*
* This function should accept a boolean, which is true to indicate the back/top
* of the panel and false otherwise, and a scroll token, which refers to the
* first element to remove if removing from the front/bottom, and last element
* to remove if removing from the back/top.
*/
onUnfillRequest: React.PropTypes.func,
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll: React.PropTypes.func,
/* onResize: a callback which is called whenever the Gemini scroll
* panel is resized
*/
onResize: React.PropTypes.func,
/* className: classnames to add to the top-level div
*/
className: React.PropTypes.string,
/* style: styles to add to the top-level div
*/
style: React.PropTypes.object,
},
getDefaultProps: function() {
return {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
};
},
componentWillMount: function() {
this._pendingFillRequests = {b: null, f: null}; this._pendingFillRequests = {b: null, f: null};
this.resetScrollState(); this.resetScrollState();
} },
componentDidMount() { componentDidMount: function() {
this.checkFillState(); this.checkScroll();
} },
componentDidUpdate() { componentDidUpdate: function() {
// after adding event tiles, we may need to tweak the scroll (either to // after adding event tiles, we may need to tweak the scroll (either to
// keep at the bottom of the timeline, or to maintain the view after // keep at the bottom of the timeline, or to maintain the view after
// adding events to the top). // adding events to the top).
// //
// This will also re-check the fill state, in case the paginate was inadequate // This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll(); this.checkScroll();
} },
componentWillUnmount() { componentWillUnmount: function() {
// set a boolean to say we've been unmounted, which any pending // set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results. // promises can use to throw away their results.
// //
// (We could use isMounted(), but facebook have deprecated that.) // (We could use isMounted(), but facebook have deprecated that.)
this.unmounted = true; this.unmounted = true;
} },
onScroll(ev) { onScroll: function(ev) {
var sn = this._getScrollNode(); var sn = this._getScrollNode();
debuglog("Scroll event: offset now:", sn.scrollTop, debuglog("Scroll event: offset now:", sn.scrollTop,
"_lastSetScroll:", this._lastSetScroll); "_lastSetScroll:", this._lastSetScroll);
// Set the node and notify subscribers of the StickyContainer
// By extending StickyContainer, we can set the scroll node to be that of the
// ScrolPanel to allow any `<Sticky>` children to be sticky, namely DateSeparators.
this.node = sn;
// Update subscribers - arbitrarily nested `<Sticky>` children
this.notifySubscribers(ev);
// Sometimes we see attempts to write to scrollTop essentially being // Sometimes we see attempts to write to scrollTop essentially being
// ignored. (Or rather, it is successfully written, but on the next // ignored. (Or rather, it is successfully written, but on the next
// scroll event, it's been reset again). // scroll event, it's been reset again).
@ -159,27 +217,27 @@ export default class ScrollPanel extends StickyContainer {
this.props.onScroll(ev); this.props.onScroll(ev);
this.checkFillState(); this.checkFillState();
} },
onResize() { onResize: function() {
this.props.onResize(); this.props.onResize();
this.checkScroll(); this.checkScroll();
this.refs.geminiPanel.forceUpdate(); this.refs.geminiPanel.forceUpdate();
} },
// after an update to the contents of the panel, check that the scroll is // after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary. // where it ought to be, and set off pagination requests if necessary.
checkScroll() { checkScroll: function() {
this._restoreSavedScrollState(); this._restoreSavedScrollState();
this.checkFillState(); this.checkFillState();
} },
// 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 this is independent of the 'stuckAtBottom' state - it is simply // note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the the content is scrolled down right now, irrespective of // about whether the the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update. // whether it will stay that way when the children update.
isAtBottom() { isAtBottom: function() {
var sn = this._getScrollNode(); var sn = this._getScrollNode();
// there seems to be some bug with flexbox/gemini/chrome/richvdh's // there seems to be some bug with flexbox/gemini/chrome/richvdh's
@ -189,7 +247,7 @@ export default class ScrollPanel extends StickyContainer {
// that we're at the bottom when we're still a few pixels off. // that we're at the bottom when we're still a few pixels off.
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3; return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
} },
// returns the vertical height in the given direction that can be removed from // returns the vertical height in the given direction that can be removed from
// the content box (which has a height of scrollHeight, see checkFillState) without // the content box (which has a height of scrollHeight, see checkFillState) without
@ -222,17 +280,17 @@ export default class ScrollPanel extends StickyContainer {
// |#########| - | // |#########| - |
// |#########| | // |#########| |
// `---------' - // `---------' -
_getExcessHeight(backwards) { _getExcessHeight: function(backwards) {
var sn = this._getScrollNode(); var sn = this._getScrollNode();
if (backwards) { if (backwards) {
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING; return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
} else { } else {
return sn.scrollHeight - (sn.scrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING; return sn.scrollHeight - (sn.scrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
} }
} },
// check the scroll state and send out backfill requests if necessary. // check the scroll state and send out backfill requests if necessary.
checkFillState() { checkFillState: function() {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
@ -271,10 +329,10 @@ export default class ScrollPanel extends StickyContainer {
// need to forward-fill // need to forward-fill
this._maybeFill(false); this._maybeFill(false);
} }
} },
// check if unfilling is possible and send an unfill request if necessary // check if unfilling is possible and send an unfill request if necessary
_checkUnfillState(backwards) { _checkUnfillState: function(backwards) {
let excessHeight = this._getExcessHeight(backwards); let excessHeight = this._getExcessHeight(backwards);
if (excessHeight <= 0) { if (excessHeight <= 0) {
return; return;
@ -315,10 +373,10 @@ export default class ScrollPanel extends StickyContainer {
this.props.onUnfillRequest(backwards, markerScrollToken); this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS); }, UNFILL_REQUEST_DEBOUNCE_MS);
} }
} },
// check if there is already a pending fill request. If not, set one off. // check if there is already a pending fill request. If not, set one off.
_maybeFill(backwards) { _maybeFill: function(backwards) {
var dir = backwards ? 'b' : 'f'; var dir = backwards ? 'b' : 'f';
if (this._pendingFillRequests[dir]) { if (this._pendingFillRequests[dir]) {
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another"); debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
@ -350,7 +408,7 @@ export default class ScrollPanel extends StickyContainer {
this.checkFillState(); this.checkFillState();
} }
}).done(); }).done();
} },
/* get the current scroll state. This returns an object with the following /* get the current scroll state. This returns an object with the following
* properties: * properties:
@ -366,9 +424,9 @@ export default class ScrollPanel extends StickyContainer {
* the number of pixels the bottom of the tracked child is above the * the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel. * bottom of the scroll panel.
*/ */
getScrollState() { getScrollState: function() {
return this.scrollState; return this.scrollState;
} },
/* reset the saved scroll state. /* reset the saved scroll state.
* *
@ -382,46 +440,46 @@ export default class ScrollPanel extends StickyContainer {
* no use if no children exist yet, or if you are about to replace the * no use if no children exist yet, or if you are about to replace the
* child list.) * child list.)
*/ */
resetScrollState() { resetScrollState: function() {
this.scrollState = {stuckAtBottom: this.props.startAtBottom}; this.scrollState = {stuckAtBottom: this.props.startAtBottom};
} },
/** /**
* jump to the top of the content. * jump to the top of the content.
*/ */
scrollToTop() { scrollToTop: function() {
this._setScrollTop(0); this._setScrollTop(0);
this._saveScrollState(); this._saveScrollState();
} },
/** /**
* jump to the bottom of the content. * jump to the bottom of the content.
*/ */
scrollToBottom() { scrollToBottom: function() {
// the easiest way to make sure that the scroll state is correctly // the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating // 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 // it ourselves is hard, and we can't rely on an onScroll callback
// happening, since there may be no user-visible change here). // happening, since there may be no user-visible change here).
this._setScrollTop(Number.MAX_VALUE); this._setScrollTop(Number.MAX_VALUE);
this._saveScrollState(); this._saveScrollState();
} },
/** /**
* Page up/down. * Page up/down.
* *
* mult: -1 to page up, +1 to page down * mult: -1 to page up, +1 to page down
*/ */
scrollRelative(mult) { scrollRelative: function(mult) {
var scrollNode = this._getScrollNode(); var scrollNode = this._getScrollNode();
var delta = mult * scrollNode.clientHeight * 0.5; var delta = mult * scrollNode.clientHeight * 0.5;
this._setScrollTop(scrollNode.scrollTop + delta); this._setScrollTop(scrollNode.scrollTop + delta);
this._saveScrollState(); this._saveScrollState();
} },
/** /**
* Scroll up/down in response to a scroll key * Scroll up/down in response to a scroll key
*/ */
handleScrollKey(ev) { handleScrollKey: function(ev) {
switch (ev.keyCode) { switch (ev.keyCode) {
case KeyCode.PAGE_UP: case KeyCode.PAGE_UP:
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
@ -447,7 +505,7 @@ export default class ScrollPanel extends StickyContainer {
} }
break; break;
} }
} },
/* Scroll the panel to bring the DOM node with the scroll token /* Scroll the panel to bring the DOM node with the scroll token
* `scrollToken` into view. * `scrollToken` into view.
@ -460,7 +518,7 @@ export default class ScrollPanel extends StickyContainer {
* node (specifically, the bottom of it) will be positioned. If omitted, it * node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0. * defaults to 0.
*/ */
scrollToToken(scrollToken, pixelOffset, offsetBase) { scrollToToken: function(scrollToken, pixelOffset, offsetBase) {
pixelOffset = pixelOffset || 0; pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 0; offsetBase = offsetBase || 0;
@ -482,11 +540,11 @@ export default class ScrollPanel extends StickyContainer {
// ... then make it so. // ... then make it so.
this._restoreSavedScrollState(); this._restoreSavedScrollState();
} },
// set the scrollTop attribute appropriately to position the given child at the // set the scrollTop attribute appropriately to position the given child at the
// given offset in the window. A helper for _restoreSavedScrollState. // given offset in the window. A helper for _restoreSavedScrollState.
_scrollToToken(scrollToken, pixelOffset) { _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;
@ -518,9 +576,9 @@ export default class ScrollPanel extends StickyContainer {
this._setScrollTop(scrollNode.scrollTop + scrollDelta); this._setScrollTop(scrollNode.scrollTop + scrollDelta);
} }
} },
_saveScrollState() { _saveScrollState: function() {
if (this.props.stickyBottom && this.isAtBottom()) { if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true }; this.scrollState = { stuckAtBottom: true };
debuglog("ScrollPanel: Saved scroll state", this.scrollState); debuglog("ScrollPanel: Saved scroll state", this.scrollState);
@ -558,9 +616,9 @@ export default class ScrollPanel extends StickyContainer {
} else { } else {
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport"); debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
} }
} },
_restoreSavedScrollState() { _restoreSavedScrollState: function() {
var scrollState = this.scrollState; var scrollState = this.scrollState;
var scrollNode = this._getScrollNode(); var scrollNode = this._getScrollNode();
@ -570,9 +628,9 @@ export default class ScrollPanel extends StickyContainer {
this._scrollToToken(scrollState.trackedScrollToken, this._scrollToToken(scrollState.trackedScrollToken,
scrollState.pixelOffset); scrollState.pixelOffset);
} }
} },
_setScrollTop(scrollTop) { _setScrollTop: function(scrollTop) {
var scrollNode = this._getScrollNode(); var scrollNode = this._getScrollNode();
var prevScroll = scrollNode.scrollTop; var prevScroll = scrollNode.scrollTop;
@ -594,12 +652,12 @@ export default class ScrollPanel extends StickyContainer {
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop, debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
"requested:", scrollTop, "requested:", scrollTop,
"_lastSetScroll:", this._lastSetScroll); "_lastSetScroll:", this._lastSetScroll);
} },
/* 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.
*/ */
_getScrollNode() { _getScrollNode: function() {
if (this.unmounted) { if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into // this shouldn't happen, but when it does, turn the NPE into
// something more meaningful. // something more meaningful.
@ -607,91 +665,21 @@ export default class ScrollPanel extends StickyContainer {
} }
return this.refs.geminiPanel.scrollbar.getViewElement(); return this.refs.geminiPanel.scrollbar.getViewElement();
} },
render() { render: function() {
// TODO: the classnames on the div and ol could do with being updated to // TODO: the classnames on the div and ol could do with being updated to
// reflect the fact that we don't necessarily contain a list of messages. // reflect the fact that we don't necessarily contain a list of messages.
// it's not obvious why we have a separate div and ol anyway. // it's not obvious why we have a separate div and ol anyway.
return ( return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
<GeminiScrollbar autoshow={true} ref="geminiPanel" onScroll={this.onScroll} onResize={this.onResize}
onScroll={this.onScroll} onResize={this.onResize} className={this.props.className} style={this.props.style}>
className={this.props.className} style={this.props.style}> <div className="mx_RoomView_messageListWrapper">
<div className="mx_RoomView_messageListWrapper"> <ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite"> {this.props.children}
{this.props.children} </ol>
</ol> </div>
</div> </GeminiScrollbar>
</GeminiScrollbar> );
); },
} });
}
ScrollPanel.propTypes = {
/* stickyBottom: if set to true, then once the user hits the bottom of
* the list, any new children added to the list will cause the list to
* scroll down to show the new element, rather than preserving the
* existing view.
*/
stickyBottom: React.PropTypes.bool,
/* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom.
* XXX: It's likley this is unecessary and can be derived from
* stickyBottom, but I'm adding an extra parameter to ensure
* behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line.
*/
startAtBottom: React.PropTypes.bool,
/* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards =
* false) of the list.
*
* This should return a promise; no more calls will be made until the
* promise completes.
*
* The promise should resolve to true if there is more data to be
* retrieved in this direction (in which case onFillRequest may be
* called again immediately), or false if there is no more data in this
* directon (at this time) - which will stop the pagination cycle until
* the user scrolls again.
*/
onFillRequest: React.PropTypes.func,
/* onUnfillRequest(backwards): a callback which is called on scroll when
* there are children elements that are far out of view and could be removed
* without causing pagination to occur.
*
* This function should accept a boolean, which is true to indicate the back/top
* of the panel and false otherwise, and a scroll token, which refers to the
* first element to remove if removing from the front/bottom, and last element
* to remove if removing from the back/top.
*/
onUnfillRequest: React.PropTypes.func,
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll: React.PropTypes.func,
/* onResize: a callback which is called whenever the Gemini scroll
* panel is resized
*/
onResize: React.PropTypes.func,
/* className: classnames to add to the top-level div
*/
className: React.PropTypes.string,
/* style: styles to add to the top-level div
*/
style: React.PropTypes.object,
};
ScrollPanel.defaultProps = {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onScroll: function() {},
};

View file

@ -1147,6 +1147,7 @@ var TimelinePanel = React.createClass({
highlightedEventId={ this.props.highlightedEventId } highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId } readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible } readMarkerVisible={ this.state.readMarkerVisible }
suppressFirstDateSeparator={ this.state.canBackPaginate }
showUrlPreview={ this.props.showUrlPreview } showUrlPreview={ this.props.showUrlPreview }
showReadReceipts={ this.props.showReadReceipts } showReadReceipts={ this.props.showReadReceipts }
ourUserId={ MatrixClientPeg.get().credentials.userId } ourUserId={ MatrixClientPeg.get().credentials.userId }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,12 +16,19 @@
*/ */
import React from 'react'; import React from 'react';
import {emojifyText} from '../../../HtmlUtils'; import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) { export default function EmojiText(props) {
const {element, children, ...restProps} = props; const {element, children, ...restProps} = props;
restProps.dangerouslySetInnerHTML = emojifyText(children);
return React.createElement(element, restProps); // fast path: simple regex to detect strings that don't contain
// emoji and just return them
if (containsEmoji(children)) {
restProps.dangerouslySetInnerHTML = emojifyText(children);
return React.createElement(element, restProps);
} else {
return React.createElement(element, restProps, children);
}
} }
EmojiText.propTypes = { EmojiText.propTypes = {

View file

@ -118,28 +118,7 @@ module.exports = React.createClass({
} }
}, 10); }, 10);
} }
// add event handlers to the 'copy code' buttons this._addCodeCopyButton();
const buttons = ReactDOM.findDOMNode(this).getElementsByClassName("mx_EventTile_copyButton");
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = (e) => {
const copyCode = buttons[i].parentNode.getElementsByTagName("code")[0];
const successful = this.copyToClipboard(copyCode.textContent);
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
chevronOffset: 10,
left: x,
top: y,
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
e.target.onmouseout = close;
};
}
} }
}, },
@ -276,6 +255,33 @@ module.exports = React.createClass({
} }
}, },
_addCodeCopyButton() {
// Add 'copy' buttons to pre blocks
ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre').forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
button.onclick = (e) => {
const copyCode = button.parentNode.getElementsByTagName("code")[0];
const successful = this.copyToClipboard(copyCode.textContent);
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
chevronOffset: 10,
left: x,
top: y,
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
e.target.onmouseout = close;
};
p.appendChild(button);
});
},
onCancelClick: function(event) { onCancelClick: function(event) {
this.setState({ widgetHidden: true }); this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage // FIXME: persist this somewhere smarter than local storage

View file

@ -0,0 +1,50 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Stores where the user has scrolled to in each room
*/
class RoomScrollStateStore {
constructor() {
// 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.
this._scrollStateMap = {};
}
getScrollState(roomId) {
return this._scrollStateMap[roomId];
}
setScrollState(roomId, scrollState) {
this._scrollStateMap[roomId] = scrollState;
}
}
if (global.mx_RoomScrollStateStore === undefined) {
global.mx_RoomScrollStateStore = new RoomScrollStateStore();
}
export default global.mx_RoomScrollStateStore;

View file

@ -30,8 +30,6 @@ const INITIAL_STATE = {
// The event to scroll to when the room is first viewed // The event to scroll to when the room is first viewed
initialEventId: null, initialEventId: null,
// The offset to display the initial event at (see scrollStateMap)
initialEventPixelOffset: null,
// Whether to highlight the initial event // Whether to highlight the initial event
isInitialEventHighlighted: false, isInitialEventHighlighted: false,
@ -41,20 +39,6 @@ const INITIAL_STATE = {
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: {},
forwardingEvent: null, forwardingEvent: null,
}; };
@ -115,9 +99,6 @@ 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;
case 'forward_event': case 'forward_event':
this._setState({ this._setState({
forwardingEvent: payload.event, forwardingEvent: payload.event,
@ -132,7 +113,6 @@ class RoomViewStore extends Store {
roomId: payload.room_id, roomId: payload.room_id,
roomAlias: payload.room_alias, roomAlias: payload.room_alias,
initialEventId: payload.event_id, initialEventId: payload.event_id,
initialEventPixelOffset: undefined,
isInitialEventHighlighted: payload.highlighted, isInitialEventHighlighted: payload.highlighted,
forwardingEvent: null, forwardingEvent: null,
roomLoading: false, roomLoading: false,
@ -145,16 +125,6 @@ class RoomViewStore extends Store {
newState.joining = false; newState.joining = false;
} }
// If an event ID wasn't specified, default to the one saved for this room
// via update_scroll_state. Assume initialEventPixelOffset should be set.
if (!newState.initialEventId) {
const roomScrollState = this._state.scrollStateMap[payload.room_id];
if (roomScrollState) {
newState.initialEventId = roomScrollState.focussedEvent;
newState.initialEventPixelOffset = roomScrollState.pixelOffset;
}
}
if (this._state.forwardingEvent) { if (this._state.forwardingEvent) {
dis.dispatch({ dis.dispatch({
action: 'send_event', action: 'send_event',
@ -241,15 +211,6 @@ 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);
} }
@ -264,11 +225,6 @@ class RoomViewStore extends Store {
return this._state.initialEventId; return this._state.initialEventId;
} }
// The offset to display the initial event at (see scrollStateMap)
getInitialEventPixelOffset() {
return this._state.initialEventPixelOffset;
}
// Whether to highlight the initial event // Whether to highlight the initial event
isInitialEventHighlighted() { isInitialEventHighlighted() {
return this._state.isInitialEventHighlighted; return this._state.isInitialEventHighlighted;

View file

@ -234,6 +234,7 @@ describe('TimelinePanel', function() {
// 5 times, and we should have given up paginating // 5 times, and we should have given up paginating
expect(client.paginateEventTimeline.callCount).toEqual(5); expect(client.paginateEventTimeline.callCount).toEqual(5);
expect(messagePanel.props.backPaginating).toBe(false); expect(messagePanel.props.backPaginating).toBe(false);
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
// now, if we update the events, there shouldn't be any // now, if we update the events, there shouldn't be any
// more requests. // more requests.
@ -338,6 +339,7 @@ describe('TimelinePanel', function() {
awaitScroll().then(() => { awaitScroll().then(() => {
// we should now have loaded the first few events // we should now have loaded the first few events
expect(messagePanel.props.backPaginating).toBe(false); expect(messagePanel.props.backPaginating).toBe(false);
expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
// back-paginate until we hit the start // back-paginate until we hit the start
return backPaginate(); return backPaginate();
@ -345,6 +347,7 @@ describe('TimelinePanel', function() {
// hopefully, we got to the start of the timeline // hopefully, we got to the start of the timeline
expect(messagePanel.props.backPaginating).toBe(false); expect(messagePanel.props.backPaginating).toBe(false);
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
var events = scryEventTiles(panel); var events = scryEventTiles(panel);
expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]); expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);