Add interaction modal to logged-out web UI (#19306)
This commit is contained in:
		
							parent
							
								
									99a43f0282
								
							
						
					
					
						commit
						7fb738c837
					
				
					 12 changed files with 391 additions and 47 deletions
				
			
		|  | @ -86,6 +86,7 @@ class Status extends ImmutablePureComponent { | |||
|     onToggleHidden: PropTypes.func, | ||||
|     onToggleCollapsed: PropTypes.func, | ||||
|     onTranslate: PropTypes.func, | ||||
|     onInteractionModal: PropTypes.func, | ||||
|     muted: PropTypes.bool, | ||||
|     hidden: PropTypes.bool, | ||||
|     unread: PropTypes.bool, | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ class StatusActionBar extends ImmutablePureComponent { | |||
|     onBookmark: PropTypes.func, | ||||
|     onFilter: PropTypes.func, | ||||
|     onAddFilter: PropTypes.func, | ||||
|     onInteractionModal: PropTypes.func, | ||||
|     withDismiss: PropTypes.bool, | ||||
|     withCounters: PropTypes.bool, | ||||
|     scrollKey: PropTypes.string, | ||||
|  | @ -97,10 +98,12 @@ class StatusActionBar extends ImmutablePureComponent { | |||
|   ] | ||||
| 
 | ||||
|   handleReplyClick = () => { | ||||
|     if (me) { | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       this.props.onReply(this.props.status, this.context.router.history); | ||||
|     } else { | ||||
|       this._openInteractionDialog('reply'); | ||||
|       this.props.onInteractionModal('reply', this.props.status); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -114,25 +117,25 @@ class StatusActionBar extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleFavouriteClick = () => { | ||||
|     if (me) { | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       this.props.onFavourite(this.props.status); | ||||
|     } else { | ||||
|       this._openInteractionDialog('favourite'); | ||||
|       this.props.onInteractionModal('favourite', this.props.status); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleReblogClick = e => { | ||||
|     if (me) { | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       this.props.onReblog(this.props.status, e); | ||||
|     } else { | ||||
|       this._openInteractionDialog('reblog'); | ||||
|       this.props.onInteractionModal('reblog', this.props.status); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   _openInteractionDialog = type => { | ||||
|     window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); | ||||
|   } | ||||
| 
 | ||||
|   handleBookmarkClick = () => { | ||||
|     this.props.onBookmark(this.props.status); | ||||
|   } | ||||
|  |  | |||
|  | @ -237,6 +237,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ | |||
|     dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); | ||||
|   }, | ||||
| 
 | ||||
|   onInteractionModal (type, status) { | ||||
|     dispatch(openModal('INTERACTION', { | ||||
|       type, | ||||
|       accountId: status.getIn(['account', 'id']), | ||||
|       url: status.get('url'), | ||||
|     })); | ||||
|   }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); | ||||
|  |  | |||
|  | @ -96,6 +96,7 @@ class Header extends ImmutablePureComponent { | |||
|     onAddToList: PropTypes.func.isRequired, | ||||
|     onEditAccountNote: PropTypes.func.isRequired, | ||||
|     onChangeLanguages: PropTypes.func.isRequired, | ||||
|     onInteractionModal: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     domain: PropTypes.string.isRequired, | ||||
|     hidden: PropTypes.bool, | ||||
|  | @ -177,7 +178,7 @@ class Header extends ImmutablePureComponent { | |||
|       } else if (account.getIn(['relationship', 'requested'])) { | ||||
|         actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />; | ||||
|       } else if (!account.getIn(['relationship', 'blocking'])) { | ||||
|         actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : undefined} />; | ||||
|         actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />; | ||||
|       } else if (account.getIn(['relationship', 'blocking'])) { | ||||
|         actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; | ||||
|       } | ||||
|  |  | |||
|  | @ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent { | |||
|     onEndorseToggle: PropTypes.func.isRequired, | ||||
|     onAddToList: PropTypes.func.isRequired, | ||||
|     onChangeLanguages: PropTypes.func.isRequired, | ||||
|     onInteractionModal: PropTypes.func.isRequired, | ||||
|     hideTabs: PropTypes.bool, | ||||
|     domain: PropTypes.string.isRequired, | ||||
|     hidden: PropTypes.bool, | ||||
|  | @ -96,6 +97,10 @@ export default class Header extends ImmutablePureComponent { | |||
|     this.props.onChangeLanguages(this.props.account); | ||||
|   } | ||||
| 
 | ||||
|   handleInteractionModal = () => { | ||||
|     this.props.onInteractionModal(this.props.account); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, hidden, hideTabs } = this.props; | ||||
| 
 | ||||
|  | @ -123,6 +128,7 @@ export default class Header extends ImmutablePureComponent { | |||
|           onAddToList={this.handleAddToList} | ||||
|           onEditAccountNote={this.handleEditAccountNote} | ||||
|           onChangeLanguages={this.handleChangeLanguages} | ||||
|           onInteractionModal={this.handleInteractionModal} | ||||
|           domain={this.props.domain} | ||||
|           hidden={hidden} | ||||
|         /> | ||||
|  |  | |||
|  | @ -57,6 +57,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | |||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onInteractionModal (account) { | ||||
|     dispatch(openModal('INTERACTION', { | ||||
|       type: 'follow', | ||||
|       accountId: account.get('id'), | ||||
|       url: account.get('url'), | ||||
|     })); | ||||
|   }, | ||||
| 
 | ||||
|   onBlock (account) { | ||||
|     if (account.getIn(['relationship', 'blocking'])) { | ||||
|       dispatch(unblockAccount(account.get('id'))); | ||||
|  |  | |||
							
								
								
									
										132
									
								
								app/javascript/mastodon/features/interaction_modal/index.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								app/javascript/mastodon/features/interaction_modal/index.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { registrationsOpen } from 'mastodon/initial_state'; | ||||
| import { connect } from 'react-redux'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { accountId }) => ({ | ||||
|   displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), | ||||
| }); | ||||
| 
 | ||||
| class Copypaste extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     value: PropTypes.string, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     copied: false, | ||||
|   }; | ||||
| 
 | ||||
|   setRef = c => { | ||||
|     this.input = c; | ||||
|   } | ||||
| 
 | ||||
|   handleInputClick = () => { | ||||
|     this.setState({ copied: false }); | ||||
|     this.input.focus(); | ||||
|     this.input.select(); | ||||
|     this.input.setSelectionRange(0, this.input.value.length); | ||||
|   } | ||||
| 
 | ||||
|   handleButtonClick = () => { | ||||
|     const { value } = this.props; | ||||
|     navigator.clipboard.writeText(value); | ||||
|     this.input.blur(); | ||||
|     this.setState({ copied: true }); | ||||
|     this.timeout = setTimeout(() => this.setState({ copied: false }), 700); | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     if (this.timeout) clearTimeout(this.timeout); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { value } = this.props; | ||||
|     const { copied } = this.state; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={classNames('copypaste', { copied })}> | ||||
|         <input | ||||
|           type='text' | ||||
|           ref={this.setRef} | ||||
|           value={value} | ||||
|           readOnly | ||||
|           onClick={this.handleInputClick} | ||||
|         /> | ||||
| 
 | ||||
|         <button className='button' onClick={this.handleButtonClick}> | ||||
|           {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy' defaultMessage='Copy' />} | ||||
|         </button> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
| class InteractionModal extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     displayNameHtml: PropTypes.string, | ||||
|     url: PropTypes.string, | ||||
|     type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']), | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { url, type, displayNameHtml } = this.props; | ||||
| 
 | ||||
|     const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />; | ||||
| 
 | ||||
|     let title, actionDescription, icon; | ||||
| 
 | ||||
|     switch(type) { | ||||
|     case 'reply': | ||||
|       icon = <Icon id='reply' />; | ||||
|       title = <FormattedMessage id='interaction_modal.title.reply' defaultMessage="Reply to {name}'s post" values={{ name }} />; | ||||
|       actionDescription = <FormattedMessage id='interaction_modal.description.reply' defaultMessage='With an account on Mastodon, you can respond to this post.' />; | ||||
|       break; | ||||
|     case 'reblog': | ||||
|       icon = <Icon id='retweet' />; | ||||
|       title = <FormattedMessage id='interaction_modal.title.reblog' defaultMessage="Boost {name}'s post" values={{ name }} />; | ||||
|       actionDescription = <FormattedMessage id='interaction_modal.description.reblog' defaultMessage='With an account on Mastodon, you can boost this post to share it with your own followers.' />; | ||||
|       break; | ||||
|     case 'favourite': | ||||
|       icon = <Icon id='star' />; | ||||
|       title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favourite {name}'s post" values={{ name }} />; | ||||
|       actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.' />; | ||||
|       break; | ||||
|     case 'follow': | ||||
|       icon = <Icon id='user-plus' />; | ||||
|       title = <FormattedMessage id='interaction_modal.title.follow' defaultMessage='Follow {name}' values={{ name }} />; | ||||
|       actionDescription = <FormattedMessage id='interaction_modal.description.follow' defaultMessage='With an account on Mastodon, you can follow {name} to receive their posts in your home feed.' values={{ name }} />; | ||||
|       break; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='modal-root__modal interaction-modal'> | ||||
|         <div className='interaction-modal__lead'> | ||||
|           <h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3> | ||||
|           <p>{actionDescription} <FormattedMessage id='interaction_modal.preamble' defaultMessage="Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one." /></p> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='interaction-modal__choices'> | ||||
|           <div className='interaction-modal__choices__choice'> | ||||
|             <h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3> | ||||
|             <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a> | ||||
|             <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className='interaction-modal__choices__choice'> | ||||
|             <h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3> | ||||
|             <p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Simply copy and paste this URL into the search bar of your favourite app or the web interface where you are signed in.' /></p> | ||||
|             <Copypaste value={url} /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -43,6 +43,7 @@ class Footer extends ImmutablePureComponent { | |||
| 
 | ||||
|   static contextTypes = { | ||||
|     router: PropTypes.object, | ||||
|     identity: PropTypes.object, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|  | @ -67,26 +68,44 @@ class Footer extends ImmutablePureComponent { | |||
|   }; | ||||
| 
 | ||||
|   handleReplyClick = () => { | ||||
|     const { dispatch, askReplyConfirmation, intl } = this.props; | ||||
|     const { dispatch, askReplyConfirmation, status, intl } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (askReplyConfirmation) { | ||||
|       dispatch(openModal('CONFIRM', { | ||||
|         message: intl.formatMessage(messages.replyMessage), | ||||
|         confirm: intl.formatMessage(messages.replyConfirm), | ||||
|         onConfirm: this._performReply, | ||||
|       })); | ||||
|     if (signedIn) { | ||||
|       if (askReplyConfirmation) { | ||||
|         dispatch(openModal('CONFIRM', { | ||||
|           message: intl.formatMessage(messages.replyMessage), | ||||
|           confirm: intl.formatMessage(messages.replyConfirm), | ||||
|           onConfirm: this._performReply, | ||||
|         })); | ||||
|       } else { | ||||
|         this._performReply(); | ||||
|       } | ||||
|     } else { | ||||
|       this._performReply(); | ||||
|       dispatch(openModal('INTERACTION', { | ||||
|         type: 'reply', | ||||
|         accountId: status.getIn(['account', 'id']), | ||||
|         url: status.get('url'), | ||||
|       })); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   handleFavouriteClick = () => { | ||||
|     const { dispatch, status } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (status.get('favourited')) { | ||||
|       dispatch(unfavourite(status)); | ||||
|     if (signedIn) { | ||||
|       if (status.get('favourited')) { | ||||
|         dispatch(unfavourite(status)); | ||||
|       } else { | ||||
|         dispatch(favourite(status)); | ||||
|       } | ||||
|     } else { | ||||
|       dispatch(favourite(status)); | ||||
|       dispatch(openModal('INTERACTION', { | ||||
|         type: 'favourite', | ||||
|         accountId: status.getIn(['account', 'id']), | ||||
|         url: status.get('url'), | ||||
|       })); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|  | @ -97,13 +116,22 @@ class Footer extends ImmutablePureComponent { | |||
| 
 | ||||
|   handleReblogClick = e => { | ||||
|     const { dispatch, status } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (status.get('reblogged')) { | ||||
|       dispatch(unreblog(status)); | ||||
|     } else if ((e && e.shiftKey) || !boostModal) { | ||||
|       this._performReblog(status); | ||||
|     if (signedIn) { | ||||
|       if (status.get('reblogged')) { | ||||
|         dispatch(unreblog(status)); | ||||
|       } else if ((e && e.shiftKey) || !boostModal) { | ||||
|         this._performReblog(status); | ||||
|       } else { | ||||
|         dispatch(initBoostModal({ status, onReblog: this._performReblog })); | ||||
|       } | ||||
|     } else { | ||||
|       dispatch(initBoostModal({ status, onReblog: this._performReblog })); | ||||
|       dispatch(openModal('INTERACTION', { | ||||
|         type: 'reblog', | ||||
|         accountId: status.getIn(['account', 'id']), | ||||
|         url: status.get('url'), | ||||
|       })); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -194,6 +194,7 @@ class ActionBar extends React.PureComponent { | |||
| 
 | ||||
|   render () { | ||||
|     const { status, relationship, intl } = this.props; | ||||
|     const { signedIn, permissions } = this.context.identity; | ||||
| 
 | ||||
|     const publicStatus       = ['public', 'unlisted'].includes(status.get('visibility')); | ||||
|     const pinnableStatus     = ['public', 'unlisted', 'private'].includes(status.get('visibility')); | ||||
|  | @ -250,7 +251,7 @@ class ActionBar extends React.PureComponent { | |||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { | ||||
|       if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { | ||||
|         menu.push(null); | ||||
|         menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); | ||||
|         menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` }); | ||||
|  | @ -287,10 +288,10 @@ class ActionBar extends React.PureComponent { | |||
|         <div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div> | ||||
|         <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> | ||||
|         {shareButton} | ||||
|         <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> | ||||
|         <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> | ||||
| 
 | ||||
|         <div className='detailed-status__action-bar-dropdown'> | ||||
|           <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> | ||||
|           <DropdownMenuContainer size={18} icon='ellipsis-h' disabled={!signedIn} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -180,6 +180,7 @@ class Status extends ImmutablePureComponent { | |||
| 
 | ||||
|   static contextTypes = { | ||||
|     router: PropTypes.object, | ||||
|     identity: PropTypes.object, | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|  | @ -228,10 +229,21 @@ class Status extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleFavouriteClick = (status) => { | ||||
|     if (status.get('favourited')) { | ||||
|       this.props.dispatch(unfavourite(status)); | ||||
|     const { dispatch } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       if (status.get('favourited')) { | ||||
|         dispatch(unfavourite(status)); | ||||
|       } else { | ||||
|         dispatch(favourite(status)); | ||||
|       } | ||||
|     } else { | ||||
|       this.props.dispatch(favourite(status)); | ||||
|       dispatch(openModal('INTERACTION', { | ||||
|         type: 'favourite', | ||||
|         accountId: status.getIn(['account', 'id']), | ||||
|         url: status.get('url'), | ||||
|       })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -244,15 +256,25 @@ class Status extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleReplyClick = (status) => { | ||||
|     let { askReplyConfirmation, dispatch, intl } = this.props; | ||||
|     if (askReplyConfirmation) { | ||||
|       dispatch(openModal('CONFIRM', { | ||||
|         message: intl.formatMessage(messages.replyMessage), | ||||
|         confirm: intl.formatMessage(messages.replyConfirm), | ||||
|         onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), | ||||
|       })); | ||||
|     const { askReplyConfirmation, dispatch, intl } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       if (askReplyConfirmation) { | ||||
|         dispatch(openModal('CONFIRM', { | ||||
|           message: intl.formatMessage(messages.replyMessage), | ||||
|           confirm: intl.formatMessage(messages.replyConfirm), | ||||
|           onConfirm: () => dispatch(replyCompose(status, this.context.router.history)), | ||||
|         })); | ||||
|       } else { | ||||
|         dispatch(replyCompose(status, this.context.router.history)); | ||||
|       } | ||||
|     } else { | ||||
|       dispatch(replyCompose(status, this.context.router.history)); | ||||
|       dispatch(openModal('INTERACTION', { | ||||
|         type: 'reply', | ||||
|         accountId: status.getIn(['account', 'id']), | ||||
|         url: status.get('url'), | ||||
|       })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -261,14 +283,25 @@ class Status extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleReblogClick = (status, e) => { | ||||
|     if (status.get('reblogged')) { | ||||
|       this.props.dispatch(unreblog(status)); | ||||
|     } else { | ||||
|       if ((e && e.shiftKey) || !boostModal) { | ||||
|         this.handleModalReblog(status); | ||||
|     const { dispatch } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
| 
 | ||||
|     if (signedIn) { | ||||
|       if (status.get('reblogged')) { | ||||
|         dispatch(unreblog(status)); | ||||
|       } else { | ||||
|         this.props.dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); | ||||
|         if ((e && e.shiftKey) || !boostModal) { | ||||
|           this.handleModalReblog(status); | ||||
|         } else { | ||||
|           dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       dispatch(openModal('INTERACTION', { | ||||
|         type: 'reblog', | ||||
|         accountId: status.getIn(['account', 'id']), | ||||
|         url: status.get('url'), | ||||
|       })); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import AudioModal from './audio_modal'; | |||
| import ConfirmationModal from './confirmation_modal'; | ||||
| import SubscribedLanguagesModal from 'mastodon/features/subscribed_languages_modal'; | ||||
| import FocalPointModal from './focal_point_modal'; | ||||
| import InteractionModal from 'mastodon/features/interaction_modal'; | ||||
| import { | ||||
|   MuteModal, | ||||
|   BlockModal, | ||||
|  | @ -41,6 +42,7 @@ const MODAL_COMPONENTS = { | |||
|   'COMPARE_HISTORY': CompareHistoryModal, | ||||
|   'FILTER': FilterModal, | ||||
|   'SUBSCRIBED_LANGUAGES': () => Promise.resolve({ default: SubscribedLanguagesModal }), | ||||
|   'INTERACTION': () => Promise.resolve({ default: InteractionModal }), | ||||
| }; | ||||
| 
 | ||||
| export default class ModalRoot extends React.PureComponent { | ||||
|  |  | |||
|  | @ -4899,6 +4899,7 @@ a.status-card.compact:hover { | |||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   box-sizing: border-box; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|  | @ -8105,3 +8106,123 @@ noscript { | |||
|     margin: 10px 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .interaction-modal { | ||||
|   max-width: 90vw; | ||||
|   width: 600px; | ||||
|   background: $ui-base-color; | ||||
|   border-radius: 8px; | ||||
|   overflow: hidden; | ||||
|   position: relative; | ||||
|   display: block; | ||||
|   padding: 20px; | ||||
| 
 | ||||
|   h3 { | ||||
|     font-size: 22px; | ||||
|     line-height: 33px; | ||||
|     font-weight: 700; | ||||
|     text-align: center; | ||||
|   } | ||||
| 
 | ||||
|   &__icon { | ||||
|     color: $highlight-text-color; | ||||
|     margin: 0 5px; | ||||
|   } | ||||
| 
 | ||||
|   &__lead { | ||||
|     padding: 20px; | ||||
|     text-align: center; | ||||
| 
 | ||||
|     h3 { | ||||
|       margin-bottom: 15px; | ||||
|     } | ||||
| 
 | ||||
|     p { | ||||
|       font-size: 17px; | ||||
|       line-height: 22px; | ||||
|       color: $darker-text-color; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &__choices { | ||||
|     display: flex; | ||||
| 
 | ||||
|     &__choice { | ||||
|       flex: 0 0 auto; | ||||
|       width: 50%; | ||||
|       box-sizing: border-box; | ||||
|       padding: 20px; | ||||
| 
 | ||||
|       h3 { | ||||
|         margin-bottom: 20px; | ||||
|       } | ||||
| 
 | ||||
|       p { | ||||
|         color: $darker-text-color; | ||||
|         margin-bottom: 20px; | ||||
|       } | ||||
| 
 | ||||
|       .button { | ||||
|         margin-bottom: 10px; | ||||
| 
 | ||||
|         &:last-child { | ||||
|           margin-bottom: 0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @media screen and (max-width: $no-gap-breakpoint - 1px) { | ||||
|     &__choices { | ||||
|       display: block; | ||||
| 
 | ||||
|       &__choice { | ||||
|         width: auto; | ||||
|         margin-bottom: 20px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .copypaste { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 10px; | ||||
| 
 | ||||
|   input { | ||||
|     display: block; | ||||
|     font-family: inherit; | ||||
|     background: darken($ui-base-color, 8%); | ||||
|     border: 1px solid $highlight-text-color; | ||||
|     color: $darker-text-color; | ||||
|     border-radius: 4px; | ||||
|     padding: 6px 9px; | ||||
|     line-height: 22px; | ||||
|     font-size: 14px; | ||||
|     transition: border-color 300ms linear; | ||||
|     flex: 1 1 auto; | ||||
|     overflow: hidden; | ||||
| 
 | ||||
|     &:focus { | ||||
|       outline: 0; | ||||
|       background: darken($ui-base-color, 4%); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .button { | ||||
|     flex: 0 0 auto; | ||||
|     transition: background 300ms linear; | ||||
|   } | ||||
| 
 | ||||
|   &.copied { | ||||
|     input { | ||||
|       border: 1px solid $valid-value-color; | ||||
|       transition: none; | ||||
|     } | ||||
| 
 | ||||
|     .button { | ||||
|       background: $valid-value-color; | ||||
|       transition: none; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Reference in a new issue