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) {
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

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;
// 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 = {

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]);