Add option to open original page in dropdowns of remote content in web UI (#20299)
Change profile picture click to open profile picture in modal in web UI
This commit is contained in:
		
					parent
					
						
							
								e37e8deb0f
							
						
					
				
			
			
				commit
				
					
						ef582dc4f2
					
				
			
		
					 7 changed files with 111 additions and 40 deletions
				
			
		|  | @ -45,6 +45,7 @@ const messages = defineMessages({ | |||
|   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, | ||||
|   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, | ||||
|   filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, | ||||
|   openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = (state, { status }) => ({ | ||||
|  | @ -221,25 +222,10 @@ class StatusActionBar extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleCopy = () => { | ||||
|     const url      = this.props.status.get('url'); | ||||
|     const textarea = document.createElement('textarea'); | ||||
| 
 | ||||
|     textarea.textContent    = url; | ||||
|     textarea.style.position = 'fixed'; | ||||
| 
 | ||||
|     document.body.appendChild(textarea); | ||||
| 
 | ||||
|     try { | ||||
|       textarea.select(); | ||||
|       document.execCommand('copy'); | ||||
|     } catch (e) { | ||||
| 
 | ||||
|     } finally { | ||||
|       document.body.removeChild(textarea); | ||||
|     } | ||||
|     const url = this.props.status.get('url'); | ||||
|     navigator.clipboard.writeText(url); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   handleHideClick = () => { | ||||
|     this.props.onFilter(); | ||||
|   } | ||||
|  | @ -254,12 +240,17 @@ class StatusActionBar extends ImmutablePureComponent { | |||
|     const mutingConversation = status.get('muted'); | ||||
|     const account            = status.get('account'); | ||||
|     const writtenByMe        = status.getIn(['account', 'id']) === me; | ||||
|     const isRemote           = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); | ||||
| 
 | ||||
|     let menu = []; | ||||
| 
 | ||||
|     menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); | ||||
| 
 | ||||
|     if (publicStatus) { | ||||
|       if (isRemote) { | ||||
|         menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); | ||||
|       } | ||||
| 
 | ||||
|       menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); | ||||
|       menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); | ||||
|     } | ||||
|  |  | |||
|  | @ -53,6 +53,7 @@ const messages = defineMessages({ | |||
|   add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, | ||||
|   admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, | ||||
|   languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, | ||||
|   openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, | ||||
| }); | ||||
| 
 | ||||
| const titleFromAccount = account => { | ||||
|  | @ -97,6 +98,7 @@ class Header extends ImmutablePureComponent { | |||
|     onEditAccountNote: PropTypes.func.isRequired, | ||||
|     onChangeLanguages: PropTypes.func.isRequired, | ||||
|     onInteractionModal: PropTypes.func.isRequired, | ||||
|     onOpenAvatar: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     domain: PropTypes.string.isRequired, | ||||
|     hidden: PropTypes.bool, | ||||
|  | @ -140,6 +142,13 @@ class Header extends ImmutablePureComponent { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleAvatarClick = e => { | ||||
|     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { | ||||
|       e.preventDefault(); | ||||
|       this.props.onOpenAvatar(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, hidden, intl, domain } = this.props; | ||||
|     const { signedIn } = this.context.identity; | ||||
|  | @ -148,7 +157,9 @@ class Header extends ImmutablePureComponent { | |||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     const suspended = account.get('suspended'); | ||||
|     const suspended    = account.get('suspended'); | ||||
|     const isRemote     = account.get('acct') !== account.get('username'); | ||||
|     const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null; | ||||
| 
 | ||||
|     let info        = []; | ||||
|     let actionBtn   = ''; | ||||
|  | @ -200,6 +211,11 @@ class Header extends ImmutablePureComponent { | |||
|       menu.push(null); | ||||
|     } | ||||
| 
 | ||||
|     if (isRemote) { | ||||
|       menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); | ||||
|       menu.push(null); | ||||
|     } | ||||
| 
 | ||||
|     if ('share' in navigator) { | ||||
|       menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); | ||||
|       menu.push(null); | ||||
|  | @ -250,15 +266,13 @@ class Header extends ImmutablePureComponent { | |||
|       menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); | ||||
|     } | ||||
| 
 | ||||
|     if (signedIn && account.get('acct') !== account.get('username')) { | ||||
|       const domain = account.get('acct').split('@')[1]; | ||||
| 
 | ||||
|     if (signedIn && isRemote) { | ||||
|       menu.push(null); | ||||
| 
 | ||||
|       if (account.getIn(['relationship', 'domain_blocking'])) { | ||||
|         menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); | ||||
|         menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); | ||||
|       } else { | ||||
|         menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); | ||||
|         menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -296,7 +310,7 @@ class Header extends ImmutablePureComponent { | |||
| 
 | ||||
|         <div className='account__header__bar'> | ||||
|           <div className='account__header__tabs'> | ||||
|             <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> | ||||
|             <a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}> | ||||
|               <Avatar account={suspended || hidden ? undefined : account} size={90} /> | ||||
|             </a> | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ export default class Header extends ImmutablePureComponent { | |||
|     onAddToList: PropTypes.func.isRequired, | ||||
|     onChangeLanguages: PropTypes.func.isRequired, | ||||
|     onInteractionModal: PropTypes.func.isRequired, | ||||
|     onOpenAvatar: PropTypes.func.isRequired, | ||||
|     hideTabs: PropTypes.bool, | ||||
|     domain: PropTypes.string.isRequired, | ||||
|     hidden: PropTypes.bool, | ||||
|  | @ -101,6 +102,10 @@ export default class Header extends ImmutablePureComponent { | |||
|     this.props.onInteractionModal(this.props.account); | ||||
|   } | ||||
| 
 | ||||
|   handleOpenAvatar = () => { | ||||
|     this.props.onOpenAvatar(this.props.account); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, hidden, hideTabs } = this.props; | ||||
| 
 | ||||
|  | @ -129,6 +134,7 @@ export default class Header extends ImmutablePureComponent { | |||
|           onEditAccountNote={this.handleEditAccountNote} | ||||
|           onChangeLanguages={this.handleChangeLanguages} | ||||
|           onInteractionModal={this.handleInteractionModal} | ||||
|           onOpenAvatar={this.handleOpenAvatar} | ||||
|           domain={this.props.domain} | ||||
|           hidden={hidden} | ||||
|         /> | ||||
|  |  | |||
|  | @ -152,6 +152,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ | |||
|     })); | ||||
|   }, | ||||
| 
 | ||||
|   onOpenAvatar (account) { | ||||
|     dispatch(openModal('IMAGE', { | ||||
|       src: account.get('avatar'), | ||||
|       alt: account.get('acct'), | ||||
|     })); | ||||
|   }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ const messages = defineMessages({ | |||
|   unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, | ||||
|   unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, | ||||
|   unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, | ||||
|   openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = (state, { status }) => ({ | ||||
|  | @ -174,22 +175,8 @@ class ActionBar extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleCopy = () => { | ||||
|     const url      = this.props.status.get('url'); | ||||
|     const textarea = document.createElement('textarea'); | ||||
| 
 | ||||
|     textarea.textContent    = url; | ||||
|     textarea.style.position = 'fixed'; | ||||
| 
 | ||||
|     document.body.appendChild(textarea); | ||||
| 
 | ||||
|     try { | ||||
|       textarea.select(); | ||||
|       document.execCommand('copy'); | ||||
|     } catch (e) { | ||||
| 
 | ||||
|     } finally { | ||||
|       document.body.removeChild(textarea); | ||||
|     } | ||||
|     const url = this.props.status.get('url'); | ||||
|     navigator.clipboard.writeText(url); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|  | @ -201,10 +188,15 @@ class ActionBar extends React.PureComponent { | |||
|     const mutingConversation = status.get('muted'); | ||||
|     const account            = status.get('account'); | ||||
|     const writtenByMe        = status.getIn(['account', 'id']) === me; | ||||
|     const isRemote           = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); | ||||
| 
 | ||||
|     let menu = []; | ||||
| 
 | ||||
|     if (publicStatus) { | ||||
|       if (isRemote) { | ||||
|         menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); | ||||
|       } | ||||
| 
 | ||||
|       menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); | ||||
|       menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); | ||||
|       menu.push(null); | ||||
|  |  | |||
|  | @ -0,0 +1,59 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import classNames from 'classnames'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import IconButton from 'mastodon/components/icon_button'; | ||||
| import ImageLoader from './image_loader'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
| }); | ||||
| 
 | ||||
| export default @injectIntl | ||||
| class ImageModal extends React.PureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     src: PropTypes.string.isRequired, | ||||
|     alt: PropTypes.string.isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     navigationHidden: false, | ||||
|   }; | ||||
| 
 | ||||
|   toggleNavigation = () => { | ||||
|     this.setState(prevState => ({ | ||||
|       navigationHidden: !prevState.navigationHidden, | ||||
|     })); | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl, src, alt, onClose } = this.props; | ||||
|     const { navigationHidden } = this.state; | ||||
| 
 | ||||
|     const navigationClassName = classNames('media-modal__navigation', { | ||||
|       'media-modal__navigation--hidden': navigationHidden, | ||||
|     }); | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='modal-root__modal media-modal'> | ||||
|         <div className='media-modal__closer' role='presentation' onClick={onClose} > | ||||
|           <ImageLoader | ||||
|             src={src} | ||||
|             width={400} | ||||
|             height={400} | ||||
|             alt={alt} | ||||
|             onClick={this.toggleNavigation} | ||||
|           /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className={navigationClassName}> | ||||
|           <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | @ -12,6 +12,7 @@ import BoostModal from './boost_modal'; | |||
| import AudioModal from './audio_modal'; | ||||
| import ConfirmationModal from './confirmation_modal'; | ||||
| import FocalPointModal from './focal_point_modal'; | ||||
| import ImageModal from './image_modal'; | ||||
| import { | ||||
|   MuteModal, | ||||
|   BlockModal, | ||||
|  | @ -31,6 +32,7 @@ const MODAL_COMPONENTS = { | |||
|   'MEDIA': () => Promise.resolve({ default: MediaModal }), | ||||
|   'VIDEO': () => Promise.resolve({ default: VideoModal }), | ||||
|   'AUDIO': () => Promise.resolve({ default: AudioModal }), | ||||
|   'IMAGE': () => Promise.resolve({ default: ImageModal }), | ||||
|   'BOOST': () => Promise.resolve({ default: BoostModal }), | ||||
|   'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), | ||||
|   'MUTE': MuteModal, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue