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
							parent
							
								
									d15722d4c9
								
							
						
					
					
						commit
						6b726a8e13
					
				|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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() {}, | ||||
| }; | ||||
|  |  | |||
|  | @ -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} | ||||
|                         /> | ||||
|  |  | |||
|  | @ -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> | ||||
|         ); | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston