Merge remote-tracking branch 'origin/develop' into dbkr/group_userlist
This commit is contained in:
commit
3c3328c5f1
55 changed files with 1747 additions and 3616 deletions
|
@ -81,10 +81,6 @@ export default React.createClass({
|
|||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
// _scrollStateMap is a map from room id to the scroll state returned by
|
||||
// RoomView.getScrollState()
|
||||
this._scrollStateMap = {};
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
|
@ -116,10 +112,6 @@ export default React.createClass({
|
|||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
|
||||
getScrollStateForRoom: function(roomId) {
|
||||
return this._scrollStateMap[roomId];
|
||||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
return true;
|
||||
|
@ -248,7 +240,6 @@ export default React.createClass({
|
|||
opacity={this.props.middleOpacity}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
scrollStateMap={this._scrollStateMap}
|
||||
/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||
break;
|
||||
|
|
|
@ -38,7 +38,6 @@ import linkifyMatrix from "../../linkify-matrix";
|
|||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
require('../../stores/LifecycleStore');
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
|
@ -214,9 +213,6 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
|
||||
this._onRoomViewStoreUpdated();
|
||||
|
||||
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
||||
|
||||
// Used by _viewRoom before getting state from sync
|
||||
|
@ -353,7 +349,6 @@ module.exports = React.createClass({
|
|||
UDEHandler.stopListening();
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this._roomViewStoreToken.remove();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
@ -587,10 +582,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdated: function() {
|
||||
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
|
||||
},
|
||||
|
||||
_setPage: function(pageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
|
@ -677,6 +668,7 @@ module.exports = React.createClass({
|
|||
this.focusComposer = true;
|
||||
|
||||
const newState = {
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
thirdPartyInvite: roomInfo.third_party_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
|
|
|
@ -61,6 +61,9 @@ module.exports = React.createClass({
|
|||
// for pending messages.
|
||||
ourUserId: React.PropTypes.string,
|
||||
|
||||
// true to suppress the date at the start of the timeline
|
||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
|
||||
// whether to show read receipts
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
|
||||
|
@ -514,10 +517,10 @@ module.exports = React.createClass({
|
|||
|
||||
_wantsDateSeparator: function(prevEvent, nextEventDate) {
|
||||
if (prevEvent == null) {
|
||||
// First event in the panel always wants a DateSeparator
|
||||
return true;
|
||||
// first event in the panel: depends if we could back-paginate from
|
||||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
|
||||
const prevEventDate = prevEvent.getDate();
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
|
|
|
@ -47,6 +47,7 @@ import KeyCode from '../../KeyCode';
|
|||
import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
|
||||
let DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -156,6 +157,22 @@ module.exports = React.createClass({
|
|||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
|
||||
// RoomView explicitly does not support changing what room
|
||||
// is being viewed: instead it should just be re-mounted when
|
||||
// switching rooms. Therefore, if the room ID changes, we
|
||||
// ignore this. We either need to do this or add code to handle
|
||||
// saving the scroll position (otherwise we end up saving the
|
||||
// scroll position against the wrong room).
|
||||
|
||||
// Given that doing the setState here would cause a bunch of
|
||||
// unnecessary work, we just ignore the change since we know
|
||||
// that if the current room ID has changed from what we thought
|
||||
// it was, it means we're about to be unmounted.
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
|
@ -163,7 +180,6 @@ module.exports = React.createClass({
|
|||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
joining: RoomViewStore.isJoining(),
|
||||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
|
@ -189,6 +205,25 @@ module.exports = React.createClass({
|
|||
// the RoomView instance
|
||||
if (initial) {
|
||||
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
|
||||
|
@ -197,22 +232,20 @@ module.exports = React.createClass({
|
|||
newState.searchResults = null;
|
||||
}
|
||||
|
||||
// Store the scroll state for the previous room so that we can return to this
|
||||
// position when viewing this room in future.
|
||||
if (this.state.roomId !== newState.roomId) {
|
||||
this._updateScrollMap(this.state.roomId);
|
||||
}
|
||||
this.setState(newState);
|
||||
// At this point, newState.roomId could be null (e.g. the alias might not
|
||||
// have been resolved yet) so anything called here must handle this case.
|
||||
|
||||
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.
|
||||
if (initial) {
|
||||
this._onHaveRoom();
|
||||
}
|
||||
});
|
||||
// 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) {
|
||||
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:
|
||||
// - 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)
|
||||
|
@ -228,23 +261,15 @@ module.exports = React.createClass({
|
|||
// 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,
|
||||
// making it impossible to indicate a newly joined room.
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
this.setState({
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
showApps: this._shouldShowApps(room),
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}
|
||||
if (!this.state.joining && this.state.roomId) {
|
||||
if (!joining && roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room && this.state.shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
} else if (!room && shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
|
@ -340,7 +365,9 @@ module.exports = React.createClass({
|
|||
this.unmounted = true;
|
||||
|
||||
// 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) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
|
@ -617,18 +644,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) {
|
||||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
import Promise from 'bluebird';
|
||||
var KeyCode = require('../../KeyCode');
|
||||
|
||||
|
@ -78,52 +77,111 @@ if (DEBUG_SCROLL) {
|
|||
* scroll down further. If stickyBottom is disabled, we just save the scroll
|
||||
* offset as normal.
|
||||
*/
|
||||
export default class ScrollPanel extends StickyContainer {
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ScrollPanel',
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onResize = this.onResize.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
}
|
||||
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,
|
||||
|
||||
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.resetScrollState();
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.checkFillState();
|
||||
}
|
||||
componentDidMount: function() {
|
||||
this.checkScroll();
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate: function() {
|
||||
// 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
|
||||
// adding events to the top).
|
||||
//
|
||||
// This will also re-check the fill state, in case the paginate was inadequate
|
||||
this.checkScroll();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
componentWillUnmount: function() {
|
||||
// set a boolean to say we've been unmounted, which any pending
|
||||
// promises can use to throw away their results.
|
||||
//
|
||||
// (We could use isMounted(), but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
}
|
||||
},
|
||||
|
||||
onScroll(ev) {
|
||||
onScroll: function(ev) {
|
||||
var sn = this._getScrollNode();
|
||||
debuglog("Scroll event: offset now:", sn.scrollTop,
|
||||
"_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
|
||||
// ignored. (Or rather, it is successfully written, but on the next
|
||||
// scroll event, it's been reset again).
|
||||
|
@ -159,27 +217,27 @@ export default class ScrollPanel extends StickyContainer {
|
|||
this.props.onScroll(ev);
|
||||
|
||||
this.checkFillState();
|
||||
}
|
||||
},
|
||||
|
||||
onResize() {
|
||||
onResize: function() {
|
||||
this.props.onResize();
|
||||
this.checkScroll();
|
||||
this.refs.geminiPanel.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
// 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.
|
||||
checkScroll() {
|
||||
checkScroll: function() {
|
||||
this._restoreSavedScrollState();
|
||||
this.checkFillState();
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
// about whether the the content is scrolled down right now, irrespective of
|
||||
// whether it will stay that way when the children update.
|
||||
isAtBottom() {
|
||||
isAtBottom: function() {
|
||||
var sn = this._getScrollNode();
|
||||
|
||||
// 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.
|
||||
|
||||
return sn.scrollHeight - Math.ceil(sn.scrollTop) <= sn.clientHeight + 3;
|
||||
}
|
||||
},
|
||||
|
||||
// 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
|
||||
|
@ -222,17 +280,17 @@ export default class ScrollPanel extends StickyContainer {
|
|||
// |#########| - |
|
||||
// |#########| |
|
||||
// `---------' -
|
||||
_getExcessHeight(backwards) {
|
||||
_getExcessHeight: function(backwards) {
|
||||
var sn = this._getScrollNode();
|
||||
if (backwards) {
|
||||
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||
} else {
|
||||
return sn.scrollHeight - (sn.scrollTop + 2*sn.clientHeight) - UNPAGINATION_PADDING;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState() {
|
||||
checkFillState: function() {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -271,10 +329,10 @@ export default class ScrollPanel extends StickyContainer {
|
|||
// need to forward-fill
|
||||
this._maybeFill(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// check if unfilling is possible and send an unfill request if necessary
|
||||
_checkUnfillState(backwards) {
|
||||
_checkUnfillState: function(backwards) {
|
||||
let excessHeight = this._getExcessHeight(backwards);
|
||||
if (excessHeight <= 0) {
|
||||
return;
|
||||
|
@ -315,10 +373,10 @@ export default class ScrollPanel extends StickyContainer {
|
|||
this.props.onUnfillRequest(backwards, markerScrollToken);
|
||||
}, UNFILL_REQUEST_DEBOUNCE_MS);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill(backwards) {
|
||||
_maybeFill: function(backwards) {
|
||||
var dir = backwards ? 'b' : 'f';
|
||||
if (this._pendingFillRequests[dir]) {
|
||||
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||
|
@ -350,7 +408,7 @@ export default class ScrollPanel extends StickyContainer {
|
|||
this.checkFillState();
|
||||
}
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
|
||||
/* get the current scroll state. This returns an object with the following
|
||||
* properties:
|
||||
|
@ -366,9 +424,9 @@ export default class ScrollPanel extends StickyContainer {
|
|||
* the number of pixels the bottom of the tracked child is above the
|
||||
* bottom of the scroll panel.
|
||||
*/
|
||||
getScrollState() {
|
||||
getScrollState: function() {
|
||||
return this.scrollState;
|
||||
}
|
||||
},
|
||||
|
||||
/* 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
|
||||
* child list.)
|
||||
*/
|
||||
resetScrollState() {
|
||||
resetScrollState: function() {
|
||||
this.scrollState = {stuckAtBottom: this.props.startAtBottom};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop() {
|
||||
scrollToTop: function() {
|
||||
this._setScrollTop(0);
|
||||
this._saveScrollState();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom() {
|
||||
scrollToBottom: function() {
|
||||
// the easiest way to make sure that the scroll state is correctly
|
||||
// saved is to do the scroll, then save the updated state. (Calculating
|
||||
// it ourselves is hard, and we can't rely on an onScroll callback
|
||||
// happening, since there may be no user-visible change here).
|
||||
this._setScrollTop(Number.MAX_VALUE);
|
||||
this._saveScrollState();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative(mult) {
|
||||
scrollRelative: function(mult) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
var delta = mult * scrollNode.clientHeight * 0.5;
|
||||
this._setScrollTop(scrollNode.scrollTop + delta);
|
||||
this._saveScrollState();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
*/
|
||||
handleScrollKey(ev) {
|
||||
handleScrollKey: function(ev) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.PAGE_UP:
|
||||
if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
|
@ -447,7 +505,7 @@ export default class ScrollPanel extends StickyContainer {
|
|||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* Scroll the panel to bring the DOM node with the scroll token
|
||||
* `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
|
||||
* defaults to 0.
|
||||
*/
|
||||
scrollToToken(scrollToken, pixelOffset, offsetBase) {
|
||||
scrollToToken: function(scrollToken, pixelOffset, offsetBase) {
|
||||
pixelOffset = pixelOffset || 0;
|
||||
offsetBase = offsetBase || 0;
|
||||
|
||||
|
@ -482,11 +540,11 @@ export default class ScrollPanel extends StickyContainer {
|
|||
|
||||
// ... then make it so.
|
||||
this._restoreSavedScrollState();
|
||||
}
|
||||
},
|
||||
|
||||
// set the scrollTop attribute appropriately to position the given child at the
|
||||
// given offset in the window. A helper for _restoreSavedScrollState.
|
||||
_scrollToToken(scrollToken, pixelOffset) {
|
||||
_scrollToToken: function(scrollToken, pixelOffset) {
|
||||
/* find the dom node with the right scrolltoken */
|
||||
var node;
|
||||
var messages = this.refs.itemlist.children;
|
||||
|
@ -518,9 +576,9 @@ export default class ScrollPanel extends StickyContainer {
|
|||
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
_saveScrollState() {
|
||||
_saveScrollState: function() {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("ScrollPanel: Saved scroll state", this.scrollState);
|
||||
|
@ -558,9 +616,9 @@ export default class ScrollPanel extends StickyContainer {
|
|||
} else {
|
||||
debuglog("ScrollPanel: unable to save scroll state: found no children in the viewport");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_restoreSavedScrollState() {
|
||||
_restoreSavedScrollState: function() {
|
||||
var scrollState = this.scrollState;
|
||||
var scrollNode = this._getScrollNode();
|
||||
|
||||
|
@ -570,9 +628,9 @@ export default class ScrollPanel extends StickyContainer {
|
|||
this._scrollToToken(scrollState.trackedScrollToken,
|
||||
scrollState.pixelOffset);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_setScrollTop(scrollTop) {
|
||||
_setScrollTop: function(scrollTop) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
|
||||
var prevScroll = scrollNode.scrollTop;
|
||||
|
@ -594,12 +652,12 @@ export default class ScrollPanel extends StickyContainer {
|
|||
debuglog("ScrollPanel: set scrollTop:", scrollNode.scrollTop,
|
||||
"requested:", scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
}
|
||||
},
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
* message panel.
|
||||
*/
|
||||
_getScrollNode() {
|
||||
_getScrollNode: function() {
|
||||
if (this.unmounted) {
|
||||
// this shouldn't happen, but when it does, turn the NPE into
|
||||
// something more meaningful.
|
||||
|
@ -607,91 +665,21 @@ export default class ScrollPanel extends StickyContainer {
|
|||
}
|
||||
|
||||
return this.refs.geminiPanel.scrollbar.getViewElement();
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
render: function() {
|
||||
// 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.
|
||||
// it's not obvious why we have a separate div and ol anyway.
|
||||
return (
|
||||
<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{this.props.children}
|
||||
</ol>
|
||||
</div>
|
||||
</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() {},
|
||||
};
|
||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel"
|
||||
onScroll={this.onScroll} onResize={this.onResize}
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{this.props.children}
|
||||
</ol>
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1147,6 +1147,7 @@ var TimelinePanel = React.createClass({
|
|||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview={ this.props.showUrlPreview }
|
||||
showReadReceipts={ this.props.showReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
|
|
|
@ -729,6 +729,7 @@ module.exports = React.createClass({
|
|||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
this._syncedSettings[setting.id] = setting.value;
|
||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||
}
|
||||
dis.dispatch({
|
||||
|
@ -741,7 +742,7 @@ module.exports = React.createClass({
|
|||
type="radio"
|
||||
name={ setting.id }
|
||||
value={ setting.value }
|
||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||
checked={ this._syncedSettings[setting.id] === setting.value }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
<label htmlFor={ setting.id + "_" + setting.value }>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
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.
|
||||
|
@ -15,12 +16,19 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {emojifyText} from '../../../HtmlUtils';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(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 = {
|
||||
|
|
|
@ -31,6 +31,7 @@ import dis from '../../../dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import UserSettingsStore from "../../../UserSettingsStore";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import {RoomMember} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -72,12 +73,16 @@ module.exports = React.createClass({
|
|||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
let successful = false;
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.log('Unable to copy');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
return successful;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -113,14 +118,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
}, 10);
|
||||
}
|
||||
// add event handlers to the 'copy code' buttons
|
||||
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];
|
||||
this.copyToClipboard(copyCode.textContent);
|
||||
};
|
||||
}
|
||||
this._addCodeCopyButton();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -257,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) {
|
||||
this.setState({ widgetHidden: true });
|
||||
// FIXME: persist this somewhere smarter than local storage
|
||||
|
|
|
@ -63,7 +63,6 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
currentRoom: React.PropTypes.string,
|
||||
searchFilter: React.PropTypes.string,
|
||||
},
|
||||
|
||||
|
@ -88,6 +87,7 @@ module.exports = React.createClass({
|
|||
cli.on("Room.receipt", this.onRoomReceipt);
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
cli.on("Event.decrypted", this.onEventDecrypted);
|
||||
cli.on("accountData", this.onAccountData);
|
||||
|
||||
this.refreshRoomList();
|
||||
|
@ -155,6 +155,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
|
@ -224,6 +225,11 @@ module.exports = React.createClass({
|
|||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onEventDecrypted: function(ev) {
|
||||
// An event being decrypted may mean we need to re-order the room list
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onAccountData: function(ev) {
|
||||
if (ev.getType() == 'm.direct') {
|
||||
this._delayedRefreshRoomList();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -27,6 +28,8 @@ var RoomNotifs = require('../../../RoomNotifs');
|
|||
var FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomTile',
|
||||
|
@ -39,7 +42,6 @@ module.exports = React.createClass({
|
|||
|
||||
room: React.PropTypes.object.isRequired,
|
||||
collapsed: React.PropTypes.bool.isRequired,
|
||||
selected: React.PropTypes.bool.isRequired,
|
||||
unread: React.PropTypes.bool.isRequired,
|
||||
highlight: React.PropTypes.bool.isRequired,
|
||||
isInvite: React.PropTypes.bool.isRequired,
|
||||
|
@ -58,6 +60,7 @@ module.exports = React.createClass({
|
|||
badgeHover : false,
|
||||
menuDisplayed: false,
|
||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -87,8 +90,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onActiveRoomChange: function() {
|
||||
this.setState({
|
||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -96,6 +106,7 @@ module.exports = React.createClass({
|
|||
if (cli) {
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
|
@ -174,7 +185,7 @@ module.exports = React.createClass({
|
|||
|
||||
var classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.props.selected,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
|
@ -221,7 +232,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
if (this.props.selected) {
|
||||
if (this.state.selected) {
|
||||
let nameSelected = <EmojiText>{name}</EmojiText>;
|
||||
|
||||
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;
|
||||
|
|
97
src/components/views/voip/CallPreview.js
Normal file
97
src/components/views/voip/CallPreview.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CallPreview',
|
||||
|
||||
propTypes: {
|
||||
// A Conference Handler implementation
|
||||
// Must have a function signature:
|
||||
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||
ConferenceHandler: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
activeCall: CallHandler.getAnyActiveCall(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(payload) {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
_onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
// listen for call state changes to prod the render method, which
|
||||
// may hide the global CallView if the call it is tracking is dead
|
||||
case 'call_state':
|
||||
this.setState({
|
||||
activeCall: CallHandler.getAnyActiveCall(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_onCallViewClick: function() {
|
||||
const call = CallHandler.getAnyActiveCall();
|
||||
if (call) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: call.groupRoomId || call.roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
|
||||
const showCall = (this.state.activeCall && this.state.activeCall.call_state === 'connected' && !callForRoom);
|
||||
|
||||
if (showCall) {
|
||||
const CallView = sdk.getComponent('voip.CallView');
|
||||
return (
|
||||
<CallView
|
||||
className="mx_LeftPanel_callView" showVoice={true} onClick={this._onCallViewClick}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue