diff options
9 files changed, 122 insertions, 33 deletions
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index ac9f5962f..b4183bbee 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -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 }; }; diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx index 98d914514..285945fa8 100644 --- a/app/assets/javascripts/components/components/status_content.jsx +++ b/app/assets/javascripts/components/components/status_content.jsx @@ -14,15 +14,24 @@ const StatusContent = React.createClass({ mixins: [PureRenderMixin], componentDidMount () { - const node = ReactDOM.findDOMNode(this); - - this.props.status.get('mentions').forEach(mention => { - const links = node.querySelector(`a[href="${mention.get('url')}"]`); - links.addEventListener('click', this.onLinkClick.bind(this, mention)); - }); + const node = ReactDOM.findDOMNode(this); + const links = node.querySelectorAll('a'); + + 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} />; diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx new file mode 100644 index 000000000..a6eb01d61 --- /dev/null +++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx @@ -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; diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index 20ed7512b..8a4a629ce 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -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], diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index 46d0e5954..5b09594cc 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -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> ); diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 9180b17a2..3b8beafaa 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -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: diff --git a/app/controllers/api/accounts_controller.rb b/app/controllers/api/accounts_controller.rb index ccbbc93ff..490c28e75 100644 --- a/app/controllers/api/accounts_controller.rb +++ b/app/controllers/api/accounts_controller.rb @@ -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 diff --git a/app/views/api/accounts/relationship.rabl b/app/views/api/accounts/relationship.rabl new file mode 100644 index 000000000..3e5bf882c --- /dev/null +++ b/app/views/api/accounts/relationship.rabl @@ -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 } diff --git a/app/views/api/accounts/relationships.rabl b/app/views/api/accounts/relationships.rabl index dc1c1bc65..16fdc40d9 100644 --- a/app/views/api/accounts/relationships.rabl +++ b/app/views/api/accounts/relationships.rabl @@ -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' |