Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup

This commit is contained in:
J. Ryan Stinnett 2019-11-29 11:31:23 +00:00
commit c6e56d98b3
59 changed files with 1164 additions and 371 deletions

View file

@ -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,

View file

@ -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()}

View 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;

View file

@ -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}

View file

@ -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,

View file

@ -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

View file

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

View file

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

View file

@ -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;

View file

@ -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 = (

View file

@ -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();

View file

@ -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>

View file

@ -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} />

View 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),
};

View file

@ -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 });
}