Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into joriks/font-scaling-slider

This commit is contained in:
Jorik Schellekens 2020-04-28 15:38:46 +01:00
commit 1b83faaa8d
51 changed files with 1699 additions and 333 deletions

View file

@ -221,10 +221,27 @@ export default class RightPanel extends React.Component {
case RIGHT_PANEL_PHASES.EncryptionPanel:
if (SettingsStore.getValue("feature_cross_signing")) {
const onClose = () => {
dis.dispatch({
action: "view_user",
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
});
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
if (this.props.user) {
// If we have a user prop then we're displaying a user from the 'user' page type
// in LoggedInView, so need to change the page type to close the panel (we switch
// to the home page which is not obviosuly the correct thing to do, but I'm not sure
// anything else is - we could hide the close button altogether?)
dis.dispatch({
action: "view_home_page",
});
} else {
// Otherwise we have got our user from RoomViewStore which means we're being shown
// within a room, so go back to the member panel if we were in the encryption panel,
// or the member list if we were in the member panel... phew.
dis.dispatch({
action: "view_user",
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ?
this.state.member : null,
});
}
};
panel = <UserInfo
user={this.state.member}

View file

@ -131,7 +131,7 @@ export default class SetupEncryptionBody extends React.Component {
</div>
<div className="mx_CompleteSecurity_actionRow">
<AccessibleButton kind="link" onClick={this.onSkipClick}>
<AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
{_t("Use Recovery Passphrase or Key")}
</AccessibleButton>
<AccessibleButton kind="danger" onClick={this.onSkipClick}>

View file

@ -165,9 +165,11 @@ export default createReactClass({
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: toRem(width * 0.65),
width: toRem(width),
lineHeight: toRem(height) }}
style={{
fontSize: toRem(width * 0.65),
width: toRem(width),
lineHeight: toRem(height),
}}
>
{ initialLetter }
</span>
@ -208,8 +210,8 @@ export default createReactClass({
onClick={onClick}
onError={this.onError}
style={{
width: {width},
height: {height},
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
inputRef={inputRef}
@ -223,7 +225,7 @@ export default createReactClass({
onError={this.onError}
style={{
width: toRem(width),
height: toRem(height)
height: toRem(height),
}}
title={title} alt=""
ref={inputRef}

View file

@ -14,16 +14,52 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
export default function SetupEncryptionDialog({onFinished}) {
return <BaseDialog
headerImage={require("../../../../res/img/e2e/warning.svg")}
onFinished={onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={onFinished} />
</BaseDialog>;
function iconFromPhase(phase) {
if (phase === PHASE_DONE) {
return require("../../../../res/img/e2e/verified.svg");
} else {
return require("../../../../res/img/e2e/warning.svg");
}
}
export default class SetupEncryptionDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this.store = SetupEncryptionStore.sharedInstance();
this.state = {icon: iconFromPhase(this.store.phase)};
}
componentDidMount() {
this.store.on("update", this._onStoreUpdate);
}
componentWillUnmount() {
this.store.removeListener("update", this._onStoreUpdate);
}
_onStoreUpdate = () => {
this.setState({icon: iconFromPhase(this.store.phase)});
};
render() {
return <BaseDialog
headerImage={this.state.icon}
onFinished={this.props.onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={this.props.onFinished} />
</BaseDialog>;
}
}

View file

@ -95,7 +95,7 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab />,
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(

View file

@ -188,13 +188,14 @@ export default class AppTile extends React.Component {
}
}
// TODO: Generify the name of this function. It's not just scalar tokens.
/**
* Adds a scalar token to the widget URL, if required
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
console.warn('Widget does not match integration manager, refusing to set auth token', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.app.url),
@ -218,7 +219,7 @@ export default class AppTile extends React.Component {
const defaultManager = managers.getPrimaryManager();
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
console.warn('Non-scalar manager, not setting scalar token!', url);
console.warn('Unknown integration manager, refusing to set auth token', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.app.url),
@ -423,8 +424,13 @@ export default class AppTile extends React.Component {
_setupWidgetMessaging() {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
// We use the app's URL over the rendered URL so that anything the widget does which could
// lead to requesting a "security key" will pass accordingly. The only other thing this URL
// is used for is to determine the origin we're talking to, and therefore we don't need the
// fully templated URL.
const widgetMessaging = new WidgetMessaging(
this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow);
this.props.app.id, this.props.app.url, this.props.userWidget, this._appFrame.current.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
@ -560,15 +566,18 @@ export default class AppTile extends React.Component {
* Replace the widget template variables in a url with their values
*
* @param {string} u The URL with template variables
* @param {string} widgetType The widget's type
*
* @returns {string} url with temlate variables replaced
*/
_templatedUrl(u) {
_templatedUrl(u, widgetType: string) {
const targetData = {};
if (WidgetType.JITSI.matches(widgetType)) {
targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
}
const myUserId = MatrixClientPeg.get().credentials.userId;
const myUser = MatrixClientPeg.get().getUser(myUserId);
const vars = Object.assign({
domain: "jitsi.riot.im", // v1 widgets have this hardcoded
}, this.props.app.data, {
const vars = Object.assign(targetData, this.props.app.data, {
'matrix_user_id': myUserId,
'matrix_room_id': this.props.room.roomId,
'matrix_display_name': myUser ? myUser.displayName : myUserId,
@ -605,18 +614,19 @@ export default class AppTile extends React.Component {
} else {
url = this._getSafeUrl(this.state.widgetUrl);
}
return this._templatedUrl(url);
return this._templatedUrl(url, this.props.app.type);
}
_getPopoutUrl() {
if (WidgetType.JITSI.matches(this.props.app.type)) {
return this._templatedUrl(
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
this.props.app.type,
);
} else {
// use app.url, not state.widgetUrl, because we want the one without
// the wURL params for the popped-out version.
return this._templatedUrl(this._getSafeUrl(this.props.app.url));
return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
}
}
@ -630,7 +640,10 @@ export default class AppTile extends React.Component {
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
// We also need the dollar signs in-tact for variable substitution.
return safeWidgetUrl.replace(/%24/g, '$');
}
_getTileTitle() {

View file

@ -181,9 +181,7 @@ function DeviceItem({userId, device}) {
});
const onDeviceClick = () => {
if (!isVerified) {
verifyDevice(cli.getUser(userId), device);
}
verifyDevice(cli.getUser(userId), device);
};
const deviceName = device.ambiguous ?
@ -191,17 +189,29 @@ function DeviceItem({userId, device}) {
device.getDisplayName();
let trustedLabel = null;
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
return (
<AccessibleButton
className={classes}
title={device.deviceId}
onClick={onDeviceClick}
>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</AccessibleButton>
);
if (isVerified) {
return (
<div className={classes} title={device.deviceId} >
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</div>
);
} else {
return (
<AccessibleButton
className={classes}
title={device.deviceId}
onClick={onDeviceClick}
>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</AccessibleButton>
);
}
}
function DevicesSection({devices, userId, loading}) {

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import SettingsStore from '../../../settings/SettingsStore';
export default class InviteOnlyIcon extends React.Component {
constructor() {
@ -39,10 +38,6 @@ export default class InviteOnlyIcon extends React.Component {
render() {
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
if (!SettingsStore.isFeatureEnabled("feature_invite_only_padlocks")) {
return null;
}
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.hover) {

View file

@ -26,8 +26,7 @@ import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu} from "../../structures/ContextMenu";
const widgetType = 'm.stickerpicker';
import {WidgetType} from "../../../widgets/WidgetType";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
@ -87,7 +86,7 @@ export default class Stickerpicker extends React.Component {
console.log('Removing Stickerpicker widgets');
if (this.state.widgetId) {
if (scalarClient) {
scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
console.log('Assets disabled');
}).catch((err) => {
console.error('Failed to disable assets');
@ -364,13 +363,13 @@ export default class Stickerpicker extends React.Component {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
`type_${widgetType}`,
`type_${WidgetType.STICKERPICKER.preferred}`,
this.state.widgetId,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
`type_${widgetType}`,
`type_${WidgetType.STICKERPICKER.preferred}`,
this.state.widgetId,
);
}

View file

@ -52,6 +52,10 @@ export class IgnoredUser extends React.Component {
}
export default class SecurityUserSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() {
super();
@ -107,6 +111,11 @@ export default class SecurityUserSettingsTab extends React.Component {
);
};
_onGoToUserProfileClick = () => {
// close the settings dialog & let the default action run (ie. navigate to the link)
this.props.closeSettingsFn();
}
_onUserUnignored = async (userId) => {
const {ignoredUserIds, waitingUnignored} = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
@ -312,7 +321,18 @@ export default class SecurityUserSettingsTab extends React.Component {
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Sessions")}</span>
<span className="mx_SettingsTab_subheading">{_t("Where youre logged in")}</span>
<span>
{_t(
"Manage the names of and sign out of your sessions below or " +
"<a>verify them in your User Profile</a>.", {},
{
a: sub => <a href={"#/user/" + MatrixClientPeg.get().getUserId()}
onClick={this._onGoToUserProfileClick}
>{sub}</a>,
},
)}
</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("A session's public name is visible to people you communicate with")}
<DevicesPanel />

View file

@ -15,52 +15,32 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import Modal from "../../../Modal";
import dis from "../../../dispatcher";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
};
_onLaterClick = () => {
const { device } = this.props;
DeviceListener.sharedInstance().dismissVerification(device.deviceId);
DeviceListener.sharedInstance().dismissVerifications();
};
_onReviewClick = async () => {
const { device } = this.props;
DeviceListener.sharedInstance().dismissVerifications();
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
device,
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
this._onLaterClick();
}
},
}, null, /* priority = */ false, /* static = */ true);
});
};
render() {
const { device } = this.props;
return (<div>
<div className="mx_Toast_description">
<span className="mx_Toast_deviceName">
{device.getDisplayName()}
</span> <span className="mx_Toast_deviceID">
({device.deviceId})
</span>
{_t("Verify your other sessions")}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />