From c3d665f5314d8b382a3c75d6598bf054594a9130 Mon Sep 17 00:00:00 2001 From: KMY Date: Sat, 23 Sep 2023 20:06:07 +0900 Subject: [PATCH] Wip: Add circle history page and record circle posts --- .../api/v1/circles/statuses_controller.rb | 66 +++++++++++++++++++ .../features/circle_statuses/index.jsx | 7 +- .../mastodon/features/circles/index.jsx | 6 +- app/javascript/mastodon/features/ui/index.jsx | 2 + .../features/ui/util/async-components.js | 4 ++ app/javascript/mastodon/reducers/circles.js | 10 ++- app/models/circle.rb | 2 + app/models/circle_status.rb | 26 ++++++++ app/models/status.rb | 1 + app/services/process_mentions_service.rb | 2 + config/routes.rb | 2 +- config/routes/api.rb | 1 + .../20230923103430_create_circle_statuses.rb | 22 +++++++ db/schema.rb | 14 +++- 14 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 app/controllers/api/v1/circles/statuses_controller.rb create mode 100644 app/models/circle_status.rb create mode 100644 db/migrate/20230923103430_create_circle_statuses.rb diff --git a/app/controllers/api/v1/circles/statuses_controller.rb b/app/controllers/api/v1/circles/statuses_controller.rb new file mode 100644 index 00000000000000..84e9b05543a7e2 --- /dev/null +++ b/app/controllers/api/v1/circles/statuses_controller.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +class Api::V1::Circles::StatusesController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] + before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] + + before_action :require_user! + before_action :set_circle + + after_action :insert_pagination_headers, only: :show + + def show + @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer + end + + private + + def set_circle + @circle = current_account.circles.find(params[:circle_id]) + end + + def load_statuses + if unlimited? + @circle.statuses.includes(:status_stat).all + else + @circle.statuses.includes(:status_stat).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) + end + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + return if unlimited? + + api_v1_circle_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + return if unlimited? + + api_v1_circle_statuses_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id + end + + def records_continue? + @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + + def unlimited? + params[:limit] == '0' + end +end diff --git a/app/javascript/mastodon/features/circle_statuses/index.jsx b/app/javascript/mastodon/features/circle_statuses/index.jsx index 4ec0b5da978521..2896455ab5c6a9 100644 --- a/app/javascript/mastodon/features/circle_statuses/index.jsx +++ b/app/javascript/mastodon/features/circle_statuses/index.jsx @@ -10,7 +10,7 @@ import { connect } from 'react-redux'; import { debounce } from 'lodash'; -import { deleteCircle, expandCircleStatuses, fetchCircle, fetchCircleStatuses , setupCircleEditor } from 'mastodon/actions/circles'; +import { deleteCircle, expandCircleStatuses, fetchCircle, fetchCircleStatuses } from 'mastodon/actions/circles'; import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { openModal } from 'mastodon/actions/modal'; import ColumnHeader from 'mastodon/components/column_header'; @@ -80,7 +80,10 @@ class CircleStatuses extends ImmutablePureComponent { }; handleEditClick = () => { - this.props.dispatch(setupCircleEditor(this.props.params.id)); + this.props.dispatch(openModal({ + modalType: 'CIRCLE_EDITOR', + modalProps: { circleId: this.props.params.id }, + })); }; handleDeleteClick = () => { diff --git a/app/javascript/mastodon/features/circles/index.jsx b/app/javascript/mastodon/features/circles/index.jsx index 1b83876827fd0d..1cd3ae417fdbf1 100644 --- a/app/javascript/mastodon/features/circles/index.jsx +++ b/app/javascript/mastodon/features/circles/index.jsx @@ -13,7 +13,6 @@ import { fetchCircles, deleteCircle } from 'mastodon/actions/circles'; import { openModal } from 'mastodon/actions/modal'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import { IconButton } from 'mastodon/components/icon_button'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import ScrollableList from 'mastodon/components/scrollable_list'; import ColumnLink from 'mastodon/features/ui/components/column_link'; @@ -106,10 +105,7 @@ class Circles extends ImmutablePureComponent { bindToDocument={!multiColumn} > {circles.map(circle => - (
- - -
) + , )} diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index f23fdc66fc389c..ce6196a8824e6f 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -65,6 +65,7 @@ import { Lists, Antennas, Circles, + CircleStatuses, AntennaSetting, Directory, Explore, @@ -259,6 +260,7 @@ class SwitchingColumnsArea extends PureComponent { + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 81d83ec818354f..10daf553574923 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -54,6 +54,10 @@ export function Circles () { return import(/* webpackChunkName: "features/circles" */'../../circles'); } +export function CircleStatuses () { + return import(/* webpackChunkName: "features/circle_statuses" */'../../circle_statuses'); +} + export function Status () { return import(/* webpackChunkName: "features/status" */'../../status'); } diff --git a/app/javascript/mastodon/reducers/circles.js b/app/javascript/mastodon/reducers/circles.js index 280fb04920bc72..53277e035f1890 100644 --- a/app/javascript/mastodon/reducers/circles.js +++ b/app/javascript/mastodon/reducers/circles.js @@ -1,4 +1,4 @@ -import { List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { List as ImmutableList, Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { CIRCLE_FETCH_SUCCESS, @@ -17,6 +17,12 @@ import { const initialState = ImmutableList(); +const initialStatusesState = ImmutableMap({ + items: ImmutableList(), + isLoading: false, + next: null, +}); + const normalizeCircle = (state, circle) => { const old = state.get(circle.id); if (old === false) { @@ -26,6 +32,8 @@ const normalizeCircle = (state, circle) => { let s = state.set(circle.id, fromJS(circle)); if (old) { s = s.setIn([circle.id, 'statuses'], old.get('statuses')); + } else { + s = s.setIn([circle.id, 'statuses'], initialStatusesState); } return s; }; diff --git a/app/models/circle.rb b/app/models/circle.rb index cb58b97bcea8be..cec49df88c83bf 100644 --- a/app/models/circle.rb +++ b/app/models/circle.rb @@ -20,6 +20,8 @@ class Circle < ApplicationRecord has_many :circle_accounts, inverse_of: :circle, dependent: :destroy has_many :accounts, through: :circle_accounts + has_many :circle_statuses, inverse_of: :circle, dependent: :destroy + has_many :statuses, through: :circle_statuses validates :title, presence: true diff --git a/app/models/circle_status.rb b/app/models/circle_status.rb new file mode 100644 index 00000000000000..b394a4f9270904 --- /dev/null +++ b/app/models/circle_status.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: circle_statuses +# +# id :bigint(8) not null, primary key +# circle_id :bigint(8) +# status_id :bigint(8) not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class CircleStatus < ApplicationRecord + belongs_to :circle + belongs_to :status + + validates :status, uniqueness: { scope: :circle } + validate :account_own_status + + private + + def account_own_status + errors.add(:status_id, :invalid) unless status.account_id == circle.account_id + end +end diff --git a/app/models/status.rb b/app/models/status.rb index b532f58a3083a6..98b274ce046cc9 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -106,6 +106,7 @@ class Status < ApplicationRecord has_one :poll, inverse_of: :status, dependent: :destroy has_one :trend, class_name: 'StatusTrend', inverse_of: :status has_one :scheduled_expiration_status, inverse_of: :status, dependent: :destroy + has_one :circle_status, inverse_of: :status, dependent: :destroy validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index 493facbea773c5..0a5b48a9d9e654 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -112,5 +112,7 @@ def process_circle! @circle.accounts.find_each do |target_account| @current_mentions << @status.mentions.new(silent: true, account: target_account) unless mentioned_account_ids.include?(target_account.id) end + + @circle.statuses << @status end end diff --git a/config/routes.rb b/config/routes.rb index 3db26c7f8c0439..ed5995b2f4042b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,7 +18,7 @@ /lists/(*any) /antennasw/(*any) /antennast/(*any) - /circles + /circles/(*any) /notifications /favourites /emoji_reactions diff --git a/config/routes/api.rb b/config/routes/api.rb index 9bf466ddab5f9f..961cd43ad35198 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -226,6 +226,7 @@ resources :circles, only: [:index, :create, :show, :update, :destroy] do resource :accounts, only: [:show, :create, :destroy], controller: 'circles/accounts' + resource :statuses, only: [:show], controller: 'circles/statuses' end resources :bookmark_categories, only: [:index, :create, :show, :update, :destroy] do diff --git a/db/migrate/20230923103430_create_circle_statuses.rb b/db/migrate/20230923103430_create_circle_statuses.rb new file mode 100644 index 00000000000000..9c14bb808ab8b9 --- /dev/null +++ b/db/migrate/20230923103430_create_circle_statuses.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class CreateCircleStatuses < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def change + safety_assured do + create_table :circle_statuses do |t| + t.belongs_to :circle, null: true, foreign_key: { on_delete: :cascade } + t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade } + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + + add_index :circle_statuses, [:circle_id, :status_id], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c5dcfc6a330f56..e17320cdaa0637 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_19_232836) do +ActiveRecord::Schema[7.0].define(version: 2023_09_23_103430) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -447,6 +447,16 @@ t.index ["follow_id"], name: "index_circle_accounts_on_follow_id" end + create_table "circle_statuses", force: :cascade do |t| + t.bigint "circle_id" + t.bigint "status_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["circle_id", "status_id"], name: "index_circle_statuses_on_circle_id_and_status_id", unique: true + t.index ["circle_id"], name: "index_circle_statuses_on_circle_id" + t.index ["status_id"], name: "index_circle_statuses_on_status_id" + end + create_table "circles", force: :cascade do |t| t.bigint "account_id", null: false t.string "title", default: "", null: false @@ -1414,6 +1424,8 @@ add_foreign_key "circle_accounts", "accounts", on_delete: :cascade add_foreign_key "circle_accounts", "circles", on_delete: :cascade add_foreign_key "circle_accounts", "follows", on_delete: :cascade + add_foreign_key "circle_statuses", "circles", on_delete: :cascade + add_foreign_key "circle_statuses", "statuses", on_delete: :cascade add_foreign_key "circles", "accounts", on_delete: :cascade add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade