Refactor <HashtagHeader> to TypeScript (#33096)
		
	This commit is contained in:
		
					parent
					
						
							
								a1143c522b
							
						
					
				
			
			
				commit
				
					
						25387dc423
					
				
			
		
					 10 changed files with 238 additions and 237 deletions
				
			
		|  | @ -1,9 +1,5 @@ | ||||||
| import api, { getLinks } from '../api'; | import api, { getLinks } from '../api'; | ||||||
| 
 | 
 | ||||||
| export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; |  | ||||||
| export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; |  | ||||||
| export const HASHTAG_FETCH_FAIL    = 'HASHTAG_FETCH_FAIL'; |  | ||||||
| 
 |  | ||||||
| export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; | export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; | ||||||
| export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; | export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; | ||||||
| export const FOLLOWED_HASHTAGS_FETCH_FAIL    = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; | export const FOLLOWED_HASHTAGS_FETCH_FAIL    = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; | ||||||
|  | @ -12,39 +8,6 @@ export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUES | ||||||
| export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; | export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; | ||||||
| export const FOLLOWED_HASHTAGS_EXPAND_FAIL    = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; | export const FOLLOWED_HASHTAGS_EXPAND_FAIL    = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; | ||||||
| 
 | 
 | ||||||
| export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; |  | ||||||
| export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; |  | ||||||
| export const HASHTAG_FOLLOW_FAIL    = 'HASHTAG_FOLLOW_FAIL'; |  | ||||||
| 
 |  | ||||||
| export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST'; |  | ||||||
| export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS'; |  | ||||||
| export const HASHTAG_UNFOLLOW_FAIL    = 'HASHTAG_UNFOLLOW_FAIL'; |  | ||||||
| 
 |  | ||||||
| export const fetchHashtag = name => (dispatch) => { |  | ||||||
|   dispatch(fetchHashtagRequest()); |  | ||||||
| 
 |  | ||||||
|   api().get(`/api/v1/tags/${name}`).then(({ data }) => { |  | ||||||
|     dispatch(fetchHashtagSuccess(name, data)); |  | ||||||
|   }).catch(err => { |  | ||||||
|     dispatch(fetchHashtagFail(err)); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const fetchHashtagRequest = () => ({ |  | ||||||
|   type: HASHTAG_FETCH_REQUEST, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const fetchHashtagSuccess = (name, tag) => ({ |  | ||||||
|   type: HASHTAG_FETCH_SUCCESS, |  | ||||||
|   name, |  | ||||||
|   tag, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const fetchHashtagFail = error => ({ |  | ||||||
|   type: HASHTAG_FETCH_FAIL, |  | ||||||
|   error, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const fetchFollowedHashtags = () => (dispatch) => { | export const fetchFollowedHashtags = () => (dispatch) => { | ||||||
|   dispatch(fetchFollowedHashtagsRequest()); |   dispatch(fetchFollowedHashtagsRequest()); | ||||||
| 
 | 
 | ||||||
|  | @ -116,57 +79,3 @@ export function expandFollowedHashtagsFail(error) { | ||||||
|     error, |     error, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export const followHashtag = name => (dispatch) => { |  | ||||||
|   dispatch(followHashtagRequest(name)); |  | ||||||
| 
 |  | ||||||
|   api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => { |  | ||||||
|     dispatch(followHashtagSuccess(name, data)); |  | ||||||
|   }).catch(err => { |  | ||||||
|     dispatch(followHashtagFail(name, err)); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const followHashtagRequest = name => ({ |  | ||||||
|   type: HASHTAG_FOLLOW_REQUEST, |  | ||||||
|   name, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const followHashtagSuccess = (name, tag) => ({ |  | ||||||
|   type: HASHTAG_FOLLOW_SUCCESS, |  | ||||||
|   name, |  | ||||||
|   tag, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const followHashtagFail = (name, error) => ({ |  | ||||||
|   type: HASHTAG_FOLLOW_FAIL, |  | ||||||
|   name, |  | ||||||
|   error, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const unfollowHashtag = name => (dispatch) => { |  | ||||||
|   dispatch(unfollowHashtagRequest(name)); |  | ||||||
| 
 |  | ||||||
|   api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => { |  | ||||||
|     dispatch(unfollowHashtagSuccess(name, data)); |  | ||||||
|   }).catch(err => { |  | ||||||
|     dispatch(unfollowHashtagFail(name, err)); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export const unfollowHashtagRequest = name => ({ |  | ||||||
|   type: HASHTAG_UNFOLLOW_REQUEST, |  | ||||||
|   name, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const unfollowHashtagSuccess = (name, tag) => ({ |  | ||||||
|   type: HASHTAG_UNFOLLOW_SUCCESS, |  | ||||||
|   name, |  | ||||||
|   tag, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| export const unfollowHashtagFail = (name, error) => ({ |  | ||||||
|   type: HASHTAG_UNFOLLOW_FAIL, |  | ||||||
|   name, |  | ||||||
|   error, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								app/javascript/mastodon/actions/tags_typed.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/javascript/mastodon/actions/tags_typed.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags'; | ||||||
|  | import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; | ||||||
|  | 
 | ||||||
|  | export const fetchHashtag = createDataLoadingThunk( | ||||||
|  |   'tags/fetch', | ||||||
|  |   ({ tagId }: { tagId: string }) => apiGetTag(tagId), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const followHashtag = createDataLoadingThunk( | ||||||
|  |   'tags/follow', | ||||||
|  |   ({ tagId }: { tagId: string }) => apiFollowTag(tagId), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const unfollowHashtag = createDataLoadingThunk( | ||||||
|  |   'tags/unfollow', | ||||||
|  |   ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId), | ||||||
|  | ); | ||||||
							
								
								
									
										11
									
								
								app/javascript/mastodon/api/tags.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/javascript/mastodon/api/tags.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | import { apiRequestPost, apiRequestGet } from 'mastodon/api'; | ||||||
|  | import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; | ||||||
|  | 
 | ||||||
|  | export const apiGetTag = (tagId: string) => | ||||||
|  |   apiRequestGet<ApiHashtagJSON>(`v1/tags/${tagId}`); | ||||||
|  | 
 | ||||||
|  | export const apiFollowTag = (tagId: string) => | ||||||
|  |   apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/follow`); | ||||||
|  | 
 | ||||||
|  | export const apiUnfollowTag = (tagId: string) => | ||||||
|  |   apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`); | ||||||
							
								
								
									
										13
									
								
								app/javascript/mastodon/api_types/tags.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/javascript/mastodon/api_types/tags.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | interface ApiHistoryJSON { | ||||||
|  |   day: string; | ||||||
|  |   accounts: string; | ||||||
|  |   uses: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface ApiHashtagJSON { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   url: string; | ||||||
|  |   history: [ApiHistoryJSON, ...ApiHistoryJSON[]]; | ||||||
|  |   following?: boolean; | ||||||
|  | } | ||||||
|  | @ -1,94 +0,0 @@ | ||||||
| import PropTypes from 'prop-types'; |  | ||||||
| 
 |  | ||||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; |  | ||||||
| 
 |  | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; |  | ||||||
| 
 |  | ||||||
| import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; |  | ||||||
| import { Button } from 'mastodon/components/button'; |  | ||||||
| import { ShortNumber } from 'mastodon/components/short_number'; |  | ||||||
| import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; |  | ||||||
| import { withIdentity } from 'mastodon/identity_context'; |  | ||||||
| import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions'; |  | ||||||
| 
 |  | ||||||
| const messages = defineMessages({ |  | ||||||
|   followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, |  | ||||||
|   unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, |  | ||||||
|   adminModeration: { id: 'hashtag.admin_moderation', defaultMessage: 'Open moderation interface for #{name}' }, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const usesRenderer = (displayNumber, pluralReady) => ( |  | ||||||
|   <FormattedMessage |  | ||||||
|     id='hashtag.counter_by_uses' |  | ||||||
|     defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}' |  | ||||||
|     values={{ |  | ||||||
|       count: pluralReady, |  | ||||||
|       counter: <strong>{displayNumber}</strong>, |  | ||||||
|     }} |  | ||||||
|   /> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const peopleRenderer = (displayNumber, pluralReady) => ( |  | ||||||
|   <FormattedMessage |  | ||||||
|     id='hashtag.counter_by_accounts' |  | ||||||
|     defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}' |  | ||||||
|     values={{ |  | ||||||
|       count: pluralReady, |  | ||||||
|       counter: <strong>{displayNumber}</strong>, |  | ||||||
|     }} |  | ||||||
|   /> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const usesTodayRenderer = (displayNumber, pluralReady) => ( |  | ||||||
|   <FormattedMessage |  | ||||||
|     id='hashtag.counter_by_uses_today' |  | ||||||
|     defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today' |  | ||||||
|     values={{ |  | ||||||
|       count: pluralReady, |  | ||||||
|       counter: <strong>{displayNumber}</strong>, |  | ||||||
|     }} |  | ||||||
|   /> |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| export const HashtagHeader = withIdentity(injectIntl(({ tag, intl, disabled, onClick, identity }) => { |  | ||||||
|   if (!tag) { |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const { signedIn, permissions } = identity; |  | ||||||
|   const menu = []; |  | ||||||
| 
 |  | ||||||
|   if (signedIn && (permissions & PERMISSION_MANAGE_TAXONOMIES) === PERMISSION_MANAGE_TAXONOMIES ) { |  | ||||||
|     menu.push({ text: intl.formatMessage(messages.adminModeration, { name: tag.get("name") }), href: `/admin/tags/${tag.get('id')}` }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]); |  | ||||||
|   const dividingCircle = <span aria-hidden>{' · '}</span>; |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <div className='hashtag-header'> |  | ||||||
|       <div className='hashtag-header__header'> |  | ||||||
|         <h1>#{tag.get('name')}</h1> |  | ||||||
|         <div className='hashtag-header__header__buttons'> |  | ||||||
|           { menu.length > 0 && <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> } |  | ||||||
|           <Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div> |  | ||||||
|         <ShortNumber value={uses} renderer={usesRenderer} /> |  | ||||||
|         {dividingCircle} |  | ||||||
|         <ShortNumber value={people} renderer={peopleRenderer} /> |  | ||||||
|         {dividingCircle} |  | ||||||
|         <ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} /> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| })); |  | ||||||
| 
 |  | ||||||
| HashtagHeader.propTypes = { |  | ||||||
|   tag: ImmutablePropTypes.map, |  | ||||||
|   disabled: PropTypes.bool, |  | ||||||
|   onClick: PropTypes.func, |  | ||||||
|   intl: PropTypes.object, |  | ||||||
| }; |  | ||||||
|  | @ -0,0 +1,188 @@ | ||||||
|  | import { useCallback, useMemo, useState, useEffect } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; | ||||||
|  | 
 | ||||||
|  | import { isFulfilled } from '@reduxjs/toolkit'; | ||||||
|  | 
 | ||||||
|  | import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; | ||||||
|  | import { | ||||||
|  |   fetchHashtag, | ||||||
|  |   followHashtag, | ||||||
|  |   unfollowHashtag, | ||||||
|  | } from 'mastodon/actions/tags_typed'; | ||||||
|  | import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; | ||||||
|  | import { Button } from 'mastodon/components/button'; | ||||||
|  | import { ShortNumber } from 'mastodon/components/short_number'; | ||||||
|  | import DropdownMenu from 'mastodon/containers/dropdown_menu_container'; | ||||||
|  | import { useIdentity } from 'mastodon/identity_context'; | ||||||
|  | import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions'; | ||||||
|  | import { useAppDispatch } from 'mastodon/store'; | ||||||
|  | 
 | ||||||
|  | const messages = defineMessages({ | ||||||
|  |   followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, | ||||||
|  |   unfollowHashtag: { | ||||||
|  |     id: 'hashtag.unfollow', | ||||||
|  |     defaultMessage: 'Unfollow hashtag', | ||||||
|  |   }, | ||||||
|  |   adminModeration: { | ||||||
|  |     id: 'hashtag.admin_moderation', | ||||||
|  |     defaultMessage: 'Open moderation interface for #{name}', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const usesRenderer = (displayNumber: React.ReactNode, pluralReady: number) => ( | ||||||
|  |   <FormattedMessage | ||||||
|  |     id='hashtag.counter_by_uses' | ||||||
|  |     defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}' | ||||||
|  |     values={{ | ||||||
|  |       count: pluralReady, | ||||||
|  |       counter: <strong>{displayNumber}</strong>, | ||||||
|  |     }} | ||||||
|  |   /> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const peopleRenderer = ( | ||||||
|  |   displayNumber: React.ReactNode, | ||||||
|  |   pluralReady: number, | ||||||
|  | ) => ( | ||||||
|  |   <FormattedMessage | ||||||
|  |     id='hashtag.counter_by_accounts' | ||||||
|  |     defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}' | ||||||
|  |     values={{ | ||||||
|  |       count: pluralReady, | ||||||
|  |       counter: <strong>{displayNumber}</strong>, | ||||||
|  |     }} | ||||||
|  |   /> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const usesTodayRenderer = ( | ||||||
|  |   displayNumber: React.ReactNode, | ||||||
|  |   pluralReady: number, | ||||||
|  | ) => ( | ||||||
|  |   <FormattedMessage | ||||||
|  |     id='hashtag.counter_by_uses_today' | ||||||
|  |     defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today' | ||||||
|  |     values={{ | ||||||
|  |       count: pluralReady, | ||||||
|  |       counter: <strong>{displayNumber}</strong>, | ||||||
|  |     }} | ||||||
|  |   /> | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const HashtagHeader: React.FC<{ | ||||||
|  |   tagId: string; | ||||||
|  | }> = ({ tagId }) => { | ||||||
|  |   const intl = useIntl(); | ||||||
|  |   const { signedIn, permissions } = useIdentity(); | ||||||
|  |   const dispatch = useAppDispatch(); | ||||||
|  |   const [tag, setTag] = useState<ApiHashtagJSON>(); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     void dispatch(fetchHashtag({ tagId })).then((result) => { | ||||||
|  |       if (isFulfilled(result)) { | ||||||
|  |         setTag(result.payload); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return ''; | ||||||
|  |     }); | ||||||
|  |   }, [dispatch, tagId, setTag]); | ||||||
|  | 
 | ||||||
|  |   const menu = useMemo(() => { | ||||||
|  |     const tmp = []; | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       tag && | ||||||
|  |       signedIn && | ||||||
|  |       (permissions & PERMISSION_MANAGE_TAXONOMIES) === | ||||||
|  |         PERMISSION_MANAGE_TAXONOMIES | ||||||
|  |     ) { | ||||||
|  |       tmp.push({ | ||||||
|  |         text: intl.formatMessage(messages.adminModeration, { name: tag.id }), | ||||||
|  |         href: `/admin/tags/${tag.id}`, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return tmp; | ||||||
|  |   }, [signedIn, permissions, intl, tag]); | ||||||
|  | 
 | ||||||
|  |   const handleFollow = useCallback(() => { | ||||||
|  |     if (!signedIn || !tag) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (tag.following) { | ||||||
|  |       setTag((hashtag) => hashtag && { ...hashtag, following: false }); | ||||||
|  | 
 | ||||||
|  |       void dispatch(unfollowHashtag({ tagId })).then((result) => { | ||||||
|  |         if (isFulfilled(result)) { | ||||||
|  |           setTag(result.payload); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ''; | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       setTag((hashtag) => hashtag && { ...hashtag, following: true }); | ||||||
|  | 
 | ||||||
|  |       void dispatch(followHashtag({ tagId })).then((result) => { | ||||||
|  |         if (isFulfilled(result)) { | ||||||
|  |           setTag(result.payload); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ''; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, [dispatch, setTag, signedIn, tag, tagId]); | ||||||
|  | 
 | ||||||
|  |   if (!tag) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const [uses, people] = tag.history.reduce( | ||||||
|  |     (arr, day) => [ | ||||||
|  |       arr[0] + parseInt(day.uses), | ||||||
|  |       arr[1] + parseInt(day.accounts), | ||||||
|  |     ], | ||||||
|  |     [0, 0], | ||||||
|  |   ); | ||||||
|  |   const dividingCircle = <span aria-hidden>{' · '}</span>; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className='hashtag-header'> | ||||||
|  |       <div className='hashtag-header__header'> | ||||||
|  |         <h1>#{tag.name}</h1> | ||||||
|  | 
 | ||||||
|  |         <div className='hashtag-header__header__buttons'> | ||||||
|  |           {menu.length > 0 && ( | ||||||
|  |             <DropdownMenu | ||||||
|  |               disabled={menu.length === 0} | ||||||
|  |               items={menu} | ||||||
|  |               icon='ellipsis-v' | ||||||
|  |               iconComponent={MoreHorizIcon} | ||||||
|  |               size={24} | ||||||
|  |               direction='right' | ||||||
|  |             /> | ||||||
|  |           )} | ||||||
|  | 
 | ||||||
|  |           <Button | ||||||
|  |             onClick={handleFollow} | ||||||
|  |             text={intl.formatMessage( | ||||||
|  |               tag.following ? messages.unfollowHashtag : messages.followHashtag, | ||||||
|  |             )} | ||||||
|  |             disabled={!signedIn} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div> | ||||||
|  |         <ShortNumber value={uses} renderer={usesRenderer} /> | ||||||
|  |         {dividingCircle} | ||||||
|  |         <ShortNumber value={people} renderer={peopleRenderer} /> | ||||||
|  |         {dividingCircle} | ||||||
|  |         <ShortNumber | ||||||
|  |           value={parseInt(tag.history[0].uses)} | ||||||
|  |           renderer={usesTodayRenderer} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl'; | ||||||
| 
 | 
 | ||||||
| import { Helmet } from 'react-helmet'; | import { Helmet } from 'react-helmet'; | ||||||
| 
 | 
 | ||||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; |  | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| 
 | 
 | ||||||
| import { isEqual } from 'lodash'; | import { isEqual } from 'lodash'; | ||||||
|  | @ -13,7 +12,6 @@ import { isEqual } from 'lodash'; | ||||||
| import TagIcon from '@/material-icons/400-24px/tag.svg?react'; | import TagIcon from '@/material-icons/400-24px/tag.svg?react'; | ||||||
| import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; | import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; | ||||||
| import { connectHashtagStream } from 'mastodon/actions/streaming'; | import { connectHashtagStream } from 'mastodon/actions/streaming'; | ||||||
| import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags'; |  | ||||||
| import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines'; | import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines'; | ||||||
| import Column from 'mastodon/components/column'; | import Column from 'mastodon/components/column'; | ||||||
| import ColumnHeader from 'mastodon/components/column_header'; | import ColumnHeader from 'mastodon/components/column_header'; | ||||||
|  | @ -26,7 +24,6 @@ import ColumnSettingsContainer from './containers/column_settings_container'; | ||||||
| 
 | 
 | ||||||
| const mapStateToProps = (state, props) => ({ | const mapStateToProps = (state, props) => ({ | ||||||
|   hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, |   hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, | ||||||
|   tag: state.getIn(['tags', props.params.id]), |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| class HashtagTimeline extends PureComponent { | class HashtagTimeline extends PureComponent { | ||||||
|  | @ -38,7 +35,6 @@ class HashtagTimeline extends PureComponent { | ||||||
|     columnId: PropTypes.string, |     columnId: PropTypes.string, | ||||||
|     dispatch: PropTypes.func.isRequired, |     dispatch: PropTypes.func.isRequired, | ||||||
|     hasUnread: PropTypes.bool, |     hasUnread: PropTypes.bool, | ||||||
|     tag: ImmutablePropTypes.map, |  | ||||||
|     multiColumn: PropTypes.bool, |     multiColumn: PropTypes.bool, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -130,7 +126,6 @@ class HashtagTimeline extends PureComponent { | ||||||
| 
 | 
 | ||||||
|     this._subscribe(dispatch, id, tags, local); |     this._subscribe(dispatch, id, tags, local); | ||||||
|     dispatch(expandHashtagTimeline(id, { tags, local })); |     dispatch(expandHashtagTimeline(id, { tags, local })); | ||||||
|     dispatch(fetchHashtag(id)); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   componentDidMount () { | ||||||
|  | @ -162,27 +157,10 @@ class HashtagTimeline extends PureComponent { | ||||||
|     dispatch(expandHashtagTimeline(id, { maxId, tags, local })); |     dispatch(expandHashtagTimeline(id, { maxId, tags, local })); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   handleFollow = () => { |  | ||||||
|     const { dispatch, params, tag } = this.props; |  | ||||||
|     const { id } = params; |  | ||||||
|     const { signedIn } = this.props.identity; |  | ||||||
| 
 |  | ||||||
|     if (!signedIn) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (tag.get('following')) { |  | ||||||
|       dispatch(unfollowHashtag(id)); |  | ||||||
|     } else { |  | ||||||
|       dispatch(followHashtag(id)); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   render () { |   render () { | ||||||
|     const { hasUnread, columnId, multiColumn, tag } = this.props; |     const { hasUnread, columnId, multiColumn } = this.props; | ||||||
|     const { id, local } = this.props.params; |     const { id, local } = this.props.params; | ||||||
|     const pinned = !!columnId; |     const pinned = !!columnId; | ||||||
|     const { signedIn } = this.props.identity; |  | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> |       <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> | ||||||
|  | @ -202,7 +180,7 @@ class HashtagTimeline extends PureComponent { | ||||||
|         </ColumnHeader> |         </ColumnHeader> | ||||||
| 
 | 
 | ||||||
|         <StatusListContainer |         <StatusListContainer | ||||||
|           prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />} |           prepend={pinned ? null : <HashtagHeader tagId={id} />} | ||||||
|           alwaysPrepend |           alwaysPrepend | ||||||
|           trackScroll={!pinned} |           trackScroll={!pinned} | ||||||
|           scrollKey={`hashtag_timeline-${columnId}`} |           scrollKey={`hashtag_timeline-${columnId}`} | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								app/javascript/mastodon/models/tags.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/javascript/mastodon/models/tags.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; | ||||||
|  | 
 | ||||||
|  | export type Hashtag = ApiHashtagJSON; | ||||||
|  | 
 | ||||||
|  | export const createHashtag = (serverJSON: ApiHashtagJSON): Hashtag => ({ | ||||||
|  |   ...serverJSON, | ||||||
|  | }); | ||||||
|  | @ -36,7 +36,6 @@ import settings from './settings'; | ||||||
| import status_lists from './status_lists'; | import status_lists from './status_lists'; | ||||||
| import statuses from './statuses'; | import statuses from './statuses'; | ||||||
| import { suggestionsReducer } from './suggestions'; | import { suggestionsReducer } from './suggestions'; | ||||||
| import tags from './tags'; |  | ||||||
| import timelines from './timelines'; | import timelines from './timelines'; | ||||||
| import trends from './trends'; | import trends from './trends'; | ||||||
| import user_lists from './user_lists'; | import user_lists from './user_lists'; | ||||||
|  | @ -76,7 +75,6 @@ const reducers = { | ||||||
|   markers: markersReducer, |   markers: markersReducer, | ||||||
|   picture_in_picture: pictureInPictureReducer, |   picture_in_picture: pictureInPictureReducer, | ||||||
|   history, |   history, | ||||||
|   tags, |  | ||||||
|   followed_tags, |   followed_tags, | ||||||
|   notificationPolicy: notificationPolicyReducer, |   notificationPolicy: notificationPolicyReducer, | ||||||
|   notificationRequests: notificationRequestsReducer, |   notificationRequests: notificationRequestsReducer, | ||||||
|  |  | ||||||
|  | @ -1,26 +0,0 @@ | ||||||
| import { Map as ImmutableMap, fromJS } from 'immutable'; |  | ||||||
| 
 |  | ||||||
| import { |  | ||||||
|   HASHTAG_FETCH_SUCCESS, |  | ||||||
|   HASHTAG_FOLLOW_REQUEST, |  | ||||||
|   HASHTAG_FOLLOW_FAIL, |  | ||||||
|   HASHTAG_UNFOLLOW_REQUEST, |  | ||||||
|   HASHTAG_UNFOLLOW_FAIL, |  | ||||||
| } from 'mastodon/actions/tags'; |  | ||||||
| 
 |  | ||||||
| const initialState = ImmutableMap(); |  | ||||||
| 
 |  | ||||||
| export default function tags(state = initialState, action) { |  | ||||||
|   switch(action.type) { |  | ||||||
|   case HASHTAG_FETCH_SUCCESS: |  | ||||||
|     return state.set(action.name, fromJS(action.tag)); |  | ||||||
|   case HASHTAG_FOLLOW_REQUEST: |  | ||||||
|   case HASHTAG_UNFOLLOW_FAIL: |  | ||||||
|     return state.setIn([action.name, 'following'], true); |  | ||||||
|   case HASHTAG_FOLLOW_FAIL: |  | ||||||
|   case HASHTAG_UNFOLLOW_REQUEST: |  | ||||||
|     return state.setIn([action.name, 'following'], false); |  | ||||||
|   default: |  | ||||||
|     return state; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue