Merge remote-tracking branch 'origin/t3chguy/on_copy_tooltip' into t3chguy/on_copy_tooltip
This commit is contained in:
commit
fea7af11b4
16 changed files with 342 additions and 339 deletions
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
|
||||||
this._updateScrollMap(this.state.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState, () => {
|
|
||||||
// At this point, this.state.roomId could be null (e.g. the alias might not
|
|
||||||
// have been resolved yet) so anything called here must handle this case.
|
// have been resolved yet) so anything called here must handle this case.
|
||||||
|
|
||||||
|
// We pass the new state into this function for it to read: it needs to
|
||||||
|
// observe the new state but we don't want to put it in the setState
|
||||||
|
// callback because this would prevent the setStates from being batched,
|
||||||
|
// ie. cause it to render RoomView twice rather than the once that is necessary.
|
||||||
if (initial) {
|
if (initial) {
|
||||||
this._onHaveRoom();
|
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;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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 }
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
// fast path: simple regex to detect strings that don't contain
|
||||||
|
// emoji and just return them
|
||||||
|
if (containsEmoji(children)) {
|
||||||
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
restProps.dangerouslySetInnerHTML = emojifyText(children);
|
||||||
return React.createElement(element, restProps);
|
return React.createElement(element, restProps);
|
||||||
|
} else {
|
||||||
|
return React.createElement(element, restProps, children);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EmojiText.propTypes = {
|
EmojiText.propTypes = {
|
||||||
|
|
|
@ -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
|
||||||
|
|
50
src/stores/RoomScrollStateStore.js
Normal file
50
src/stores/RoomScrollStateStore.js
Normal 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;
|
|
@ -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;
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue