Implement the bulk of the new widget permission prompt design

Part 1 of https://github.com/vector-im/riot-web/issues/11262

This is all the visual changes - the actual wiring of the UI to the right places is for another PR (though this PR still works independently).

The help icon is known to be weird here - it's a bug in the svg we have. The tooltip also goes right instead of up because making the tooltip go up is not easy work for this PR - maybe a future one if we *really* want it to go up.
pull/21833/head
Travis Ralston 2019-11-15 14:25:53 -07:00
parent d15722d4c9
commit 6b726a8e13
6 changed files with 169 additions and 86 deletions

View File

@ -550,6 +550,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
color: $username-variant8-color;
}
@define-mixin mx_Tooltip_dark {
box-shadow: none;
background-color: $tooltip-timeline-bg-color;
color: $tooltip-timeline-fg-color;
border: none;
border-radius: 3px;
padding: 6px 8px;
}
// This is a workaround for our mixins not supporting child selectors
.mx_Tooltip_dark {
.mx_Tooltip_chevron::after {
border-right-color: $tooltip-timeline-bg-color;
}
}
@define-mixin mx_Settings_fullWidthField {
margin-right: 100px;
}

View File

@ -294,49 +294,61 @@ form.mx_Custom_Widget_Form div {
.mx_AppPermissionWarning {
text-align: center;
background-color: $primary-bg-color;
background-color: $widget-menu-bar-bg-color;
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 16px;
}
.mx_AppPermissionWarningImage {
margin: 10px 0;
.mx_AppPermissionWarning_row {
margin-bottom: 12px;
}
.mx_AppPermissionWarningImage img {
width: 100px;
.mx_AppPermissionWarning_smallText {
font-size: 12px;
}
.mx_AppPermissionWarningText {
max-width: 90%;
margin: 10px auto 10px auto;
color: $primary-fg-color;
.mx_AppPermissionWarning_bolder {
font-weight: 600;
}
.mx_AppPermissionWarningTextLabel {
font-weight: bold;
display: block;
.mx_AppPermissionWarning h4 {
margin: 0;
padding: 0;
}
.mx_AppPermissionWarningTextURL {
.mx_AppPermissionWarning_helpIcon {
margin-top: 1px;
margin-right: 2px;
width: 10px;
height: 10px;
display: inline-block;
max-width: 100%;
color: $accent-color;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mx_AppPermissionButton {
border: none;
padding: 5px 20px;
border-radius: 5px;
background-color: $button-bg-color;
color: $button-fg-color;
cursor: pointer;
.mx_AppPermissionWarning_helpIcon::before {
display: inline-block;
background-color: $accent-color;
mask-repeat: no-repeat;
mask-size: 12px;
width: 12px;
height: 12px;
mask-position: center;
content: '';
vertical-align: middle;
mask-image: url('$(res)/img/feather-customised/help-circle.svg');
}
.mx_AppPermissionWarning_tooltip {
@mixin mx_Tooltip_dark;
ul {
list-style-position: inside;
padding-left: 2px;
margin-left: 0;
}
}
.mx_AppLoading {

View File

@ -19,79 +19,123 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import WidgetUtils from "../../../utils/WidgetUtils";
import MatrixClientPeg from "../../../MatrixClientPeg";
export default class AppPermission extends React.Component {
static propTypes = {
url: PropTypes.string.isRequired,
creatorUserId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
static defaultProps = {
onPermissionGranted: () => {},
};
constructor(props) {
super(props);
const curlBase = this.getCurlBase();
this.state = { curlBase: curlBase};
// The first step is to pick apart the widget so we can render information about it
const urlInfo = this.parseWidgetUrl();
// The second step is to find the user's profile so we can show it on the prompt
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
let roomMember;
if (room) roomMember = room.getMember(this.props.creatorUserId);
// Set all this into the initial state
this.state = {
...urlInfo,
roomMember,
};
}
// Return string representation of content URL without query parameters
getCurlBase() {
const wurl = url.parse(this.props.url);
let curl;
let curlString;
parseWidgetUrl() {
const widgetUrl = url.parse(this.props.url);
const params = new URLSearchParams(widgetUrl.search);
const searchParams = new URLSearchParams(wurl.search);
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
curl = url.parse(searchParams.get('url'));
if (curl) {
curl.search = curl.query = "";
curlString = curl.format();
}
// HACK: We're relying on the query params when we should be relying on the widget's `data`.
// This is a workaround for Scalar.
if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) {
const unwrappedUrl = url.parse(params.get('url'));
return {
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
isWrapped: true,
};
} else {
return {
widgetDomain: widgetUrl.host || widgetUrl.hostname,
isWrapped: false,
};
}
if (!curl && wurl) {
wurl.search = wurl.query = "";
curlString = wurl.format();
}
return curlString;
}
render() {
let e2eWarningText;
if (this.props.isRoomEncrypted) {
e2eWarningText =
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
}
const cookieWarning =
<span className='mx_AppPermissionWarningTextLabel'>
{ _t('Warning: This widget might use cookies.') }
</span>;
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip");
const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId;
const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;
const avatar = this.state.roomMember
? <MemberAvatar member={this.state.roomMember} width={38} height={38} />
: <BaseAvatar name={this.props.creatorUserId} width={38} height={38} />;
const warningTooltipText = (
<div>
{_t("Any of the following data may be shared:")}
<ul>
<li>{_t("Your display name")}</li>
<li>{_t("Your avatar URL")}</li>
<li>{_t("Your user ID")}</li>
<li>{_t("Your theme")}</li>
<li>{_t("Riot URL")}</li>
<li>{_t("Room ID")}</li>
<li>{_t("Widget ID")}</li>
</ul>
</div>
);
const warningTooltip = (
<TextWithTooltip tooltip={warningTooltipText} tooltipClass='mx_AppPermissionWarning_tooltip mx_Tooltip_dark'>
<span className='mx_AppPermissionWarning_helpIcon' />
</TextWithTooltip>
);
// Due to i18n limitations, we can't dedupe the code for variables in these two messages.
const warning = this.state.isWrapped
? _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip})
: _t("Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
{widgetDomain: this.state.widgetDomain}, {helpIcon: () => warningTooltip});
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
{_t("Widget added by")}
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span>
<span className='mx_AppPermissionWarningTextURL'
title={this.state.curlBase}
>{this.state.curlBase}</span>
{ e2eWarningText }
{ cookieWarning }
<div className='mx_AppPermissionWarning_row'>
{avatar}
<h4 className='mx_AppPermissionWarning_bolder'>{displayName}</h4>
<div className='mx_AppPermissionWarning_smallText'>{userId}</div>
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{warning}
</div>
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
{_t("This widget may use cookies.")}
</div>
<div className='mx_AppPermissionWarning_row'>
<AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
{_t("Continue")}
</AccessibleButton>
</div>
<input
className='mx_AppPermissionButton'
type='button'
value={_t('Allow')}
onClick={this.props.onPermissionGranted}
/>
</div>
);
}
}
AppPermission.propTypes = {
isRoomEncrypted: PropTypes.bool,
url: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
AppPermission.defaultProps = {
isRoomEncrypted: false,
onPermissionGranted: function() {},
};

View File

@ -569,11 +569,11 @@ export default class AppTile extends React.Component {
</div>
);
if (!this.state.hasPermissionToLoad) {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
isRoomEncrypted={isRoomEncrypted}
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>

View File

@ -21,7 +21,8 @@ import sdk from '../../../index';
export default class TextWithTooltip extends React.Component {
static propTypes = {
class: PropTypes.string,
tooltip: PropTypes.string.isRequired,
tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired,
};
constructor() {
@ -49,6 +50,7 @@ export default class TextWithTooltip extends React.Component {
<Tooltip
label={this.props.tooltip}
visible={this.state.hover}
tooltipClassName={this.props.tooltipClass}
className={"mx_TextWithTooltip_tooltip"} />
</span>
);

View File

@ -1183,10 +1183,18 @@
"Quick Reactions": "Quick Reactions",
"Cancel search": "Cancel search",
"Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Warning: This widget might use cookies.": "Warning: This widget might use cookies.",
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
"Allow": "Allow",
"Any of the following data may be shared:": "Any of the following data may be shared:",
"Your display name": "Your display name",
"Your avatar URL": "Your avatar URL",
"Your user ID": "Your user ID",
"Your theme": "Your theme",
"Riot URL": "Riot URL",
"Room ID": "Room ID",
"Widget ID": "Widget ID",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.",
"Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Using this widget may share data <helpIcon /> with %(widgetDomain)s.",
"Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.",
"Delete Widget": "Delete Widget",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"Delete widget": "Delete widget",
@ -1494,6 +1502,7 @@
"A widget would like to verify your identity": "A widget would like to verify your identity",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow",
"Deny": "Deny",
"Unable to load backup status": "Unable to load backup status",
"Recovery Key Mismatch": "Recovery Key Mismatch",