Remove global boosts state and convert boosts modal to Typescript (#29774)
		
	This commit is contained in:
		
					parent
					
						
							
								8a498f4e65
							
						
					
				
			
			
				commit
				
					
						67442f9039
					
				
			
		
					 13 changed files with 175 additions and 198 deletions
				
			
		|  | @ -1,125 +0,0 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import classNames from 'classnames'; | ||||
| import { withRouter } from 'react-router-dom'; | ||||
| 
 | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { connect } from 'react-redux'; | ||||
| 
 | ||||
| import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; | ||||
| import { changeBoostPrivacy } from 'mastodon/actions/boosts'; | ||||
| import AttachmentList from 'mastodon/components/attachment_list'; | ||||
| import { Icon }  from 'mastodon/components/icon'; | ||||
| import { VisibilityIcon } from 'mastodon/components/visibility_icon'; | ||||
| import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown'; | ||||
| import { WithRouterPropTypes } from 'mastodon/utils/react_router'; | ||||
| 
 | ||||
| import { Avatar } from '../../../components/avatar'; | ||||
| import { Button } from '../../../components/button'; | ||||
| import { DisplayName } from '../../../components/display_name'; | ||||
| import { RelativeTimestamp } from '../../../components/relative_timestamp'; | ||||
| import StatusContent from '../../../components/status_content'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, | ||||
|   reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => { | ||||
|   return { | ||||
|     privacy: state.getIn(['boosts', 'new', 'privacy']), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const mapDispatchToProps = dispatch => { | ||||
|   return { | ||||
|     onChangeBoostPrivacy(value) { | ||||
|       dispatch(changeBoostPrivacy(value)); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| class BoostModal extends ImmutablePureComponent { | ||||
|   static propTypes = { | ||||
|     status: ImmutablePropTypes.map.isRequired, | ||||
|     onReblog: PropTypes.func.isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     onChangeBoostPrivacy: PropTypes.func.isRequired, | ||||
|     privacy: PropTypes.string.isRequired, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     ...WithRouterPropTypes, | ||||
|   }; | ||||
| 
 | ||||
|   handleReblog = () => { | ||||
|     this.props.onReblog(this.props.status, this.props.privacy); | ||||
|     this.props.onClose(); | ||||
|   }; | ||||
| 
 | ||||
|   handleAccountClick = (e) => { | ||||
|     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { | ||||
|       e.preventDefault(); | ||||
|       this.props.onClose(); | ||||
|       this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   _findContainer = () => { | ||||
|     return document.getElementsByClassName('modal-root__container')[0]; | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { status, privacy, intl } = this.props; | ||||
|     const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className='modal-root__modal boost-modal'> | ||||
|         <div className='boost-modal__container'> | ||||
|           <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}> | ||||
|             <div className='status__info'> | ||||
|               <a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'> | ||||
|                 <span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span> | ||||
|                 <RelativeTimestamp timestamp={status.get('created_at')} /> | ||||
|               </a> | ||||
| 
 | ||||
|               <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name'> | ||||
|                 <div className='status__avatar'> | ||||
|                   <Avatar account={status.get('account')} size={48} /> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <DisplayName account={status.get('account')} /> | ||||
|               </a> | ||||
|             </div> | ||||
| 
 | ||||
|             <StatusContent status={status} /> | ||||
| 
 | ||||
|             {status.get('media_attachments').size > 0 && ( | ||||
|               <AttachmentList | ||||
|                 compact | ||||
|                 media={status.get('media_attachments')} | ||||
|               /> | ||||
|             )} | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='boost-modal__action-bar'> | ||||
|           <div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' icon={RepeatIcon} /></span> }} /></div> | ||||
|           {status.get('visibility') !== 'private' && !status.get('reblogged') && ( | ||||
|             <PrivacyDropdown | ||||
|               noDirect | ||||
|               value={privacy} | ||||
|               container={this._findContainer} | ||||
|               onChange={this.props.onChangeBoostPrivacy} | ||||
|             /> | ||||
|           )} | ||||
|           <Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus /> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal))); | ||||
							
								
								
									
										162
									
								
								app/javascript/mastodon/features/ui/components/boost_modal.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								app/javascript/mastodon/features/ui/components/boost_modal.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,162 @@ | |||
| import type { MouseEventHandler } from 'react'; | ||||
| import { useCallback, useState } from 'react'; | ||||
| 
 | ||||
| import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; | ||||
| 
 | ||||
| import classNames from 'classnames'; | ||||
| import { useHistory } from 'react-router'; | ||||
| 
 | ||||
| import type Immutable from 'immutable'; | ||||
| 
 | ||||
| import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; | ||||
| import AttachmentList from 'mastodon/components/attachment_list'; | ||||
| import { Icon } from 'mastodon/components/icon'; | ||||
| import { VisibilityIcon } from 'mastodon/components/visibility_icon'; | ||||
| import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown'; | ||||
| import type { Account } from 'mastodon/models/account'; | ||||
| import type { Status, StatusVisibility } from 'mastodon/models/status'; | ||||
| import { useAppSelector } from 'mastodon/store'; | ||||
| 
 | ||||
| import { Avatar } from '../../../components/avatar'; | ||||
| import { Button } from '../../../components/button'; | ||||
| import { DisplayName } from '../../../components/display_name'; | ||||
| import { RelativeTimestamp } from '../../../components/relative_timestamp'; | ||||
| import StatusContent from '../../../components/status_content'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   cancel_reblog: { | ||||
|     id: 'status.cancel_reblog_private', | ||||
|     defaultMessage: 'Unboost', | ||||
|   }, | ||||
|   reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, | ||||
| }); | ||||
| 
 | ||||
| export const BoostModal: React.FC<{ | ||||
|   status: Status; | ||||
|   onClose: () => void; | ||||
|   onReblog: (status: Status, privacy: StatusVisibility) => void; | ||||
| }> = ({ status, onReblog, onClose }) => { | ||||
|   const intl = useIntl(); | ||||
|   const history = useHistory(); | ||||
| 
 | ||||
|   const default_privacy = useAppSelector( | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | ||||
|     (state) => state.compose.get('default_privacy') as StatusVisibility, | ||||
|   ); | ||||
| 
 | ||||
|   const account = status.get('account') as Account; | ||||
|   const statusVisibility = status.get('visibility') as StatusVisibility; | ||||
| 
 | ||||
|   const [privacy, setPrivacy] = useState<StatusVisibility>( | ||||
|     statusVisibility === 'private' ? 'private' : default_privacy, | ||||
|   ); | ||||
| 
 | ||||
|   const onPrivacyChange = useCallback((value: StatusVisibility) => { | ||||
|     setPrivacy(value); | ||||
|   }, []); | ||||
| 
 | ||||
|   const handleReblog = useCallback(() => { | ||||
|     onReblog(status, privacy); | ||||
|     onClose(); | ||||
|   }, [onClose, onReblog, status, privacy]); | ||||
| 
 | ||||
|   const handleAccountClick = useCallback<MouseEventHandler>( | ||||
|     (e) => { | ||||
|       if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { | ||||
|         e.preventDefault(); | ||||
|         onClose(); | ||||
|         history.push(`/@${account.acct}`); | ||||
|       } | ||||
|     }, | ||||
|     [history, onClose, account], | ||||
|   ); | ||||
| 
 | ||||
|   const buttonText = status.get('reblogged') | ||||
|     ? messages.cancel_reblog | ||||
|     : messages.reblog; | ||||
| 
 | ||||
|   const findContainer = useCallback( | ||||
|     () => document.getElementsByClassName('modal-root__container')[0], | ||||
|     [], | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className='modal-root__modal boost-modal'> | ||||
|       <div className='boost-modal__container'> | ||||
|         <div | ||||
|           className={classNames( | ||||
|             'status', | ||||
|             `status-${statusVisibility}`, | ||||
|             'light', | ||||
|           )} | ||||
|         > | ||||
|           <div className='status__info'> | ||||
|             <a | ||||
|               href={`/@${account.acct}/${status.get('id') as string}`} | ||||
|               className='status__relative-time' | ||||
|               target='_blank' | ||||
|               rel='noopener noreferrer' | ||||
|             > | ||||
|               <span className='status__visibility-icon'> | ||||
|                 <VisibilityIcon visibility={statusVisibility} /> | ||||
|               </span> | ||||
|               <RelativeTimestamp | ||||
|                 timestamp={status.get('created_at') as string} | ||||
|               /> | ||||
|             </a> | ||||
| 
 | ||||
|             <a | ||||
|               onClick={handleAccountClick} | ||||
|               href={`/@${account.acct}`} | ||||
|               className='status__display-name' | ||||
|             > | ||||
|               <div className='status__avatar'> | ||||
|                 <Avatar account={account} size={48} /> | ||||
|               </div> | ||||
| 
 | ||||
|               <DisplayName account={account} /> | ||||
|             </a> | ||||
|           </div> | ||||
| 
 | ||||
|           {/* @ts-expect-error Expected until StatusContent is typed */} | ||||
|           <StatusContent status={status} /> | ||||
| 
 | ||||
|           {(status.get('media_attachments') as Immutable.List<unknown>).size > | ||||
|             0 && ( | ||||
|             <AttachmentList compact media={status.get('media_attachments')} /> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className='boost-modal__action-bar'> | ||||
|         <div> | ||||
|           <FormattedMessage | ||||
|             id='boost_modal.combo' | ||||
|             defaultMessage='You can press {combo} to skip this next time' | ||||
|             values={{ | ||||
|               combo: ( | ||||
|                 <span> | ||||
|                   Shift + <Icon id='retweet' icon={RepeatIcon} /> | ||||
|                 </span> | ||||
|               ), | ||||
|             }} | ||||
|           /> | ||||
|         </div> | ||||
|         {statusVisibility !== 'private' && !status.get('reblogged') && ( | ||||
|           <PrivacyDropdown | ||||
|             noDirect | ||||
|             value={privacy} | ||||
|             container={findContainer} | ||||
|             onChange={onPrivacyChange} | ||||
|           /> | ||||
|         )} | ||||
|         <Button | ||||
|           text={intl.formatMessage(buttonText)} | ||||
|           onClick={handleReblog} | ||||
|           // eslint-disable-next-line jsx-a11y/no-autofocus
 | ||||
|           autoFocus | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | @ -24,7 +24,7 @@ import BundleContainer from '../containers/bundle_container'; | |||
| 
 | ||||
| import ActionsModal from './actions_modal'; | ||||
| import AudioModal from './audio_modal'; | ||||
| import BoostModal from './boost_modal'; | ||||
| import { BoostModal } from './boost_modal'; | ||||
| import BundleModalError from './bundle_modal_error'; | ||||
| import ConfirmationModal from './confirmation_modal'; | ||||
| import FocalPointModal from './focal_point_modal'; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue