Change output of api/accounts/:id/follow and unfollow to return relationship
Track relationship in redux state. Display follow/unfollow and following-back information on account view (unstyled)
This commit is contained in:
		
							parent
							
								
									c6d893a71d
								
							
						
					
					
						commit
						3f9708edc4
					
				
					 9 changed files with 121 additions and 32 deletions
				
			
		|  | @ -124,10 +124,10 @@ export function followAccountRequest(id) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function followAccountSuccess(account) { | ||||
| export function followAccountSuccess(relationship) { | ||||
|   return { | ||||
|     type: ACCOUNT_FOLLOW_SUCCESS, | ||||
|     account: account | ||||
|     relationship: relationship | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  | @ -145,10 +145,10 @@ export function unfollowAccountRequest(id) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function unfollowAccountSuccess(account) { | ||||
| export function unfollowAccountSuccess(relationship) { | ||||
|   return { | ||||
|     type: ACCOUNT_UNFOLLOW_SUCCESS, | ||||
|     account: account | ||||
|     relationship: relationship | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,15 +14,24 @@ const StatusContent = React.createClass({ | |||
|   mixins: [PureRenderMixin], | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     const node = ReactDOM.findDOMNode(this); | ||||
|     const node  = ReactDOM.findDOMNode(this); | ||||
|     const links = node.querySelectorAll('a'); | ||||
| 
 | ||||
|     this.props.status.get('mentions').forEach(mention => { | ||||
|       const links = node.querySelector(`a[href="${mention.get('url')}"]`); | ||||
|       links.addEventListener('click', this.onLinkClick.bind(this, mention)); | ||||
|     }); | ||||
|     for (var i = 0; i < links.length; ++i) { | ||||
|       let link    = links[i]; | ||||
|       let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); | ||||
| 
 | ||||
|       if (mention) { | ||||
|         link.addEventListener('click', this.onMentionClick.bind(this, mention)); | ||||
|       } else { | ||||
|         link.setAttribute('target', '_blank'); | ||||
|         link.setAttribute('rel', 'noopener'); | ||||
|         link.addEventListener('click', this.onNormalClick); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onLinkClick (mention, e) { | ||||
|   onMentionClick (mention, e) { | ||||
|     if (e.button === 0) { | ||||
|       e.preventDefault(); | ||||
|       this.context.router.push(`/accounts/${mention.get('id')}`); | ||||
|  | @ -31,6 +40,10 @@ const StatusContent = React.createClass({ | |||
|     e.stopPropagation(); | ||||
|   }, | ||||
| 
 | ||||
|   onNormalClick (e) { | ||||
|     e.stopPropagation(); | ||||
|   }, | ||||
| 
 | ||||
|   render () { | ||||
|     const content = { __html: this.props.status.get('content') }; | ||||
|     return <div className='status__content' dangerouslySetInnerHTML={content} />; | ||||
|  |  | |||
|  | @ -0,0 +1,48 @@ | |||
| import PureRenderMixin    from 'react-addons-pure-render-mixin'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import Button             from '../../../components/button'; | ||||
| 
 | ||||
| const ActionBar = React.createClass({ | ||||
| 
 | ||||
|   propTypes: { | ||||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     me: React.PropTypes.number.isRequired, | ||||
|     onFollow: React.PropTypes.func.isRequired, | ||||
|     onUnfollow: React.PropTypes.func.isRequired | ||||
|   }, | ||||
| 
 | ||||
|   mixins: [PureRenderMixin], | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, me } = this.props; | ||||
|      | ||||
|     let followBack   = ''; | ||||
|     let actionButton = ''; | ||||
| 
 | ||||
|     if (account.get('id') === me) { | ||||
|       actionButton = 'This is you!'; | ||||
|     } else { | ||||
|       if (account.getIn(['relationship', 'following'])) { | ||||
|         actionButton = <Button text='Unfollow' onClick={this.props.onUnfollow} /> | ||||
|       } else { | ||||
|         actionButton = <Button text='Follow' onClick={this.props.onFollow} /> | ||||
|       } | ||||
| 
 | ||||
|       if (account.getIn(['relationship', 'followed_by'])) { | ||||
|         followBack = 'follows you'; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|         {actionButton} | ||||
|         {account.get('followers_count')} followers | ||||
|         {account.get('following_count')} following | ||||
|         {followBack} | ||||
|       </div> | ||||
|     ); | ||||
|   }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default ActionBar; | ||||
|  | @ -1,13 +1,10 @@ | |||
| import PureRenderMixin    from 'react-addons-pure-render-mixin'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import Button             from '../../../components/button'; | ||||
| 
 | ||||
| const Header = React.createClass({ | ||||
| 
 | ||||
|   propTypes: { | ||||
|     account: ImmutablePropTypes.map.isRequired, | ||||
|     onFollow: React.PropTypes.func.isRequired, | ||||
|     onUnfollow: React.PropTypes.func.isRequired | ||||
|     account: ImmutablePropTypes.map.isRequired | ||||
|   }, | ||||
| 
 | ||||
|   mixins: [PureRenderMixin], | ||||
|  |  | |||
|  | @ -11,13 +11,13 @@ import { | |||
| import { replyCompose }      from '../../actions/compose'; | ||||
| import { favourite, reblog } from '../../actions/interactions'; | ||||
| import Header                from './components/header'; | ||||
| import { selectStatus }      from '../../reducers/timelines'; | ||||
| import { | ||||
|   selectStatus, | ||||
|   selectAccount | ||||
| }                            from '../../reducers/timelines'; | ||||
| import StatusList            from '../../components/status_list'; | ||||
| import Immutable             from 'immutable'; | ||||
| 
 | ||||
| function selectAccount(state, id) { | ||||
|   return state.getIn(['timelines', 'accounts', id], null); | ||||
| }; | ||||
| import ActionBar             from './components/action_bar'; | ||||
| 
 | ||||
| function selectStatuses(state, accountId) { | ||||
|   return state.getIn(['timelines', 'accounts_timelines', accountId], Immutable.List()).map(id => selectStatus(state, id)).filterNot(status => status === null); | ||||
|  | @ -25,7 +25,8 @@ function selectStatuses(state, accountId) { | |||
| 
 | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|   account: selectAccount(state, Number(props.params.accountId)), | ||||
|   statuses: selectStatuses(state, Number(props.params.accountId)) | ||||
|   statuses: selectStatuses(state, Number(props.params.accountId)), | ||||
|   me: state.getIn(['timelines', 'me']) | ||||
| }); | ||||
| 
 | ||||
| const Account = React.createClass({ | ||||
|  | @ -76,7 +77,7 @@ const Account = React.createClass({ | |||
|   }, | ||||
| 
 | ||||
|   render () { | ||||
|     const { account, statuses } = this.props; | ||||
|     const { account, statuses, me } = this.props; | ||||
| 
 | ||||
|     if (account === null) { | ||||
|       return <div>Loading {this.props.params.accountId}...</div>; | ||||
|  | @ -84,7 +85,8 @@ const Account = React.createClass({ | |||
| 
 | ||||
|     return ( | ||||
|       <div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}> | ||||
|         <Header account={account} onFollow={this.handleFollow} onUnfollow={this.handleUnfollow} /> | ||||
|         <Header account={account} /> | ||||
|         <ActionBar account={account} me={me} onFollow={this.handleFollow} onUnfollow={this.handleUnfollow} /> | ||||
|         <StatusList statuses={statuses} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} /> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ export function selectStatus(state, id) { | |||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   status = status.set('account', state.getIn(['timelines', 'accounts', status.get('account')])); | ||||
|   status = status.set('account', selectAccount(state, status.get('account'))); | ||||
| 
 | ||||
|   if (status.get('reblog') !== null) { | ||||
|     status = status.set('reblog', selectStatus(state, status.get('reblog'))); | ||||
|  | @ -48,6 +48,16 @@ export function selectStatus(state, id) { | |||
|   return status; | ||||
| }; | ||||
| 
 | ||||
| export function selectAccount(state, id) { | ||||
|   let account = state.getIn(['timelines', 'accounts', id], null); | ||||
| 
 | ||||
|   if (account === null) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   return account.set('relationship', state.getIn(['timelines', 'relationships', id])); | ||||
| }; | ||||
| 
 | ||||
| function normalizeStatus(state, status) { | ||||
|   // Separate account | ||||
|   let account = status.get('account'); | ||||
|  | @ -139,10 +149,18 @@ function deleteStatus(state, id) { | |||
|   return state.deleteIn(['statuses', id]); | ||||
| }; | ||||
| 
 | ||||
| function normalizeAccount(state, account) { | ||||
| function normalizeAccount(state, account, relationship) { | ||||
|   if (relationship) { | ||||
|     state = normalizeRelationship(state, relationship); | ||||
|   } | ||||
|    | ||||
|   return state.setIn(['accounts', account.get('id')], account); | ||||
| }; | ||||
| 
 | ||||
| function normalizeRelationship(state, relationship) { | ||||
|   return state.setIn(['relationships', relationship.get('id')], relationship); | ||||
| }; | ||||
| 
 | ||||
| function setSelf(state, account) { | ||||
|   state = normalizeAccount(state, account); | ||||
|   return state.set('me', account.get('id')); | ||||
|  | @ -184,9 +202,10 @@ export default function timelines(state = initialState, action) { | |||
|       return setSelf(state, Immutable.fromJS(action.account)); | ||||
|     case ACCOUNT_FETCH_SUCCESS: | ||||
|     case FOLLOW_SUBMIT_SUCCESS: | ||||
|       return normalizeAccount(state, Immutable.fromJS(action.account), Immutable.fromJS(action.relationship)); | ||||
|     case ACCOUNT_FOLLOW_SUCCESS: | ||||
|     case ACCOUNT_UNFOLLOW_SUCCESS: | ||||
|       return normalizeAccount(state, Immutable.fromJS(action.account)); | ||||
|       return normalizeRelationship(state, Immutable.fromJS(action.relationship)); | ||||
|     case STATUS_FETCH_SUCCESS: | ||||
|       return normalizeContext(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants)); | ||||
|     case ACCOUNT_TIMELINE_FETCH_SUCCESS: | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| class Api::AccountsController < ApiController | ||||
|   before_action :set_account | ||||
|   before_action :doorkeeper_authorize! | ||||
|   before_action :set_account | ||||
|   respond_to    :json | ||||
| 
 | ||||
|   def show | ||||
|  | @ -20,12 +20,14 @@ class Api::AccountsController < ApiController | |||
| 
 | ||||
|   def follow | ||||
|     @follow = FollowService.new.(current_user.account, @account.acct) | ||||
|     render action: :show | ||||
|     set_relationship | ||||
|     render action: :relationship | ||||
|   end | ||||
| 
 | ||||
|   def unfollow | ||||
|     @unfollow = UnfollowService.new.(current_user.account, @account) | ||||
|     render action: :show | ||||
|     set_relationship | ||||
|     render action: :relationship | ||||
|   end | ||||
| 
 | ||||
|   def relationships | ||||
|  | @ -41,4 +43,10 @@ class Api::AccountsController < ApiController | |||
|   def set_account | ||||
|     @account = Account.find(params[:id]) | ||||
|   end | ||||
| 
 | ||||
|   def set_relationship | ||||
|     @following   = Account.following_map([@account.id], current_user.account_id) | ||||
|     @followed_by = Account.followed_by_map([@account.id], current_user.account_id) | ||||
|     @blocking    = {} | ||||
|   end | ||||
| end | ||||
|  |  | |||
							
								
								
									
										5
									
								
								app/views/api/accounts/relationship.rabl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/api/accounts/relationship.rabl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| object @account | ||||
| attribute :id | ||||
| node(:following)   { |account| @following[account.id]   || false } | ||||
| node(:followed_by) { |account| @followed_by[account.id] || false } | ||||
| node(:blocking)    { |account| @blocking[account.id]    || false } | ||||
|  | @ -1,5 +1,2 @@ | |||
| collection @accounts | ||||
| attribute :id | ||||
| node(:following)   { |account| @following[account.id]   || false } | ||||
| node(:followed_by) { |account| @followed_by[account.id] || false } | ||||
| node(:blocking)    { |account| @blocking[account.id]    || false } | ||||
| extends 'api/accounts/relationship' | ||||
|  |  | |||
		Reference in a new issue