mirror of https://github.com/vector-im/riot-web
Merge pull request #3622 from matrix-org/travis/new-widget-prompt
Implement the bulk of the new widget permission prompt designpull/21833/head
commit
ef1c575bff
|
@ -550,6 +550,22 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
color: $username-variant8-color;
|
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 {
|
@define-mixin mx_Settings_fullWidthField {
|
||||||
margin-right: 100px;
|
margin-right: 100px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,49 +294,61 @@ form.mx_Custom_Widget_Form div {
|
||||||
|
|
||||||
.mx_AppPermissionWarning {
|
.mx_AppPermissionWarning {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: $primary-bg-color;
|
background-color: $widget-menu-bar-bg-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppPermissionWarningImage {
|
.mx_AppPermissionWarning_row {
|
||||||
margin: 10px 0;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppPermissionWarningImage img {
|
.mx_AppPermissionWarning_smallText {
|
||||||
width: 100px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppPermissionWarningText {
|
.mx_AppPermissionWarning_bolder {
|
||||||
max-width: 90%;
|
font-weight: 600;
|
||||||
margin: 10px auto 10px auto;
|
|
||||||
color: $primary-fg-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppPermissionWarningTextLabel {
|
.mx_AppPermissionWarning h4 {
|
||||||
font-weight: bold;
|
margin: 0;
|
||||||
display: block;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppPermissionWarningTextURL {
|
.mx_AppPermissionWarning_helpIcon {
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-right: 2px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 100%;
|
|
||||||
color: $accent-color;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppPermissionButton {
|
.mx_AppPermissionWarning_helpIcon::before {
|
||||||
border: none;
|
display: inline-block;
|
||||||
padding: 5px 20px;
|
background-color: $accent-color;
|
||||||
border-radius: 5px;
|
mask-repeat: no-repeat;
|
||||||
background-color: $button-bg-color;
|
mask-size: 12px;
|
||||||
color: $button-fg-color;
|
width: 12px;
|
||||||
cursor: pointer;
|
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 {
|
.mx_AppLoading {
|
||||||
|
|
|
@ -19,79 +19,123 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
export default class AppPermission extends React.Component {
|
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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const curlBase = this.getCurlBase();
|
// The first step is to pick apart the widget so we can render information about it
|
||||||
this.state = { curlBase: curlBase};
|
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
|
parseWidgetUrl() {
|
||||||
getCurlBase() {
|
const widgetUrl = url.parse(this.props.url);
|
||||||
const wurl = url.parse(this.props.url);
|
const params = new URLSearchParams(widgetUrl.search);
|
||||||
let curl;
|
|
||||||
let curlString;
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(wurl.search);
|
// 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(wurl) && searchParams && searchParams.get('url')) {
|
if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) {
|
||||||
curl = url.parse(searchParams.get('url'));
|
const unwrappedUrl = url.parse(params.get('url'));
|
||||||
if (curl) {
|
return {
|
||||||
curl.search = curl.query = "";
|
widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname,
|
||||||
curlString = curl.format();
|
isWrapped: true,
|
||||||
}
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
widgetDomain: widgetUrl.host || widgetUrl.hostname,
|
||||||
|
isWrapped: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!curl && wurl) {
|
|
||||||
wurl.search = wurl.query = "";
|
|
||||||
curlString = wurl.format();
|
|
||||||
}
|
|
||||||
return curlString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let e2eWarningText;
|
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
|
||||||
if (this.props.isRoomEncrypted) {
|
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||||
e2eWarningText =
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip");
|
||||||
}
|
|
||||||
const cookieWarning =
|
const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId;
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>
|
const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;
|
||||||
{ _t('Warning: This widget might use cookies.') }
|
|
||||||
</span>;
|
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 (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
|
||||||
<img src={require("../../../../res/img/feather-customised/warning-triangle.svg")} alt={_t('Warning!')} />
|
{_t("Widget added by")}
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarning_row'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span>
|
{avatar}
|
||||||
<span className='mx_AppPermissionWarningTextURL'
|
<h4 className='mx_AppPermissionWarning_bolder'>{displayName}</h4>
|
||||||
title={this.state.curlBase}
|
<div className='mx_AppPermissionWarning_smallText'>{userId}</div>
|
||||||
>{this.state.curlBase}</span>
|
</div>
|
||||||
{ e2eWarningText }
|
<div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
|
||||||
{ cookieWarning }
|
{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>
|
</div>
|
||||||
<input
|
|
||||||
className='mx_AppPermissionButton'
|
|
||||||
type='button'
|
|
||||||
value={_t('Allow')}
|
|
||||||
onClick={this.props.onPermissionGranted}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppPermission.propTypes = {
|
|
||||||
isRoomEncrypted: PropTypes.bool,
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
onPermissionGranted: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
AppPermission.defaultProps = {
|
|
||||||
isRoomEncrypted: false,
|
|
||||||
onPermissionGranted: function() {},
|
|
||||||
};
|
|
||||||
|
|
|
@ -569,11 +569,11 @@ export default class AppTile extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (!this.state.hasPermissionToLoad) {
|
if (!this.state.hasPermissionToLoad) {
|
||||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={appTileBodyClass}>
|
<div className={appTileBodyClass}>
|
||||||
<AppPermission
|
<AppPermission
|
||||||
isRoomEncrypted={isRoomEncrypted}
|
roomId={this.props.room.roomId}
|
||||||
|
creatorUserId={this.props.creatorUserId}
|
||||||
url={this.state.widgetUrl}
|
url={this.state.widgetUrl}
|
||||||
onPermissionGranted={this._grantWidgetPermission}
|
onPermissionGranted={this._grantWidgetPermission}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,7 +21,8 @@ import sdk from '../../../index';
|
||||||
export default class TextWithTooltip extends React.Component {
|
export default class TextWithTooltip extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
class: PropTypes.string,
|
class: PropTypes.string,
|
||||||
tooltip: PropTypes.string.isRequired,
|
tooltipClass: PropTypes.string,
|
||||||
|
tooltip: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -49,6 +50,7 @@ export default class TextWithTooltip extends React.Component {
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={this.props.tooltip}
|
label={this.props.tooltip}
|
||||||
visible={this.state.hover}
|
visible={this.state.hover}
|
||||||
|
tooltipClassName={this.props.tooltipClass}
|
||||||
className={"mx_TextWithTooltip_tooltip"} />
|
className={"mx_TextWithTooltip_tooltip"} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1184,10 +1184,18 @@
|
||||||
"Quick Reactions": "Quick Reactions",
|
"Quick Reactions": "Quick Reactions",
|
||||||
"Cancel search": "Cancel search",
|
"Cancel search": "Cancel search",
|
||||||
"Unknown Address": "Unknown Address",
|
"Unknown Address": "Unknown Address",
|
||||||
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
|
"Any of the following data may be shared:": "Any of the following data may be shared:",
|
||||||
"Warning: This widget might use cookies.": "Warning: This widget might use cookies.",
|
"Your display name": "Your display name",
|
||||||
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
|
"Your avatar URL": "Your avatar URL",
|
||||||
"Allow": "Allow",
|
"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",
|
"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?",
|
"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",
|
"Delete widget": "Delete widget",
|
||||||
|
@ -1495,6 +1503,7 @@
|
||||||
"A widget would like to verify your identity": "A widget would like to verify your identity",
|
"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.",
|
"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",
|
"Remember my selection for this widget": "Remember my selection for this widget",
|
||||||
|
"Allow": "Allow",
|
||||||
"Deny": "Deny",
|
"Deny": "Deny",
|
||||||
"Unable to load backup status": "Unable to load backup status",
|
"Unable to load backup status": "Unable to load backup status",
|
||||||
"Recovery Key Mismatch": "Recovery Key Mismatch",
|
"Recovery Key Mismatch": "Recovery Key Mismatch",
|
||||||
|
|
Loading…
Reference in New Issue