From 9c4577ab7c7a0ae38903dd14ae58341d81466351 Mon Sep 17 00:00:00 2001 From: KMY Date: Tue, 22 Aug 2023 15:41:22 +0900 Subject: [PATCH] Add circle adder --- .../api/v1/accounts/circles_controller.rb | 18 ++++++++ .../features/account/components/header.jsx | 5 ++ .../account_timeline/components/header.jsx | 6 +++ .../containers/header_container.jsx | 9 ++++ .../mastodon/reducers/circle_adder.js | 46 +++++++++---------- .../mastodon/reducers/list_adder.js | 46 +++++++++---------- app/models/concerns/account_associations.rb | 4 ++ config/routes/api.rb | 1 + 8 files changed, 89 insertions(+), 46 deletions(-) create mode 100644 app/controllers/api/v1/accounts/circles_controller.rb diff --git a/app/controllers/api/v1/accounts/circles_controller.rb b/app/controllers/api/v1/accounts/circles_controller.rb new file mode 100644 index 0000000000..1b21eb7ce4 --- /dev/null +++ b/app/controllers/api/v1/accounts/circles_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::CirclesController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:lists' } + before_action :require_user! + before_action :set_account + + def index + @circles = @account.suspended? ? [] : @account.joined_circles.where(account: current_account) + render json: @circles, each_serializer: REST::CircleSerializer + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end +end diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index 4646b8d188..c347cf2f87 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -59,6 +59,7 @@ const messages = defineMessages({ unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, add_or_remove_from_antenna: { id: 'account.add_or_remove_from_antenna', defaultMessage: 'Add or Remove from antennas' }, + add_or_remove_from_circle: { id: 'account.add_or_remove_from_circle', defaultMessage: 'Add or Remove from circles' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, @@ -105,6 +106,7 @@ class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, onAddToAntenna: PropTypes.func.isRequired, + onAddToCircle: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired, @@ -330,6 +332,9 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList }); } menu.push({ text: intl.formatMessage(messages.add_or_remove_from_antenna), action: this.props.onAddToAntenna }); + if (account.getIn(['relationship', 'followed_by'])) { + menu.push({ text: intl.formatMessage(messages.add_or_remove_from_circle), action: this.props.onAddToCircle }); + } menu.push(null); if (account.getIn(['relationship', 'muting'])) { diff --git a/app/javascript/mastodon/features/account_timeline/components/header.jsx b/app/javascript/mastodon/features/account_timeline/components/header.jsx index b78dba1cc5..ccf8290574 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.jsx +++ b/app/javascript/mastodon/features/account_timeline/components/header.jsx @@ -28,6 +28,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, onAddToAntenna: PropTypes.func.isRequired, + onAddToCircle: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired, onOpenAvatar: PropTypes.func.isRequired, @@ -101,6 +102,10 @@ export default class Header extends ImmutablePureComponent { this.props.onAddToAntenna(this.props.account); }; + handleAddToCircle = () => { + this.props.onAddToCircle(this.props.account); + }; + handleEditAccountNote = () => { this.props.onEditAccountNote(this.props.account); }; @@ -144,6 +149,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle={this.handleEndorseToggle} onAddToList={this.handleAddToList} onAddToAntenna={this.handleAddToAntenna} + onAddToCircle={this.handleAddToCircle} onEditAccountNote={this.handleEditAccountNote} onChangeLanguages={this.handleChangeLanguages} onInteractionModal={this.handleInteractionModal} diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx index 64c5c0ce6b..b71ee4b945 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx @@ -173,6 +173,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, + onAddToCircle (account) { + dispatch(openModal({ + modalType: 'CIRCLE_ADDER', + modalProps: { + accountId: account.get('id'), + }, + })); + }, + onChangeLanguages (account) { dispatch(openModal({ modalType: 'SUBSCRIBED_LANGUAGES', diff --git a/app/javascript/mastodon/reducers/circle_adder.js b/app/javascript/mastodon/reducers/circle_adder.js index 0f61273aa6..f09db160e6 100644 --- a/app/javascript/mastodon/reducers/circle_adder.js +++ b/app/javascript/mastodon/reducers/circle_adder.js @@ -1,47 +1,47 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { - LIST_ADDER_RESET, - LIST_ADDER_SETUP, - LIST_ADDER_LISTS_FETCH_REQUEST, - LIST_ADDER_LISTS_FETCH_SUCCESS, - LIST_ADDER_LISTS_FETCH_FAIL, - LIST_EDITOR_ADD_SUCCESS, - LIST_EDITOR_REMOVE_SUCCESS, -} from '../actions/lists'; + CIRCLE_ADDER_RESET, + CIRCLE_ADDER_SETUP, + CIRCLE_ADDER_CIRCLES_FETCH_REQUEST, + CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS, + CIRCLE_ADDER_CIRCLES_FETCH_FAIL, + CIRCLE_EDITOR_ADD_SUCCESS, + CIRCLE_EDITOR_REMOVE_SUCCESS, +} from '../actions/circles'; const initialState = ImmutableMap({ accountId: null, - lists: ImmutableMap({ + circles: ImmutableMap({ items: ImmutableList(), loaded: false, isLoading: false, }), }); -export default function listAdderReducer(state = initialState, action) { +export default function circleAdderReducer(state = initialState, action) { switch(action.type) { - case LIST_ADDER_RESET: + case CIRCLE_ADDER_RESET: return initialState; - case LIST_ADDER_SETUP: + case CIRCLE_ADDER_SETUP: return state.withMutations(map => { map.set('accountId', action.account.get('id')); }); - case LIST_ADDER_LISTS_FETCH_REQUEST: - return state.setIn(['lists', 'isLoading'], true); - case LIST_ADDER_LISTS_FETCH_FAIL: - return state.setIn(['lists', 'isLoading'], false); - case LIST_ADDER_LISTS_FETCH_SUCCESS: - return state.update('lists', lists => lists.withMutations(map => { + case CIRCLE_ADDER_CIRCLES_FETCH_REQUEST: + return state.setIn(['circles', 'isLoading'], true); + case CIRCLE_ADDER_CIRCLES_FETCH_FAIL: + return state.setIn(['circles', 'isLoading'], false); + case CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS: + return state.update('circles', circles => circles.withMutations(map => { map.set('isLoading', false); map.set('loaded', true); - map.set('items', ImmutableList(action.lists.map(item => item.id))); + map.set('items', ImmutableList(action.circles.map(item => item.id))); })); - case LIST_EDITOR_ADD_SUCCESS: - return state.updateIn(['lists', 'items'], list => list.unshift(action.listId)); - case LIST_EDITOR_REMOVE_SUCCESS: - return state.updateIn(['lists', 'items'], list => list.filterNot(item => item === action.listId)); + case CIRCLE_EDITOR_ADD_SUCCESS: + return state.updateIn(['circles', 'items'], circle => circle.unshift(action.circleId)); + case CIRCLE_EDITOR_REMOVE_SUCCESS: + return state.updateIn(['circles', 'items'], circle => circle.filterNot(item => item === action.circleId)); default: return state; } diff --git a/app/javascript/mastodon/reducers/list_adder.js b/app/javascript/mastodon/reducers/list_adder.js index f09db160e6..0f61273aa6 100644 --- a/app/javascript/mastodon/reducers/list_adder.js +++ b/app/javascript/mastodon/reducers/list_adder.js @@ -1,47 +1,47 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { - CIRCLE_ADDER_RESET, - CIRCLE_ADDER_SETUP, - CIRCLE_ADDER_CIRCLES_FETCH_REQUEST, - CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS, - CIRCLE_ADDER_CIRCLES_FETCH_FAIL, - CIRCLE_EDITOR_ADD_SUCCESS, - CIRCLE_EDITOR_REMOVE_SUCCESS, -} from '../actions/circles'; + LIST_ADDER_RESET, + LIST_ADDER_SETUP, + LIST_ADDER_LISTS_FETCH_REQUEST, + LIST_ADDER_LISTS_FETCH_SUCCESS, + LIST_ADDER_LISTS_FETCH_FAIL, + LIST_EDITOR_ADD_SUCCESS, + LIST_EDITOR_REMOVE_SUCCESS, +} from '../actions/lists'; const initialState = ImmutableMap({ accountId: null, - circles: ImmutableMap({ + lists: ImmutableMap({ items: ImmutableList(), loaded: false, isLoading: false, }), }); -export default function circleAdderReducer(state = initialState, action) { +export default function listAdderReducer(state = initialState, action) { switch(action.type) { - case CIRCLE_ADDER_RESET: + case LIST_ADDER_RESET: return initialState; - case CIRCLE_ADDER_SETUP: + case LIST_ADDER_SETUP: return state.withMutations(map => { map.set('accountId', action.account.get('id')); }); - case CIRCLE_ADDER_CIRCLES_FETCH_REQUEST: - return state.setIn(['circles', 'isLoading'], true); - case CIRCLE_ADDER_CIRCLES_FETCH_FAIL: - return state.setIn(['circles', 'isLoading'], false); - case CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS: - return state.update('circles', circles => circles.withMutations(map => { + case LIST_ADDER_LISTS_FETCH_REQUEST: + return state.setIn(['lists', 'isLoading'], true); + case LIST_ADDER_LISTS_FETCH_FAIL: + return state.setIn(['lists', 'isLoading'], false); + case LIST_ADDER_LISTS_FETCH_SUCCESS: + return state.update('lists', lists => lists.withMutations(map => { map.set('isLoading', false); map.set('loaded', true); - map.set('items', ImmutableList(action.circles.map(item => item.id))); + map.set('items', ImmutableList(action.lists.map(item => item.id))); })); - case CIRCLE_EDITOR_ADD_SUCCESS: - return state.updateIn(['circles', 'items'], circle => circle.unshift(action.circleId)); - case CIRCLE_EDITOR_REMOVE_SUCCESS: - return state.updateIn(['circles', 'items'], circle => circle.filterNot(item => item === action.circleId)); + case LIST_EDITOR_ADD_SUCCESS: + return state.updateIn(['lists', 'items'], list => list.unshift(action.listId)); + case LIST_EDITOR_REMOVE_SUCCESS: + return state.updateIn(['lists', 'items'], list => list.filterNot(item => item === action.listId)); default: return state; } diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 05a7509a33..abda458e96 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -52,6 +52,10 @@ module AccountAssociations has_many :antenna_accounts, inverse_of: :account, dependent: :destroy has_many :joined_antennas, class_name: 'Antenna', through: :antenna_accounts, source: :antenna + # Circles (that the account is on, not owned by the account) + has_many :circle_accounts, inverse_of: :account, dependent: :destroy + has_many :joined_circles, class_name: 'Circle', through: :circle_accounts, source: :circle + # Lists (that the account is on, not owned by the account) has_many :list_accounts, inverse_of: :account, dependent: :destroy has_many :lists, through: :list_accounts diff --git a/config/routes/api.rb b/config/routes/api.rb index 2f718a50e9..f760edc524 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -180,6 +180,7 @@ namespace :api, format: false do resources :following, only: :index, controller: 'accounts/following_accounts' resources :lists, only: :index, controller: 'accounts/lists' resources :antennas, only: :index, controller: 'accounts/antennas' + resources :circles, only: :index, controller: 'accounts/circles' resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' resources :featured_tags, only: :index, controller: 'accounts/featured_tags'