[Glitch] Add ability to follow hashtags in web UI
This commit is contained in:
		
							parent
							
								
									bcb958c264
								
							
						
					
					
						commit
						991349e6e2
					
				
					 4 changed files with 185 additions and 16 deletions
				
			
		
							
								
								
									
										92
									
								
								app/javascript/flavours/glitch/actions/tags.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/javascript/flavours/glitch/actions/tags.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | |||
| import api from 'flavours/glitch/util/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 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, getState) => { | ||||
|   dispatch(fetchHashtagRequest()); | ||||
| 
 | ||||
|   api(getState).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 followHashtag = name => (dispatch, getState) => { | ||||
|   dispatch(followHashtagRequest(name)); | ||||
| 
 | ||||
|   api(getState).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, getState) => { | ||||
|   dispatch(unfollowHashtagRequest(name)); | ||||
| 
 | ||||
|   api(getState).post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => { | ||||
|     dispatch(unfollowHashtagSuccess(name, data)); | ||||
|   }).catch(err => { | ||||
|     dispatch(unfollowHashtagFail(name, err)); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| export const unfollowHashtagRequest = name => ({ | ||||
|   type: HASHTAG_FETCH_REQUEST, | ||||
|   name, | ||||
| }); | ||||
| 
 | ||||
| export const unfollowHashtagSuccess = (name, tag) => ({ | ||||
|   type: HASHTAG_FETCH_SUCCESS, | ||||
|   name, | ||||
|   tag, | ||||
| }); | ||||
| 
 | ||||
| export const unfollowHashtagFail = (name, error) => ({ | ||||
|   type: HASHTAG_FETCH_FAIL, | ||||
|   name, | ||||
|   error, | ||||
| }); | ||||
|  | @ -4,18 +4,29 @@ import PropTypes from 'prop-types'; | |||
| import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; | ||||
| import Column from 'flavours/glitch/components/column'; | ||||
| import ColumnHeader from 'flavours/glitch/components/column_header'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import ColumnSettingsContainer from './containers/column_settings_container'; | ||||
| import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines'; | ||||
| import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { connectHashtagStream } from 'flavours/glitch/actions/streaming'; | ||||
| import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; | ||||
| import { isEqual } from 'lodash'; | ||||
| import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags'; | ||||
| import Icon from 'flavours/glitch/components/icon'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, | ||||
|   unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|   hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, | ||||
|   tag: state.getIn(['tags', props.params.id]), | ||||
| }); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
| @injectIntl | ||||
| class HashtagTimeline extends React.PureComponent { | ||||
| 
 | ||||
|   disconnects = []; | ||||
|  | @ -25,7 +36,9 @@ class HashtagTimeline extends React.PureComponent { | |||
|     columnId: PropTypes.string, | ||||
|     dispatch: PropTypes.func.isRequired, | ||||
|     hasUnread: PropTypes.bool, | ||||
|     tag: ImmutablePropTypes.map, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     intl: PropTypes.object, | ||||
|   }; | ||||
| 
 | ||||
|   handlePin = () => { | ||||
|  | @ -39,7 +52,8 @@ class HashtagTimeline extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   title = () => { | ||||
|     let title = [this.props.params.id]; | ||||
|     const { id } = this.props.params; | ||||
|     const title  = [id]; | ||||
| 
 | ||||
|     if (this.additionalFor('any')) { | ||||
|       title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any'  values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />); | ||||
|  | @ -95,23 +109,34 @@ class HashtagTimeline extends React.PureComponent { | |||
|     this.disconnects = []; | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|   _unload () { | ||||
|     const { dispatch } = this.props; | ||||
|     const { id, local } = this.props.params; | ||||
| 
 | ||||
|     this._unsubscribe(); | ||||
|     dispatch(clearTimeline(`hashtag:${id}${local ? ':local' : ''}`)); | ||||
|   } | ||||
| 
 | ||||
|   _load() { | ||||
|     const { dispatch } = this.props; | ||||
|     const { id, tags, local } = this.props.params; | ||||
| 
 | ||||
|     this._subscribe(dispatch, id, tags, local); | ||||
|     dispatch(expandHashtagTimeline(id, { tags, local })); | ||||
|     dispatch(fetchHashtag(id)); | ||||
|   } | ||||
| 
 | ||||
|   componentWillReceiveProps (nextProps) { | ||||
|     const { dispatch, params } = this.props; | ||||
|     const { id, tags, local } = nextProps.params; | ||||
|   componentDidMount () { | ||||
|     this._load(); | ||||
|   } | ||||
| 
 | ||||
|   componentDidUpdate (prevProps) { | ||||
|     const { params } = this.props; | ||||
|     const { id, tags, local } = prevProps.params; | ||||
| 
 | ||||
|     if (id !== params.id || !isEqual(tags, params.tags) || !isEqual(local, params.local)) { | ||||
|       this._unsubscribe(); | ||||
|       this._subscribe(dispatch, id, tags, local); | ||||
|       dispatch(clearTimeline(`hashtag:${id}${local ? ':local' : ''}`)); | ||||
|       dispatch(expandHashtagTimeline(id, { tags, local })); | ||||
|       this._unload(); | ||||
|       this._load(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -124,17 +149,42 @@ class HashtagTimeline extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleLoadMore = maxId => { | ||||
|     const { id, tags, local } = this.props.params; | ||||
|     this.props.dispatch(expandHashtagTimeline(id, { maxId, tags, local })); | ||||
|     const { dispatch, params } = this.props; | ||||
|     const { id, tags, local }  = params; | ||||
| 
 | ||||
|     dispatch(expandHashtagTimeline(id, { maxId, tags, local })); | ||||
|   } | ||||
| 
 | ||||
|   handleFollow = () => { | ||||
|     const { dispatch, params, tag } = this.props; | ||||
|     const { id } = params; | ||||
| 
 | ||||
|     if (tag.get('following')) { | ||||
|       dispatch(unfollowHashtag(id)); | ||||
|     } else { | ||||
|       dispatch(followHashtag(id)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { hasUnread, columnId, multiColumn } = this.props; | ||||
|     const { hasUnread, columnId, multiColumn, tag, intl } = this.props; | ||||
|     const { id, local } = this.props.params; | ||||
|     const pinned = !!columnId; | ||||
| 
 | ||||
|     let followButton; | ||||
| 
 | ||||
|     if (tag) { | ||||
|       const following = tag.get('following'); | ||||
| 
 | ||||
|       followButton = ( | ||||
|         <button className={classNames('column-header__button')} onClick={this.handleFollow} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-pressed={following ? 'true' : 'false'}> | ||||
|           <Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' /> | ||||
|         </button> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Column ref={this.setRef} name='hashtag' label={`#${id}`}> | ||||
|       <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> | ||||
|         <ColumnHeader | ||||
|           icon='hashtag' | ||||
|           active={hasUnread} | ||||
|  | @ -144,8 +194,8 @@ class HashtagTimeline extends React.PureComponent { | |||
|           onClick={this.handleHeaderClick} | ||||
|           pinned={pinned} | ||||
|           multiColumn={multiColumn} | ||||
|           extraButton={followButton} | ||||
|           showBackButton | ||||
|           bindToDocument={!multiColumn} | ||||
|         > | ||||
|           {columnId && <ColumnSettingsContainer columnId={columnId} />} | ||||
|         </ColumnHeader> | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ import account_notes from './account_notes'; | |||
| import picture_in_picture from './picture_in_picture'; | ||||
| import accounts_map from './accounts_map'; | ||||
| import history from './history'; | ||||
| import tags from './tags'; | ||||
| 
 | ||||
| const reducers = { | ||||
|   announcements, | ||||
|  | @ -85,6 +86,7 @@ const reducers = { | |||
|   account_notes, | ||||
|   picture_in_picture, | ||||
|   history, | ||||
|   tags, | ||||
| }; | ||||
| 
 | ||||
| export default combineReducers(reducers); | ||||
|  |  | |||
							
								
								
									
										25
									
								
								app/javascript/flavours/glitch/reducers/tags.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/javascript/flavours/glitch/reducers/tags.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| import { | ||||
|   HASHTAG_FETCH_SUCCESS, | ||||
|   HASHTAG_FOLLOW_REQUEST, | ||||
|   HASHTAG_FOLLOW_FAIL, | ||||
|   HASHTAG_UNFOLLOW_REQUEST, | ||||
|   HASHTAG_UNFOLLOW_FAIL, | ||||
| } from 'mastodon/actions/tags'; | ||||
| import { Map as ImmutableMap, fromJS } from 'immutable'; | ||||
| 
 | ||||
| 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…
	
		Reference in a new issue