Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup
This commit is contained in:
commit
c6e56d98b3
59 changed files with 1164 additions and 371 deletions
|
@ -525,6 +525,7 @@ const LoggedInView = createReactClass({
|
|||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const ToastContainer = sdk.getComponent('structures.ToastContainer');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
|
@ -628,6 +629,7 @@ const LoggedInView = createReactClass({
|
|||
return (
|
||||
<div onPaste={this._onPaste} onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
|
|
|
@ -60,6 +60,7 @@ import { countRoomsWithNotif } from '../../RoomNotifs';
|
|||
import { ThemeWatcher } from "../../theme";
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer } from "../../utils/promise";
|
||||
import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
const VIEWS = {
|
||||
|
@ -626,6 +627,22 @@ export default createReactClass({
|
|||
case 'view_invite':
|
||||
showRoomInviteDialog(payload.roomId);
|
||||
break;
|
||||
case 'view_last_screen':
|
||||
// This function does what we want, despite the name. The idea is that it shows
|
||||
// the last room we were looking at or some reasonable default/guess. We don't
|
||||
// have to worry about email invites or similar being re-triggered because the
|
||||
// function will have cleared that state and not execute that path.
|
||||
this._showScreenAfterLogin();
|
||||
break;
|
||||
case 'toggle_my_groups':
|
||||
// We just dispatch the page change rather than have to worry about
|
||||
// what the logic is for each of these branches.
|
||||
if (this.state.page_type === PageTypes.MyGroups) {
|
||||
dis.dispatch({action: 'view_last_screen'});
|
||||
} else {
|
||||
dis.dispatch({action: 'view_my_groups'});
|
||||
}
|
||||
break;
|
||||
case 'notifier_enabled': {
|
||||
this.setState({showNotifierToolbar: Notifier.shouldShowToolbar()});
|
||||
}
|
||||
|
@ -1264,7 +1281,6 @@ export default createReactClass({
|
|||
this.firstSyncComplete = false;
|
||||
this.firstSyncPromise = defer();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
|
||||
|
||||
// Allow the JS SDK to reap timeline events. This reduces the amount of
|
||||
// memory consumed as the JS SDK stores multiple distinct copies of room
|
||||
|
@ -1309,7 +1325,7 @@ export default createReactClass({
|
|||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
return;
|
||||
}
|
||||
console.log("MatrixClient sync state => %s", state);
|
||||
console.info("MatrixClient sync state => %s", state);
|
||||
if (state !== "PREPARED") { return; }
|
||||
|
||||
self.firstSyncComplete = true;
|
||||
|
@ -1463,12 +1479,35 @@ export default createReactClass({
|
|||
}
|
||||
});
|
||||
|
||||
cli.on("crypto.verification.start", (verifier) => {
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
});
|
||||
});
|
||||
if (SettingsStore.isFeatureEnabled("feature_dm_verification")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
let requestObserver;
|
||||
if (request.event.getRoomId()) {
|
||||
requestObserver = new KeyVerificationStateObserver(
|
||||
request.event, MatrixClientPeg.get());
|
||||
}
|
||||
|
||||
if (!requestObserver || requestObserver.pending) {
|
||||
dis.dispatch({
|
||||
action: "show_toast",
|
||||
toast: {
|
||||
key: request.event.getId(),
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request, requestObserver},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cli.on("crypto.verification.start", (verifier) => {
|
||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
});
|
||||
});
|
||||
}
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,10 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* global Velocity */
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
@ -37,10 +35,8 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType()
|
|||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = createReactClass({
|
||||
displayName: 'MessagePanel',
|
||||
|
||||
propTypes: {
|
||||
export default class MessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
|
@ -109,17 +105,16 @@ module.exports = createReactClass({
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
},
|
||||
};
|
||||
|
||||
componentWillMount: function() {
|
||||
// the event after which we put a visible unread marker on the last
|
||||
// render cycle; null if readMarkerVisible was false or the RM was
|
||||
// suppressed (eg because it was at the end of the timeline)
|
||||
this.currentReadMarkerEventId = null;
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// the event after which we are showing a disappearing read marker
|
||||
// animation
|
||||
this.currentGhostEventId = null;
|
||||
this.state = {
|
||||
// previous positions the read marker has been in, so we can
|
||||
// display 'ghost' read markers that are animating away
|
||||
ghostReadMarkers: [],
|
||||
};
|
||||
|
||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||
// to manage its animations
|
||||
|
@ -158,47 +153,57 @@ module.exports = createReactClass({
|
|||
// displayed event in the current render cycle.
|
||||
this._readReceiptsByUserId = {};
|
||||
|
||||
// Remember the read marker ghost node so we can do the cleanup that
|
||||
// Velocity requires
|
||||
this._readMarkerGhostNode = null;
|
||||
|
||||
// Cache hidden events setting on mount since Settings is expensive to
|
||||
// query, and we check this in a hot code path.
|
||||
this._showHiddenEventsInTimeline =
|
||||
SettingsStore.getValue("showHiddenEventsInTimeline");
|
||||
|
||||
this._isMounted = true;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._isMounted = false;
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) {
|
||||
const ghostReadMarkers = this.state.ghostReadMarkers;
|
||||
ghostReadMarkers.push(prevProps.readMarkerEventId);
|
||||
this.setState({
|
||||
ghostReadMarkers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* get the DOM node representing the given event */
|
||||
getNodeForEventId: function(eventId) {
|
||||
getNodeForEventId(eventId) {
|
||||
if (!this.eventNodes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.eventNodes[eventId];
|
||||
},
|
||||
}
|
||||
|
||||
/* return true if the content is fully scrolled down right now; else false.
|
||||
*/
|
||||
isAtBottom: function() {
|
||||
isAtBottom() {
|
||||
return this.refs.scrollPanel
|
||||
&& this.refs.scrollPanel.isAtBottom();
|
||||
},
|
||||
}
|
||||
|
||||
/* get the current scroll state. See ScrollPanel.getScrollState for
|
||||
* details.
|
||||
*
|
||||
* returns null if we are not mounted.
|
||||
*/
|
||||
getScrollState: function() {
|
||||
getScrollState() {
|
||||
if (!this.refs.scrollPanel) { return null; }
|
||||
return this.refs.scrollPanel.getScrollState();
|
||||
},
|
||||
}
|
||||
|
||||
// returns one of:
|
||||
//
|
||||
|
@ -206,7 +211,7 @@ module.exports = createReactClass({
|
|||
// -1: read marker is above the window
|
||||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
getReadMarkerPosition() {
|
||||
const readMarker = this.refs.readMarkerNode;
|
||||
const messageWrapper = this.refs.scrollPanel;
|
||||
|
||||
|
@ -226,45 +231,45 @@ module.exports = createReactClass({
|
|||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop: function() {
|
||||
scrollToTop() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToTop();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom: function() {
|
||||
scrollToBottom() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToBottom();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
scrollRelative(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollRelative(mult);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
*
|
||||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
handleScrollKey(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* jump to the given event id.
|
||||
*
|
||||
|
@ -276,33 +281,33 @@ module.exports = createReactClass({
|
|||
* node (specifically, the bottom of it) will be positioned. If omitted, it
|
||||
* defaults to 0.
|
||||
*/
|
||||
scrollToEvent: function(eventId, pixelOffset, offsetBase) {
|
||||
scrollToEvent(eventId, pixelOffset, offsetBase) {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset, offsetBase);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
scrollToEventIfNeeded: function(eventId) {
|
||||
scrollToEventIfNeeded(eventId) {
|
||||
const node = this.eventNodes[eventId];
|
||||
if (node) {
|
||||
node.scrollIntoView({block: "nearest", behavior: "instant"});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/* check the scroll state and send out pagination requests if necessary.
|
||||
*/
|
||||
checkFillState: function() {
|
||||
checkFillState() {
|
||||
if (this.refs.scrollPanel) {
|
||||
this.refs.scrollPanel.checkFillState();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_isUnmounting: function() {
|
||||
_isUnmounting() {
|
||||
return !this._isMounted;
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
_shouldShowEvent: function(mxEv) {
|
||||
_shouldShowEvent(mxEv) {
|
||||
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
@ -320,16 +325,87 @@ module.exports = createReactClass({
|
|||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv);
|
||||
},
|
||||
}
|
||||
|
||||
_getEventTiles: function() {
|
||||
_readMarkerForEvent(eventId, isLastEvent) {
|
||||
const visible = !isLastEvent && this.props.readMarkerVisible;
|
||||
|
||||
if (this.props.readMarkerEventId === eventId) {
|
||||
let hr;
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
// don't have useful event ids), we don't want to show it, but
|
||||
// we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't
|
||||
// confused.
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={"readMarker_"+eventId} ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
} else if (this.state.ghostReadMarkers.includes(eventId)) {
|
||||
// We render 'ghost' read markers in the DOM while they
|
||||
// transition away. This allows the actual read marker
|
||||
// to be in the right place straight away without having
|
||||
// to wait for the transition to finish.
|
||||
// There are probably much simpler ways to do this transition,
|
||||
// possibly using react-transition-group which handles keeping
|
||||
// elements in the DOM whilst they transition out, although our
|
||||
// case is a little more complex because only some of the items
|
||||
// transition (ie. the read markers do but the event tiles do not)
|
||||
// and TransitionGroup requires that all its children are Transitions.
|
||||
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||
ref={this._collectGhostReadMarker}
|
||||
onTransitionEnd={this._onGhostTransitionEnd}
|
||||
data-eventid={eventId}
|
||||
/>;
|
||||
|
||||
// give it a key which depends on the event id. That will ensure that
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_"+eventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_collectGhostReadMarker = (node) => {
|
||||
if (node) {
|
||||
// now the element has appeared, change the style which will trigger the CSS transition
|
||||
requestAnimationFrame(() => {
|
||||
node.style.width = '10%';
|
||||
node.style.opacity = '0';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onGhostTransitionEnd = (ev) => {
|
||||
// we can now clean up the ghost element
|
||||
const finishedEventId = ev.target.dataset.eventid;
|
||||
this.setState({
|
||||
ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId),
|
||||
});
|
||||
};
|
||||
|
||||
_getEventTiles() {
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
this.eventNodes = {};
|
||||
|
||||
let visible = false;
|
||||
let i;
|
||||
|
||||
// first figure out which is the last event in the list which we're
|
||||
|
@ -364,16 +440,6 @@ module.exports = createReactClass({
|
|||
|
||||
let prevEvent = null; // the last event we showed
|
||||
|
||||
// assume there is no read marker until proven otherwise
|
||||
let readMarkerVisible = false;
|
||||
|
||||
// if the readmarker has moved, cancel any active ghost.
|
||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||
this.props.readMarkerVisible &&
|
||||
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
|
||||
this.currentGhostEventId = null;
|
||||
}
|
||||
|
||||
this._readReceiptsByEvent = {};
|
||||
if (this.props.showReadReceipts) {
|
||||
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
|
||||
|
@ -398,7 +464,7 @@ module.exports = createReactClass({
|
|||
return false;
|
||||
};
|
||||
if (mxEv.getType() === "m.room.create") {
|
||||
let readMarkerInSummary = false;
|
||||
let summaryReadMarker = null;
|
||||
const ts1 = mxEv.getTs();
|
||||
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
|
@ -407,8 +473,12 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
// If RM event is the first in the summary, append the RM after the summary
|
||||
if (mxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInSummary = true;
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
|
||||
|
||||
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||
if (this._shouldShowEvent(mxEv)) {
|
||||
// pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
|
||||
ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
|
||||
}
|
||||
|
||||
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
|
||||
|
@ -418,9 +488,7 @@ module.exports = createReactClass({
|
|||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
// If this hidden event is the RM and in or at end of a summary put RM after the summary.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInSummary = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -429,9 +497,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInSummary = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
@ -459,8 +525,8 @@ module.exports = createReactClass({
|
|||
{ eventTiles }
|
||||
</EventListSummary>);
|
||||
|
||||
if (readMarkerInSummary) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
if (summaryReadMarker) {
|
||||
ret.push(summaryReadMarker);
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
|
@ -471,7 +537,7 @@ module.exports = createReactClass({
|
|||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && wantTile) {
|
||||
let readMarkerInMels = false;
|
||||
let summaryReadMarker = null;
|
||||
const ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
|
@ -489,9 +555,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
// If RM event is the first in the MELS, append the RM after MELS
|
||||
if (mxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
|
||||
|
||||
const summarisedEvents = [mxEv];
|
||||
for (;i + 1 < this.props.events.length; i++) {
|
||||
|
@ -500,9 +564,7 @@ module.exports = createReactClass({
|
|||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -512,9 +574,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
||||
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
@ -545,8 +605,8 @@ module.exports = createReactClass({
|
|||
{ eventTiles }
|
||||
</MemberEventListSummary>);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
if (summaryReadMarker) {
|
||||
ret.push(summaryReadMarker);
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
|
@ -561,44 +621,14 @@ module.exports = createReactClass({
|
|||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
let isVisibleReadMarker = false;
|
||||
|
||||
if (eventId === this.props.readMarkerEventId) {
|
||||
visible = this.props.readMarkerVisible;
|
||||
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
// don't have useful event ids), we don't want to show it, but
|
||||
// we still want to create the <li/> for it so that the
|
||||
// algorithms which depend on its position on the screen aren't
|
||||
// confused.
|
||||
if (i >= lastShownNonLocalEchoIndex) {
|
||||
visible = false;
|
||||
}
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
readMarkerVisible = visible;
|
||||
isVisibleReadMarker = visible;
|
||||
}
|
||||
|
||||
// XXX: there should be no need for a ghost tile - we should just use a
|
||||
// a dispatch (user_activity_end) to start the RM animation.
|
||||
if (eventId === this.currentGhostEventId) {
|
||||
// if we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
} else if (!isVisibleReadMarker &&
|
||||
eventId === this.currentReadMarkerEventId) {
|
||||
// there is currently a read-up-to marker at this point, but no
|
||||
// more. Show an animation of it disappearing.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
this.currentGhostEventId = eventId;
|
||||
}
|
||||
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
|
||||
if (readMarker) ret.push(readMarker);
|
||||
}
|
||||
|
||||
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
|
||||
return ret;
|
||||
},
|
||||
}
|
||||
|
||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||
_getTilesForEvent(prevEvent, mxEv, last) {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const ret = [];
|
||||
|
@ -691,20 +721,20 @@ module.exports = createReactClass({
|
|||
);
|
||||
|
||||
return ret;
|
||||
},
|
||||
}
|
||||
|
||||
_wantsDateSeparator: function(prevEvent, nextEventDate) {
|
||||
_wantsDateSeparator(prevEvent, nextEventDate) {
|
||||
if (prevEvent == null) {
|
||||
// first event in the panel: depends if we could back-paginate from
|
||||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||
},
|
||||
}
|
||||
|
||||
// Get a list of read receipts that should be shown next to this event
|
||||
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
|
||||
_getReadReceiptsForEvent: function(event) {
|
||||
_getReadReceiptsForEvent(event) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
// get list of read receipts, sorted most recent first
|
||||
|
@ -728,12 +758,12 @@ module.exports = createReactClass({
|
|||
});
|
||||
});
|
||||
return receipts;
|
||||
},
|
||||
}
|
||||
|
||||
// Get an object that maps from event ID to a list of read receipts that
|
||||
// should be shown next to that event. If a hidden event has read receipts,
|
||||
// they are folded into the receipts of the last shown event.
|
||||
_getReadReceiptsByShownEvent: function() {
|
||||
_getReadReceiptsByShownEvent() {
|
||||
const receiptsByEvent = {};
|
||||
const receiptsByUserId = {};
|
||||
|
||||
|
@ -786,78 +816,31 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
return receiptsByEvent;
|
||||
},
|
||||
}
|
||||
|
||||
_getReadMarkerTile: function(visible) {
|
||||
let hr;
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key="_readupto" ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
||||
_startAnimation: function(ghostNode) {
|
||||
if (this._readMarkerGhostNode) {
|
||||
Velocity.Utilities.removeData(this._readMarkerGhostNode);
|
||||
}
|
||||
this._readMarkerGhostNode = ghostNode;
|
||||
|
||||
if (ghostNode) {
|
||||
// eslint-disable-next-line new-cap
|
||||
Velocity(ghostNode, {opacity: '0', width: '10%'},
|
||||
{duration: 400, easing: 'easeInSine',
|
||||
delay: 1000});
|
||||
}
|
||||
},
|
||||
|
||||
_getReadMarkerGhostTile: function() {
|
||||
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
ref={this._startAnimation}
|
||||
/>;
|
||||
|
||||
// give it a key which depends on the event id. That will ensure that
|
||||
// we get a new DOM node (restarting the animation) when the ghost
|
||||
// moves to a different event.
|
||||
return (
|
||||
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
|
||||
_collectEventNode: function(eventId, node) {
|
||||
_collectEventNode = (eventId, node) => {
|
||||
this.eventNodes[eventId] = node;
|
||||
},
|
||||
}
|
||||
|
||||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
_onHeightChanged: function() {
|
||||
_onHeightChanged = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onTypingShown: function() {
|
||||
_onTypingShown = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
// this will make the timeline grow, so checkScroll
|
||||
scrollPanel.checkScroll();
|
||||
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
|
||||
scrollPanel.preventShrinking();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onTypingHidden: function() {
|
||||
_onTypingHidden = () => {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
// as hiding the typing notifications doesn't
|
||||
|
@ -868,9 +851,9 @@ module.exports = createReactClass({
|
|||
// reveal added padding to balance the notifs disappearing.
|
||||
scrollPanel.checkScroll();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
updateTimelineMinHeight: function() {
|
||||
updateTimelineMinHeight() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
|
||||
if (scrollPanel) {
|
||||
|
@ -885,16 +868,16 @@ module.exports = createReactClass({
|
|||
scrollPanel.preventShrinking();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onTimelineReset: function() {
|
||||
onTimelineReset() {
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.clearPreventShrinking();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
@ -941,5 +924,5 @@ module.exports = createReactClass({
|
|||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -357,7 +357,7 @@ module.exports = createReactClass({
|
|||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room && shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", roomId);
|
||||
console.info("Attempting to peek into room %s", roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
isPeeking: true, // this will change to false if peeking fails
|
||||
|
@ -1896,7 +1896,7 @@ module.exports = createReactClass({
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
|
|
84
src/components/structures/ToastContainer.js
Normal file
84
src/components/structures/ToastContainer.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 * as React from "react";
|
||||
import dis from "../../dispatcher";
|
||||
import { _t } from '../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: []};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
if (payload.action === "show_toast") {
|
||||
this._addToast(payload.toast);
|
||||
}
|
||||
};
|
||||
|
||||
_addToast(toast) {
|
||||
this.setState({toasts: this.state.toasts.concat(toast)});
|
||||
}
|
||||
|
||||
dismissTopToast = () => {
|
||||
const [, ...remaining] = this.state.toasts;
|
||||
this.setState({toasts: remaining});
|
||||
};
|
||||
|
||||
render() {
|
||||
const totalCount = this.state.toasts.length;
|
||||
const isStacked = totalCount > 1;
|
||||
let toast;
|
||||
if (totalCount !== 0) {
|
||||
const topToast = this.state.toasts[0];
|
||||
const {title, icon, key, component, props} = topToast;
|
||||
const toastClasses = classNames("mx_Toast_toast", {
|
||||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
dismiss: this.dismissTopToast,
|
||||
key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
const containerClasses = classNames("mx_ToastContainer", {
|
||||
"mx_ToastContainer_stacked": isStacked,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={containerClasses} role="alert">
|
||||
{toast}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -89,7 +89,7 @@ module.exports = createReactClass({
|
|||
+ "authentication");
|
||||
}
|
||||
|
||||
console.log("Rendering to %s", divId);
|
||||
console.info("Rendering to %s", divId);
|
||||
this._captchaWidgetId = global.grecaptcha.render(divId, {
|
||||
sitekey: publicKey,
|
||||
callback: this.props.onCaptchaResponse,
|
||||
|
|
|
@ -36,6 +36,7 @@ import classNames from 'classnames';
|
|||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import {createMenu} from "../../structures/ContextualMenu";
|
||||
import PersistedElement from "./PersistedElement";
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -247,7 +248,8 @@ export default class AppTile extends React.Component {
|
|||
this.setScalarToken();
|
||||
}
|
||||
} else if (nextProps.show && !this.props.show) {
|
||||
if (this.props.waitForIframeLoad) {
|
||||
// We assume that persisted widgets are loaded and don't need a spinner.
|
||||
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
|
||||
this.setState({
|
||||
loading: true,
|
||||
});
|
||||
|
@ -362,7 +364,7 @@ export default class AppTile extends React.Component {
|
|||
}
|
||||
|
||||
_onRevokeClicked() {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
console.info("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
|
||||
|
@ -652,12 +654,7 @@ export default class AppTile extends React.Component {
|
|||
appTileBody = (
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
"allow" attribute, which is unknown to react 15.
|
||||
*/ }
|
||||
<iframe
|
||||
is
|
||||
allow={iframeFeatures}
|
||||
ref="appFrame"
|
||||
src={this._getSafeUrl()}
|
||||
|
|
28
src/components/views/elements/FormButton.js
Normal file
28
src/components/views/elements/FormButton.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 AccessibleButton from "./AccessibleButton";
|
||||
|
||||
export default function FormButton(props) {
|
||||
const {className, label, kind, ...restProps} = props;
|
||||
const newClassName = (className || "") + " mx_FormButton";
|
||||
const allProps = Object.assign({}, restProps,
|
||||
{className: newClassName, kind: kind || "primary", children: [label]});
|
||||
return React.createElement(AccessibleButton, allProps);
|
||||
}
|
||||
|
||||
FormButton.propTypes = AccessibleButton.propTypes;
|
|
@ -22,7 +22,7 @@ import { _t } from '../../../languageHandler';
|
|||
const GroupsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton className="mx_GroupsButton" action="view_my_groups"
|
||||
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
|
||||
label={_t("Communities")}
|
||||
size={props.size}
|
||||
tooltip={true}
|
||||
|
|
|
@ -100,7 +100,9 @@ module.exports = createReactClass({
|
|||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
// Hide the entire container when not visible. This prevents flashing of the tooltip
|
||||
// if it is not meant to be visible on first mount.
|
||||
style.display = this.props.visible ? "block" : "none";
|
||||
|
||||
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
|
||||
"mx_Tooltip_visible": this.props.visible,
|
||||
|
|
|
@ -111,10 +111,10 @@ export default class MKeyVerificationRequest extends React.Component {
|
|||
userLabelForEventRoom(fromUserId, mxEvent)}</div>);
|
||||
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
|
||||
if (isResolved) {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
stateNode = (<div className="mx_KeyVerification_buttons">
|
||||
<AccessibleButton kind="decline" onClick={this._onRejectClicked}>{_t("Decline")}</AccessibleButton>
|
||||
<AccessibleButton kind="accept" onClick={this._onAcceptClicked}>{_t("Accept")}</AccessibleButton>
|
||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
|
||||
</div>);
|
||||
}
|
||||
} else if (isOwn) { // request sent by us
|
||||
|
|
|
@ -88,7 +88,7 @@ module.exports = createReactClass({
|
|||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
if (content.info && content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
|
|
|
@ -144,7 +144,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
//console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
//console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
// exploit that events are immutable :)
|
||||
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
|
||||
|
@ -159,7 +159,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
calculateUrlPreview: function() {
|
||||
//console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
//console.info("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
if (this.props.showUrlPreview) {
|
||||
let links = this.findLinks(this.refs.content.children);
|
||||
|
|
|
@ -548,7 +548,7 @@ module.exports = createReactClass({
|
|||
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
|
||||
//console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
//console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const msgtype = content.msgtype;
|
||||
|
|
|
@ -698,7 +698,7 @@ module.exports = createReactClass({
|
|||
|
||||
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
|
||||
if (!canAffectUser) {
|
||||
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
||||
//console.info("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
|
||||
return can;
|
||||
}
|
||||
const editPowerLevel = (
|
||||
|
|
|
@ -31,6 +31,9 @@ import {_t} from "../../../languageHandler";
|
|||
const MAX_ROOMS = 20;
|
||||
const MIN_ROOMS_BEFORE_ENABLED = 10;
|
||||
|
||||
// The threshold time in milliseconds to wait for an autojoined room to show up.
|
||||
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds
|
||||
|
||||
export default class RoomBreadcrumbs extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -38,6 +41,10 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this._dispatcherRef = null;
|
||||
|
||||
// The room IDs we're waiting to come down the Room handler and when we
|
||||
// started waiting for them. Used to track a room over an upgrade/autojoin.
|
||||
this._waitingRoomQueue = [/* { roomId, addedTs } */];
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -54,7 +61,7 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().on("Room", this.onRoomMembershipChanged);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -68,7 +75,7 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
client.removeListener("Room", this.onRoomMembershipChanged);
|
||||
client.removeListener("Room", this.onRoom);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +94,12 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
onAction(payload) {
|
||||
switch (payload.action) {
|
||||
case 'view_room':
|
||||
if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) {
|
||||
// Queue the room instead of pushing it immediately - we're probably just waiting
|
||||
// for a join to complete (ie: joining the upgraded room).
|
||||
this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()});
|
||||
break;
|
||||
}
|
||||
this._appendRoomId(payload.room_id);
|
||||
break;
|
||||
|
||||
|
@ -153,7 +166,20 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
if (!this.state.enabled && this._shouldEnable()) {
|
||||
this.setState({enabled: true});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onRoom = (room) => {
|
||||
// Always check for membership changes when we see new rooms
|
||||
this.onRoomMembershipChanged();
|
||||
|
||||
const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId);
|
||||
if (!waitingRoom) return;
|
||||
this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1);
|
||||
|
||||
const now = (new Date()).getTime();
|
||||
if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
|
||||
this._appendRoomId(room.roomId); // add the room we've been waiting for
|
||||
};
|
||||
|
||||
_shouldEnable() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
} from '../../../notifications';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
// TODO: this "view" component still has far too much application logic in it,
|
||||
// which should be factored out to other files.
|
||||
|
@ -654,6 +655,17 @@ module.exports = createReactClass({
|
|||
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
|
||||
},
|
||||
|
||||
_onClearNotifications: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
cli.getRooms().forEach(r => {
|
||||
if (r.getUnreadNotificationCount() > 0) {
|
||||
const events = r.getLiveTimeline().getEvents();
|
||||
if (events.length) cli.sendReadReceipt(events.pop());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_updatePushRuleActions: function(rule, actions, enabled) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
|
@ -746,6 +758,13 @@ module.exports = createReactClass({
|
|||
label={_t('Enable notifications for this account')}/>;
|
||||
}
|
||||
|
||||
let clearNotificationsButton;
|
||||
if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) {
|
||||
clearNotificationsButton = <AccessibleButton onClick={this._onClearNotifications} kind='danger'>
|
||||
{_t("Clear notifications")}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
// When enabled, the master rule inhibits all existing rules
|
||||
// So do not show all notification settings
|
||||
if (this.state.masterPushRule && this.state.masterPushRule.enabled) {
|
||||
|
@ -756,6 +775,8 @@ module.exports = createReactClass({
|
|||
<div className="mx_UserNotifSettings_notifTable">
|
||||
{ _t('All notifications are currently disabled for all targets.') }
|
||||
</div>
|
||||
|
||||
{clearNotificationsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -877,6 +898,7 @@ module.exports = createReactClass({
|
|||
|
||||
{ devicesSection }
|
||||
|
||||
{ clearNotificationsButton }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -49,6 +49,17 @@ export default class LabsUserSettingsTab extends React.Component {
|
|||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Labs")}</div>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{
|
||||
_t('Customise your experience with experimental labs features. ' +
|
||||
'<a>Learn more</a>.', {}, {
|
||||
'a': (sub) => {
|
||||
return <a href="https://github.com/vector-im/riot-web/blob/develop/docs/labs.md"
|
||||
rel='noopener' target='_blank'>{sub}</a>;
|
||||
},
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{flags}
|
||||
<SettingsFlag name={"enableWidgetScreenshots"} level={SettingLevel.ACCOUNT} />
|
||||
|
|
123
src/components/views/toasts/VerificationRequestToast.js
Normal file
123
src/components/views/toasts/VerificationRequestToast.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
import sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
|
||||
import KeyVerificationStateObserver, {userLabelForEventRoom} from "../../../utils/KeyVerificationStateObserver";
|
||||
import dis from "../../../dispatcher";
|
||||
|
||||
export default class VerificationRequestToast extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const {event, timeout} = props.request;
|
||||
// to_device requests don't have a timestamp, so consider them age=0
|
||||
const age = event.getTs() ? event.getLocalAge() : 0;
|
||||
const remaining = Math.max(0, timeout - age);
|
||||
const counter = Math.ceil(remaining / 1000);
|
||||
this.state = {counter};
|
||||
if (this.props.requestObserver) {
|
||||
this.props.requestObserver.setCallback(this._checkRequestIsPending);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.requestObserver) {
|
||||
this.props.requestObserver.attach();
|
||||
this._checkRequestIsPending();
|
||||
}
|
||||
this._intervalHandle = setInterval(() => {
|
||||
let {counter} = this.state;
|
||||
counter -= 1;
|
||||
if (counter <= 0) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.setState({counter});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this._intervalHandle);
|
||||
if (this.props.requestObserver) {
|
||||
this.props.requestObserver.detach();
|
||||
}
|
||||
}
|
||||
|
||||
_checkRequestIsPending = () => {
|
||||
if (!this.props.requestObserver.pending) {
|
||||
this.props.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
cancel = () => {
|
||||
this.props.dismiss();
|
||||
try {
|
||||
this.props.request.cancel();
|
||||
} catch (err) {
|
||||
console.error("Error while cancelling verification request", err);
|
||||
}
|
||||
}
|
||||
|
||||
accept = () => {
|
||||
this.props.dismiss();
|
||||
const {event} = this.props.request;
|
||||
// no room id for to_device requests
|
||||
if (event.getRoomId()) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: event.getRoomId(),
|
||||
should_peek: false,
|
||||
});
|
||||
}
|
||||
|
||||
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
|
||||
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {verifier});
|
||||
};
|
||||
|
||||
render() {
|
||||
const FormButton = sdk.getComponent("elements.FormButton");
|
||||
const {event} = this.props.request;
|
||||
const userId = event.getSender();
|
||||
let nameLabel = event.getRoomId() ? userLabelForEventRoom(userId, event) : userId;
|
||||
// for legacy to_device verification requests
|
||||
if (nameLabel === userId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const user = client.getUser(event.getSender());
|
||||
if (user && user.displayName) {
|
||||
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
|
||||
}
|
||||
}
|
||||
return (<div>
|
||||
<div className="mx_Toast_description">{nameLabel}</div>
|
||||
<div className="mx_Toast_buttons" aria-live="off">
|
||||
<FormButton label={_t("Decline (%(counter)s)", {counter: this.state.counter})} kind="danger" onClick={this.cancel} />
|
||||
<FormButton label={_t("Accept")} onClick={this.accept} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
VerificationRequestToast.propTypes = {
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
request: PropTypes.object.isRequired,
|
||||
requestObserver: PropTypes.instanceOf(KeyVerificationStateObserver),
|
||||
};
|
|
@ -90,6 +90,13 @@ module.exports = createReactClass({
|
|||
}
|
||||
} else {
|
||||
call = CallHandler.getAnyActiveCall();
|
||||
// Ignore calls if we can't get the room associated with them.
|
||||
// I think the underlying problem is that the js-sdk sends events
|
||||
// for calls before it has made the rooms available in the store,
|
||||
// although this isn't confirmed.
|
||||
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
||||
call = null;
|
||||
}
|
||||
this.setState({ call: call });
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue