From cc9a5c965434816e382a154d6514834777a3d33c Mon Sep 17 00:00:00 2001 From: JS Moore Date: Sat, 9 Mar 2024 11:52:38 -0500 Subject: [PATCH 1/7] Get the tags into the database with a raw curl. --- .../api/v1/lists/accounts_controller.rb | 1 + .../api/v1/lists/tags_controller.rb | 94 +++++++++++++++++++ app/models/list.rb | 3 + app/models/list_tag.rb | 20 ++++ app/models/user.rb | 2 - config/routes/api.rb | 3 +- db/migrate/20240309162900_list_tags.rb | 11 +++ db/schema.rb | 13 ++- 8 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 app/controllers/api/v1/lists/tags_controller.rb create mode 100644 app/models/list_tag.rb create mode 100644 db/migrate/20240309162900_list_tags.rb diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 0604ad60fc4635..0f1c2cd0b9186f 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -32,6 +32,7 @@ def destroy private def set_list + Rails.logger.warn "This is concerning! #{current_account} and #{params}" @list = List.where(account: current_account).find(params[:list_id]) end diff --git a/app/controllers/api/v1/lists/tags_controller.rb b/app/controllers/api/v1/lists/tags_controller.rb new file mode 100644 index 00000000000000..c255821856394c --- /dev/null +++ b/app/controllers/api/v1/lists/tags_controller.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +class Api::V1::Lists::TagsController < 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_list + + after_action :insert_pagination_headers, only: :show + + def show + @tags = load_tags + render json: @tags, each_serializer: REST::TagSerializer + end + + def create + ApplicationRecord.transaction do + list_tags.each do |tag| + @list.tags << tag + end + end + + render_empty + end + + def destroy + ListTag.where(list: @list, tag_id: tag_id).destroy_all + render_empty + end + + private + + def set_list + Rails.logger.warn "This is tagged concerning! #{current_account} and #{params}" + @list = List.where(account: current_account).find(params[:list_id]) + end + + def load_tags + if unlimited? + @list.tags.all + else + @list.tags.paginate_by_max_id(limit_param(DEFAULT_TAGS_LIMIT), params[:max_id], params[:since_id]) + end + end + + def list_tags + Tag.find(tag_ids) + end + + def tag_ids + Array(resource_params[:tag_ids]) + end + + def resource_params + params.permit(tag_ids: []) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + return if unlimited? + + api_v1_list_tags_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + return if unlimited? + + api_v1_list_tags_url pagination_params(since_id: pagination_since_id) unless @tags.empty? + end + + def pagination_max_id + @tags.last.id + end + + def pagination_since_id + @tags.first.id + end + + def records_continue? + @tags.size == limit_param(DEFAULT_TAGS_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/models/list.rb b/app/models/list.rb index b45bd057bc7caa..c0b3243b8dabbc 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -25,6 +25,9 @@ class List < ApplicationRecord has_many :list_accounts, inverse_of: :list, dependent: :destroy has_many :accounts, through: :list_accounts + has_many :list_tags, inverse_of: :list, dependent: :destroy + has_many :tags, through: :list_tags + validates :title, presence: true validates_each :account_id, on: :create do |record, _attr, value| diff --git a/app/models/list_tag.rb b/app/models/list_tag.rb new file mode 100644 index 00000000000000..3b532771729a3a --- /dev/null +++ b/app/models/list_tag.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: list_tag +# +# id :bigint(8) not null, primary key +# list_id :bigint(8) not null +# tag_id :bigint(8) not null +# + +class ListTag < ApplicationRecord + belongs_to :list + belongs_to :tag + + validates :tag_id, uniqueness: { scope: :list_id } + + private + +end diff --git a/app/models/user.rb b/app/models/user.rb index fecb19c0378bfb..7f7fe197b45fbc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,7 +14,6 @@ # sign_in_count :integer default(0), not null # current_sign_in_at :datetime # last_sign_in_at :datetime -# admin :boolean default(FALSE), not null # confirmation_token :string # confirmed_at :datetime # confirmation_sent_at :datetime @@ -29,7 +28,6 @@ # otp_backup_codes :string is an Array # account_id :bigint(8) not null # disabled :boolean default(FALSE), not null -# moderator :boolean default(FALSE), not null # invite_id :bigint(8) # chosen_languages :string is an Array # created_by_application_id :bigint(8) diff --git a/config/routes/api.rb b/config/routes/api.rb index 2e79ecf46fb3d5..77af2519cd927c 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -204,7 +204,8 @@ resources :followed_tags, only: [:index] resources :lists, only: [:index, :create, :show, :update, :destroy] do - resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' + resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' + resource :tags, only: [:show, :create, :destroy], controller: 'lists/tags' end namespace :featured_tags do diff --git a/db/migrate/20240309162900_list_tags.rb b/db/migrate/20240309162900_list_tags.rb new file mode 100644 index 00000000000000..d6d8033cfce302 --- /dev/null +++ b/db/migrate/20240309162900_list_tags.rb @@ -0,0 +1,11 @@ +class ListTags < ActiveRecord::Migration[7.1] + def change + create_table :list_tags do |t| + t.belongs_to :list, foreign_key: { on_delete: :cascade }, null: false + t.belongs_to :tag, foreign_key: { on_delete: :cascade }, null: false + end + + add_index :list_tags, [:tag_id, :list_id], unique: true + add_index :list_tags, [:list_id, :tag_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index e4642b429695e8..3fd4e769b848d8 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.1].define(version: 2024_01_11_033014) do +ActiveRecord::Schema[7.1].define(version: 2024_03_09_162900) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -584,6 +584,15 @@ t.index ["list_id", "account_id"], name: "index_list_accounts_on_list_id_and_account_id" end + create_table "list_tags", force: :cascade do |t| + t.bigint "list_id", null: false + t.bigint "tag_id", null: false + t.index ["list_id", "tag_id"], name: "index_list_tags_on_list_id_and_tag_id" + t.index ["list_id"], name: "index_list_tags_on_list_id" + t.index ["tag_id", "list_id"], name: "index_list_tags_on_tag_id_and_list_id", unique: true + t.index ["tag_id"], name: "index_list_tags_on_tag_id" + end + create_table "lists", force: :cascade do |t| t.bigint "account_id", null: false t.string "title", default: "", null: false @@ -1248,6 +1257,8 @@ add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade add_foreign_key "list_accounts", "follows", on_delete: :cascade add_foreign_key "list_accounts", "lists", on_delete: :cascade + add_foreign_key "list_tags", "lists", on_delete: :cascade + add_foreign_key "list_tags", "tags", on_delete: :cascade add_foreign_key "lists", "accounts", on_delete: :cascade add_foreign_key "login_activities", "users", on_delete: :cascade add_foreign_key "markers", "users", on_delete: :cascade From c811a79c9b1a39d2a4c8f22f169f2466a94d4ee1 Mon Sep 17 00:00:00 2001 From: JS Moore Date: Sat, 9 Mar 2024 18:08:05 -0500 Subject: [PATCH 2/7] Add status to the list feed when the tag matches. --- app/controllers/api/v1/lists/accounts_controller.rb | 1 - app/controllers/api/v1/lists/tags_controller.rb | 1 - app/lib/feed_manager.rb | 2 -- app/models/tag.rb | 3 +++ app/services/fan_out_on_write_service.rb | 5 +++++ 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index 0f1c2cd0b9186f..0604ad60fc4635 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -32,7 +32,6 @@ def destroy private def set_list - Rails.logger.warn "This is concerning! #{current_account} and #{params}" @list = List.where(account: current_account).find(params[:list_id]) end diff --git a/app/controllers/api/v1/lists/tags_controller.rb b/app/controllers/api/v1/lists/tags_controller.rb index c255821856394c..c108d3d4df8e6c 100644 --- a/app/controllers/api/v1/lists/tags_controller.rb +++ b/app/controllers/api/v1/lists/tags_controller.rb @@ -32,7 +32,6 @@ def destroy private def set_list - Rails.logger.warn "This is tagged concerning! #{current_account} and #{params}" @list = List.where(account: current_account).find(params[:list_id]) end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index cdf4cdd22831d6..c71fc5c6495762 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -443,13 +443,11 @@ def filter_from_home?(status, receiver_id, crutches, timeline_type = :home) should_filter = !crutches[:following][status.in_reply_to_account_id] # and I'm not following the person it's a reply to should_filter &&= receiver_id != status.in_reply_to_account_id # and it's not a reply to me should_filter &&= status.account_id != status.in_reply_to_account_id # and it's not a self-reply - return !!should_filter elsif status.reblog? # Filter out a reblog should_filter = crutches[:hiding_reblogs][status.account_id] # if the reblogger's reblogs are suppressed should_filter ||= crutches[:blocked_by][status.reblog.account_id] # or if the author of the reblogged status is blocking me should_filter ||= crutches[:domain_blocking][status.reblog.account.domain] # or the author's domain is blocked - return !!should_filter end diff --git a/app/models/tag.rb b/app/models/tag.rb index f2168ae90470e7..98e4e6297a2325 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -28,6 +28,9 @@ class Tag < ApplicationRecord has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_many :followers, through: :passive_relationships, source: :account + has_many :list_tags, inverse_of: :tag, dependent: :destroy + has_many :lists, through: :list_tags + HASHTAG_SEPARATORS = "_\u00B7\u30FB\u200c" HASHTAG_FIRST_SEQUENCE_CHUNK_ONE = "[[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}]" HASHTAG_FIRST_SEQUENCE_CHUNK_TWO = "[[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_]" diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 71ab1ac494d53f..71f39036759a3a 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -113,6 +113,11 @@ def deliver_to_hashtag_followers! end def deliver_to_lists! + @status.tags.each do |tag| + FeedInsertWorker.push_bulk(tag.lists) do |list| + [@status.id, list.id, 'list', { 'update' => update? }] + end + end @account.lists_for_local_distribution.select(:id).reorder(nil).find_in_batches do |lists| FeedInsertWorker.push_bulk(lists) do |list| [@status.id, list.id, 'list', { 'update' => update? }] From ec5ec54af3d4d9352f53338c8db2956a06c80f76 Mon Sep 17 00:00:00 2001 From: JS Moore Date: Tue, 12 Mar 2024 13:36:15 -0400 Subject: [PATCH 3/7] Lots of UI work to get the tags to be editable in the list editor. --- app/controllers/api/base_controller.rb | 1 + .../api/v1/lists/tags_controller.rb | 15 ++- .../flavours/glitch/actions/importer/index.js | 16 +++ .../flavours/glitch/actions/lists.js | 107 +++++++++++++----- .../list_editor/components/account.jsx | 4 +- .../list_editor/components/add_tag.jsx | 77 +++++++++++++ .../features/list_editor/components/tag.jsx | 69 +++++++++++ .../glitch/features/list_editor/index.jsx | 58 +++++++--- .../flavours/glitch/reducers/list_editor.js | 26 ++++- .../flavours/glitch/selectors/index.js | 4 + .../flavours/glitch/styles/components.scss | 35 +++++- app/serializers/rest/tag_serializer.rb | 6 +- 12 files changed, 361 insertions(+), 57 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx create mode 100644 app/javascript/flavours/glitch/features/list_editor/components/tag.jsx diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 98fa1897ef0f6e..c13545f256ed9c 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -3,6 +3,7 @@ class Api::BaseController < ApplicationController DEFAULT_STATUSES_LIMIT = 20 DEFAULT_ACCOUNTS_LIMIT = 40 + DEFAULT_TAGS_LIMIT = 40 include Api::RateLimitHeaders include Api::AccessTokenTrackingConcern diff --git a/app/controllers/api/v1/lists/tags_controller.rb b/app/controllers/api/v1/lists/tags_controller.rb index c108d3d4df8e6c..6d71ee0d13ef42 100644 --- a/app/controllers/api/v1/lists/tags_controller.rb +++ b/app/controllers/api/v1/lists/tags_controller.rb @@ -20,12 +20,12 @@ def create @list.tags << tag end end - - render_empty + @tags = load_tags + render json: @tags, each_serializer: REST::TagSerializer end def destroy - ListTag.where(list: @list, tag_id: tag_id).destroy_all + ListTag.where(list: @list, tag_id: tag_ids).destroy_all render_empty end @@ -44,7 +44,14 @@ def load_tags end def list_tags - Tag.find(tag_ids) + names = tag_ids.select{|t| t !=~ /\A[0-9]+\Z/} + ids = tag_ids.select{|t| t =~ /\A[0-9]+\Z/} + existing_by_name = Tag.where(name: names.map{|n| Tag.normalize(n)}).select(:id, :name) + ids.push(*existing_by_name.map {|t| t.id}) + not_existing_by_name = names.select{|n| !existing_by_name.any? {|e| e.name == Tag.normalize(n)}} + created = Tag.find_or_create_by_names(not_existing_by_name) + ids.push(*created.map {|t| t.id}) + Tag.find(ids) end def tag_ids diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index 5fbc9bb5bbb64a..46be1107a22b45 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -6,6 +6,7 @@ export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; export const FILTERS_IMPORT = 'FILTERS_IMPORT'; +export const TAGS_IMPORT = 'TAGS_IMPORT'; function pushUnique(array, object) { if (array.every(element => element.id !== object.id)) { @@ -29,6 +30,10 @@ export function importPolls(polls) { return { type: POLLS_IMPORT, polls }; } +export function importTags(tags) { + return { type: TAGS_IMPORT, tags }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -49,6 +54,17 @@ export function importFetchedAccounts(accounts) { return importAccounts({ accounts: normalAccounts }); } +export function importFetchedTags(tags) { + return (dispatch, getState) => { + const uniqueTags = []; + function processTag(tag) { + pushUnique(uniqueTags, tag); + } + tags.forEach(processTag); + dispatch(importTags(uniqueTags)); + }; +} + export function importFetchedStatus(status) { return importFetchedStatuses([status]); } diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index b0789cd426450a..a986bbca2f0a86 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,7 +1,7 @@ import api from '../api'; import { showAlertForError } from './alerts'; -import { importFetchedAccounts } from './importer'; +import { importFetchedAccounts, importFetchedTags } from './importer'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -31,6 +31,10 @@ export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST'; export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS'; export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL'; +export const LIST_TAGS_FETCH_REQUEST = 'LIST_TAGS_FETCH_REQUEST'; +export const LIST_TAGS_FETCH_SUCCESS = 'LIST_TAGS_FETCH_SUCCESS'; +export const LIST_TAGS_FETCH_FAIL = 'LIST_TAGS_FETCH_FAIL'; + export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE'; export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY'; export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR'; @@ -118,6 +122,7 @@ export const setupListEditor = listId => (dispatch, getState) => { }); dispatch(fetchListAccounts(listId)); + dispatch(fetchListTags(listId)); }; export const changeListEditorTitle = value => ({ @@ -234,6 +239,33 @@ export const fetchListAccountsFail = (id, error) => ({ error, }); +export const fetchListTags = listId => (dispatch, getState) => { + dispatch(fetchListTagsRequest(listId)); + + api(getState).get(`/api/v1/lists/${listId}/tags`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedTags(data)); + dispatch(fetchListTagsSuccess(listId, data)); + }).catch(err => dispatch(fetchListTagsFail(listId, err))); +}; + +export const fetchListTagsFail = (id, error) => ({ + type: LIST_TAGS_FETCH_FAIL, + id, + error, +}); + +export const fetchListTagsRequest = id => ({ + type: LIST_TAGS_FETCH_REQUEST, + id, +}); + +export const fetchListTagsSuccess = (id, tags, next) => ({ + type: LIST_TAGS_FETCH_SUCCESS, + id, + tags, + next, +}); + export const fetchListSuggestions = q => (dispatch, getState) => { const params = { q, @@ -263,65 +295,84 @@ export const changeListSuggestions = value => ({ value, }); -export const addToListEditor = accountId => (dispatch, getState) => { - dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId)); +export const addToListEditor = (id, type) => (dispatch, getState) => { + dispatch(addToList(getState().getIn(['listEditor', 'listId']), id, type)); }; -export const addToList = (listId, accountId) => (dispatch, getState) => { - dispatch(addToListRequest(listId, accountId)); - - api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] }) - .then(() => dispatch(addToListSuccess(listId, accountId))) - .catch(err => dispatch(addToListFail(listId, accountId, err))); +export const addToList = (listId, id, type) => (dispatch, getState) => { + dispatch(addToListRequest(listId, id, type)); + + if ('tags' === type) { + api(getState).post(`/api/v1/lists/${listId}/tags`, { tag_ids: [id] }) + .then((data) => dispatch(addToListSuccess(listId, id, type, data))) + .catch(err => dispatch(addToListFail(listId, id, type, err))); + } else { + api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [id] }) + .then(() => dispatch(addToListSuccess(listId, id, type))) + .catch(err => dispatch(addToListFail(listId, id, type, err))); + } }; -export const addToListRequest = (listId, accountId) => ({ +export const addToListRequest = (listId, id, type) => ({ type: LIST_EDITOR_ADD_REQUEST, + addType: type, listId, - accountId, + id, }); -export const addToListSuccess = (listId, accountId) => ({ +export const addToListSuccess = (listId, id, type, data) => ({ type: LIST_EDITOR_ADD_SUCCESS, + addType: type, listId, - accountId, + id, + data, }); -export const addToListFail = (listId, accountId, error) => ({ +export const addToListFail = (listId, id, type, error) => ({ type: LIST_EDITOR_ADD_FAIL, + addType: type, listId, - accountId, + id, error, }); -export const removeFromListEditor = accountId => (dispatch, getState) => { - dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId)); +export const removeFromListEditor = (id, type) => (dispatch, getState) => { + dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), id, type)); }; -export const removeFromList = (listId, accountId) => (dispatch, getState) => { - dispatch(removeFromListRequest(listId, accountId)); +export const removeFromList = (listId, id, type) => (dispatch, getState) => { + dispatch(removeFromListRequest(listId, id, type)); - api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromListSuccess(listId, accountId))) - .catch(err => dispatch(removeFromListFail(listId, accountId, err))); + if ('tags' === type) { + api(getState).delete(`/api/v1/lists/${listId}/tags`, { params: { tag_ids: [id] } }) + .then(() => dispatch(removeFromListSuccess(listId, id, type))) + .catch(err => dispatch(removeFromListFail(listId, id, type, err))); + } else { + api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [id] } }) + .then(() => dispatch(removeFromListSuccess(listId, id, type))) + .catch(err => dispatch(removeFromListFail(listId, id, type, err))); + } }; -export const removeFromListRequest = (listId, accountId) => ({ +export const removeFromListRequest = (listId, id, type) => ({ type: LIST_EDITOR_REMOVE_REQUEST, + removeType: type, listId, - accountId, + id, }); -export const removeFromListSuccess = (listId, accountId) => ({ +export const removeFromListSuccess = (listId, id, type) => ({ type: LIST_EDITOR_REMOVE_SUCCESS, + removeType: type, listId, - accountId, + id, }); -export const removeFromListFail = (listId, accountId, error) => ({ +export const removeFromListFail = (listId, id, type, error) => ({ type: LIST_EDITOR_REMOVE_FAIL, + removeType: type, listId, - accountId, + id, error, }); diff --git a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx index 77d32af80a25a2..29a1ddb151714a 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx @@ -32,8 +32,8 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = (dispatch, { accountId }) => ({ - onRemove: () => dispatch(removeFromListEditor(accountId)), - onAdd: () => dispatch(addToListEditor(accountId)), + onRemove: () => dispatch(removeFromListEditor(accountId, 'accounts')), + onAdd: () => dispatch(addToListEditor(accountId, 'accounts')), }); class Account extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx b/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx new file mode 100644 index 00000000000000..490cf3668658de --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { connect } from 'react-redux'; + +import CancelIcon from '@/material-icons/400-24px/cancel.svg?react'; +import TagIcon from '@/material-icons/400-24px/tag.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; + +import { changeListSuggestions } from '../../../actions/lists'; +import { addToListEditor } from '../../../actions/lists'; + +const messages = defineMessages({ + addtag: { id: 'lists.addtag', defaultMessage: 'Enter a tag you\'d like to follow' }, +}); + +const mapStateToProps = state => ({ + value: state.getIn(['listEditor', 'suggestions', 'value']), +}); + +const mapDispatchToProps = dispatch => ({ + onSubmit: value => dispatch(addToListEditor(value, 'tags')), + onChange: value => dispatch(changeListSuggestions(value)), +}); + +class AddTag extends PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + value: PropTypes.string.isRequired, + onSubmit: PropTypes.func.isRequired, + }; + + handleChange = e => { + //this.props.value = e.target.value; + this.props.onChange(e.target.value); + }; + + handleKeyUp = e => { + if (e.keyCode === 13) { + this.props.onSubmit(this.props.value); + } + }; + + render() { + const { value, intl } = this.props; + const hasValue = value.length > 0; + + return ( +
+ + +
+ + +
+
+ ); + } + +} + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AddTag)); diff --git a/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx b/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx new file mode 100644 index 00000000000000..5a8761c5e76461 --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import TagIcon from '@/material-icons/400-24px/tag.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; + +import { removeFromListEditor, addToListEditor } from '../../../actions/lists'; +import { IconButton } from '../../../components/icon_button'; + +const messages = defineMessages({ + remove: { id: 'lists.tag.remove', defaultMessage: 'Remove from list' }, + add: { id: 'lists.tag.add', defaultMessage: 'Add to list' }, +}); + +const makeMapStateToProps = () => { + const mapStateToProps = (state, { tag, added }) => { + return { + tag: tag, + added: typeof added === 'undefined' ? state.getIn(['listEditor', 'tags', 'items']).includes(tag) : added, + } + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { tag }) => ({ + onRemove: () => dispatch(removeFromListEditor(tag.id, 'tags')), + onAdd: () => dispatch(addToListEditor(tag.id, 'tags')), +}); + +class Tag extends ImmutablePureComponent { + + static propTypes = { + tag: PropTypes.object.isRequired, + intl: PropTypes.object.isRequired, + onRemove: PropTypes.func.isRequired, + onAdd: PropTypes.func.isRequired, + added: PropTypes.bool, + }; + + static defaultProps = { + added: false, + }; + + render() { + const { tag, intl, onRemove } = this.props; + + return ( +
+ +
+ {tag.name} +
+ +
+ +
+
+ ); + } + +} + +export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Tag)); diff --git a/app/javascript/flavours/glitch/features/list_editor/index.jsx b/app/javascript/flavours/glitch/features/list_editor/index.jsx index 85e90169e80b2c..cfe947306675ad 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/index.jsx @@ -12,10 +12,13 @@ import { setupListEditor, clearListSuggestions, resetListEditor } from '../../ac import Motion from '../ui/util/optional_motion'; import Account from './components/account'; +import Tag from './components/tag'; import EditListForm from './components/edit_list_form'; import Search from './components/search'; +import AddTag from './components/add_tag'; const mapStateToProps = state => ({ + tags: state.getIn(['listEditor', 'tags', 'items']), accountIds: state.getIn(['listEditor', 'accounts', 'items']), searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']), }); @@ -27,6 +30,9 @@ const mapDispatchToProps = dispatch => ({ }); class ListEditor extends ImmutablePureComponent { + state = { + currentTab: 'accounts', + }; static propTypes = { listId: PropTypes.string.isRequired, @@ -35,6 +41,7 @@ class ListEditor extends ImmutablePureComponent { onInitialize: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired, + tags: ImmutablePropTypes.list.isRequired, accountIds: ImmutablePropTypes.list.isRequired, searchAccountIds: ImmutablePropTypes.list.isRequired, }; @@ -49,30 +56,45 @@ class ListEditor extends ImmutablePureComponent { onReset(); } + switchToTab(tab) { + this.setState({ ...this.state, currentTab: tab }); + } + render () { - const { accountIds, searchAccountIds, onClear } = this.props; + const { accountIds, tags, searchAccountIds, onClear } = this.props; const showSearch = searchAccountIds.size > 0; - return (
- - - -
-
- {accountIds.map(accountId => )} +
+ this.switchToTab('accounts')} className={'tab ' + ('accounts' == this.state.currentTab ? 'tab__active' : '')}>Accounts ({accountIds.size}) + this.switchToTab('tags')} className={'tab ' + ('tags' == this.state.currentTab ? 'tab__active' : '')}>Tags ({tags.size}) +
+
+ +
+
+ {accountIds.map(accountId => )} +
+ + {showSearch &&
} + + + {({ x }) => ( +
+ {searchAccountIds.map(accountId => )} +
+ )} +
+
+
+
+ +
+
+ {tags.map(tag => )} +
- - {showSearch &&
} - - - {({ x }) => ( -
- {searchAccountIds.map(accountId => )} -
- )} -
); diff --git a/app/javascript/flavours/glitch/reducers/list_editor.js b/app/javascript/flavours/glitch/reducers/list_editor.js index d3fd62adecbced..7903ba7b297b47 100644 --- a/app/javascript/flavours/glitch/reducers/list_editor.js +++ b/app/javascript/flavours/glitch/reducers/list_editor.js @@ -13,6 +13,9 @@ import { LIST_ACCOUNTS_FETCH_REQUEST, LIST_ACCOUNTS_FETCH_SUCCESS, LIST_ACCOUNTS_FETCH_FAIL, + LIST_TAGS_FETCH_REQUEST, + LIST_TAGS_FETCH_SUCCESS, + LIST_TAGS_FETCH_FAIL, LIST_EDITOR_SUGGESTIONS_READY, LIST_EDITOR_SUGGESTIONS_CLEAR, LIST_EDITOR_SUGGESTIONS_CHANGE, @@ -33,6 +36,12 @@ const initialState = ImmutableMap({ isLoading: false, }), + tags: ImmutableMap({ + items: ImmutableList(), + loaded: false, + isLoading: false, + }), + suggestions: ImmutableMap({ value: '', items: ImmutableList(), @@ -80,6 +89,16 @@ export default function listEditorReducer(state = initialState, action) { map.set('loaded', true); map.set('items', ImmutableList(action.accounts.map(item => item.id))); })); + case LIST_TAGS_FETCH_REQUEST: + return state.setIn(['tags', 'isLoading'], true); + case LIST_TAGS_FETCH_FAIL: + return state.setIn(['tags', 'isLoading'], false); + case LIST_TAGS_FETCH_SUCCESS: + return state.update('tags', tags => tags.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set('items', ImmutableList(action.tags)); + })); case LIST_EDITOR_SUGGESTIONS_CHANGE: return state.setIn(['suggestions', 'value'], action.value); case LIST_EDITOR_SUGGESTIONS_READY: @@ -90,9 +109,12 @@ export default function listEditorReducer(state = initialState, action) { map.set('value', ''); })); case LIST_EDITOR_ADD_SUCCESS: - return state.updateIn(['accounts', 'items'], list => list.unshift(action.accountId)); + if (action.data && action.data.data !== undefined) { + return state.setIn([action.addType, 'items'], ImmutableList(action.data.data)); + } + return state.updateIn([action.addType, 'items'], list => list.unshift(action.id)); case LIST_EDITOR_REMOVE_SUCCESS: - return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.accountId)); + return state.updateIn([action.removeType, 'items'], list => list.filterNot(item => item === action.id || item.id === action.id)); default: return state; } diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index 0bec708116eb1c..9b901ff4ca62e7 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -116,3 +116,7 @@ export const getAccountHidden = createSelector([ export const getStatusList = createSelector([ (state, type) => state.getIn(['status_lists', type, 'items']), ], (items) => items.toList()); + +export const getListEditorTagList = createSelector([ + (state) => state.getIn(['listEditor', 'tags', 'items']), +], (items) => items.toList()); diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index f08a6017f31316..97224423d6d66c 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -2042,7 +2042,7 @@ body > [data-popper-placement] { } } -.account__wrapper { +.account__wrapper, .tag__wrapper { display: flex; gap: 10px; align-items: center; @@ -2114,7 +2114,7 @@ a .account__avatar { } } -.account__relationship { +.account__relationship, .tag__relationship { white-space: nowrap; display: flex; align-items: center; @@ -7781,11 +7781,42 @@ noscript { border-radius: 8px 8px 0 0; } + .tab__container { + display: flex; + .tab { + flex-grow: 1; + flex-basis: 0; + display: block; + padding: 5px; + background: $ui-base-color; + text-align: center; + } + .tab.tab__active { + font-weight: bold; + background: lighten($ui-base-color, 13%); + } + } + .tab__inactive { + display: none; + } + .drawer__pager { height: 50vh; border-radius: 4px; } + .list_tag { + display: flex; + justify-content: space-between; + align-items: center; + .list_tag__display-name { + flex-grow: 1; + } + .list_tag__relationship { + flex-grow: 0; + } + } + .drawer__inner { border-radius: 0 0 8px 8px; diff --git a/app/serializers/rest/tag_serializer.rb b/app/serializers/rest/tag_serializer.rb index 017b572718ed3f..9e37ba11a16a9a 100644 --- a/app/serializers/rest/tag_serializer.rb +++ b/app/serializers/rest/tag_serializer.rb @@ -3,7 +3,7 @@ class REST::TagSerializer < ActiveModel::Serializer include RoutingHelper - attributes :name, :url, :history + attributes :name, :url, :history, :id attribute :following, if: :current_user? @@ -15,6 +15,10 @@ def name object.display_name end + def id + object.id.to_s + end + def following if instance_options && instance_options[:relationships] instance_options[:relationships].following_map[object.id] || false From 83c6600d1f5ba684785fff378d31295cbc908521 Mon Sep 17 00:00:00 2001 From: JS Moore Date: Tue, 12 Mar 2024 14:17:35 -0400 Subject: [PATCH 4/7] Linting fixes. --- .../api/v1/lists/tags_controller.rb | 12 +++---- .../flavours/glitch/actions/importer/index.js | 2 +- .../list_editor/components/add_tag.jsx | 4 +-- .../features/list_editor/components/tag.jsx | 12 +++---- .../glitch/features/list_editor/index.jsx | 36 +++++++++++++------ .../flavours/glitch/locales/en.json | 7 +++- .../flavours/glitch/styles/components.scss | 8 +++++ app/models/list_tag.rb | 3 -- app/services/fan_out_on_write_service.rb | 6 ++-- config/routes/api.rb | 4 +-- db/migrate/20240309162900_list_tags.rb | 16 +++++---- 11 files changed, 67 insertions(+), 43 deletions(-) diff --git a/app/controllers/api/v1/lists/tags_controller.rb b/app/controllers/api/v1/lists/tags_controller.rb index 6d71ee0d13ef42..3dd77aa416b10d 100644 --- a/app/controllers/api/v1/lists/tags_controller.rb +++ b/app/controllers/api/v1/lists/tags_controller.rb @@ -44,13 +44,13 @@ def load_tags end def list_tags - names = tag_ids.select{|t| t !=~ /\A[0-9]+\Z/} - ids = tag_ids.select{|t| t =~ /\A[0-9]+\Z/} - existing_by_name = Tag.where(name: names.map{|n| Tag.normalize(n)}).select(:id, :name) - ids.push(*existing_by_name.map {|t| t.id}) - not_existing_by_name = names.select{|n| !existing_by_name.any? {|e| e.name == Tag.normalize(n)}} + names = tag_ids.reject { |t| t =~ /\A[0-9]+\Z/ } + ids = tag_ids.select { |t| t =~ /\A[0-9]+\Z/ } + existing_by_name = Tag.where(name: names.map { |n| Tag.normalize(n) }).select(:id, :name) + ids.push(*existing_by_name.map { |t| t.id }) + not_existing_by_name = names.reject { |n| existing_by_name.any? { |e| e.name == Tag.normalize(n) }} created = Tag.find_or_create_by_names(not_existing_by_name) - ids.push(*created.map {|t| t.id}) + ids.push(*created.map(&:id) Tag.find(ids) end diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index 46be1107a22b45..6b1b0b35141ec8 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -55,7 +55,7 @@ export function importFetchedAccounts(accounts) { } export function importFetchedTags(tags) { - return (dispatch, getState) => { + return (dispatch) => { const uniqueTags = []; function processTag(tag) { pushUnique(uniqueTags, tag); diff --git a/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx b/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx index 490cf3668658de..1bd4a9e965ced8 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/add_tag.jsx @@ -11,8 +11,7 @@ import CancelIcon from '@/material-icons/400-24px/cancel.svg?react'; import TagIcon from '@/material-icons/400-24px/tag.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; -import { changeListSuggestions } from '../../../actions/lists'; -import { addToListEditor } from '../../../actions/lists'; +import { addToListEditor, changeListSuggestions } from '../../../actions/lists'; const messages = defineMessages({ addtag: { id: 'lists.addtag', defaultMessage: 'Enter a tag you\'d like to follow' }, @@ -33,6 +32,7 @@ class AddTag extends PureComponent { intl: PropTypes.object.isRequired, value: PropTypes.string.isRequired, onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, }; handleChange = e => { diff --git a/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx b/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx index 5a8761c5e76461..d7d808a52f37cd 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/tag.jsx @@ -5,8 +5,8 @@ import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import TagIcon from '@/material-icons/400-24px/tag.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import TagIcon from '@/material-icons/400-24px/tag.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; import { removeFromListEditor, addToListEditor } from '../../../actions/lists'; @@ -18,12 +18,10 @@ const messages = defineMessages({ }); const makeMapStateToProps = () => { - const mapStateToProps = (state, { tag, added }) => { - return { - tag: tag, - added: typeof added === 'undefined' ? state.getIn(['listEditor', 'tags', 'items']).includes(tag) : added, - } - }; + const mapStateToProps = (state, { tag, added }) => ({ + tag: tag, + added: typeof added === 'undefined' ? state.getIn(['listEditor', 'tags', 'items']).includes(tag) : added, + }); return mapStateToProps; }; diff --git a/app/javascript/flavours/glitch/features/list_editor/index.jsx b/app/javascript/flavours/glitch/features/list_editor/index.jsx index cfe947306675ad..5b58e69a145f7c 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/index.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -12,10 +12,15 @@ import { setupListEditor, clearListSuggestions, resetListEditor } from '../../ac import Motion from '../ui/util/optional_motion'; import Account from './components/account'; -import Tag from './components/tag'; +import AddTag from './components/add_tag'; import EditListForm from './components/edit_list_form'; import Search from './components/search'; -import AddTag from './components/add_tag'; +import Tag from './components/tag'; + +const messages = defineMessages({ + account_tab: { id: 'lists.account_tab', defaultMessage: 'Accounts' }, + tag_tab: { id: 'lists.tag_tab', defaultMessage: 'Tags' }, +}); const mapStateToProps = state => ({ tags: state.getIn(['listEditor', 'tags', 'items']), @@ -35,6 +40,7 @@ class ListEditor extends ImmutablePureComponent { }; static propTypes = { + intl: PropTypes.object.isRequired, listId: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -46,31 +52,39 @@ class ListEditor extends ImmutablePureComponent { searchAccountIds: ImmutablePropTypes.list.isRequired, }; - componentDidMount () { + componentDidMount() { const { onInitialize, listId } = this.props; onInitialize(listId); } - componentWillUnmount () { + componentWillUnmount() { const { onReset } = this.props; onReset(); } + switchToAccounts() { + this.switchToTab('accounts'); + } + + switchToTags() { + this.switchToTab('tags'); + } + switchToTab(tab) { this.setState({ ...this.state, currentTab: tab }); } - render () { - const { accountIds, tags, searchAccountIds, onClear } = this.props; + render() { + const { accountIds, tags, searchAccountIds, onClear, intl } = this.props; const showSearch = searchAccountIds.size > 0; return (
- this.switchToTab('accounts')} className={'tab ' + ('accounts' == this.state.currentTab ? 'tab__active' : '')}>Accounts ({accountIds.size}) - this.switchToTab('tags')} className={'tab ' + ('tags' == this.state.currentTab ? 'tab__active' : '')}>Tags ({tags.size}) +
{intl.formatMessage(messages.account_tab)} ({accountIds.size})
+
{intl.formatMessage(messages.tag_tab)} ({tags.size})
-
+
@@ -88,7 +102,7 @@ class ListEditor extends ImmutablePureComponent {
-
+
diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 8de7059787ed96..0e8750b47c250b 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -53,6 +53,11 @@ "keyboard_shortcuts.bookmark": "to bookmark", "keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting", "keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots", + "lists.account_tab": "Accounts", + "lists.addtag": "Enter a tag you'd like to follow", + "lists.tag.add": "Add to list", + "lists.tag.remove": "Remove from list", + "lists.tag_tab": "Tags", "moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.", "navigation_bar.app_settings": "App settings", "navigation_bar.featured_users": "Featured users", @@ -156,4 +161,4 @@ "status.local_only": "Only visible from your instance", "status.uncollapse": "Uncollapse", "suggestions.dismiss": "Dismiss suggestion" -} +} \ No newline at end of file diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 97224423d6d66c..cf2d5e00557086 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -7783,6 +7783,7 @@ noscript { .tab__container { display: flex; + .tab { flex-grow: 1; flex-basis: 0; @@ -7791,11 +7792,13 @@ noscript { background: $ui-base-color; text-align: center; } + .tab.tab__active { font-weight: bold; background: lighten($ui-base-color, 13%); } } + .tab__inactive { display: none; } @@ -7806,12 +7809,17 @@ noscript { } .list_tag { + padding: 10px; // glitch: reduced padding + border-bottom: 1px solid lighten($ui-base-color, 8%); + gap: 10px; display: flex; justify-content: space-between; align-items: center; + .list_tag__display-name { flex-grow: 1; } + .list_tag__relationship { flex-grow: 0; } diff --git a/app/models/list_tag.rb b/app/models/list_tag.rb index 3b532771729a3a..0397c501cf2d8a 100644 --- a/app/models/list_tag.rb +++ b/app/models/list_tag.rb @@ -14,7 +14,4 @@ class ListTag < ApplicationRecord belongs_to :tag validates :tag_id, uniqueness: { scope: :list_id } - - private - end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 71f39036759a3a..f006ff08983d75 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -114,9 +114,9 @@ def deliver_to_hashtag_followers! def deliver_to_lists! @status.tags.each do |tag| - FeedInsertWorker.push_bulk(tag.lists) do |list| - [@status.id, list.id, 'list', { 'update' => update? }] - end + FeedInsertWorker.push_bulk(tag.lists) do |list| + [@status.id, list.id, 'list', { 'update' => update? }] + end end @account.lists_for_local_distribution.select(:id).reorder(nil).find_in_batches do |lists| FeedInsertWorker.push_bulk(lists) do |list| diff --git a/config/routes/api.rb b/config/routes/api.rb index 77af2519cd927c..5830c21f95c0bb 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -204,8 +204,8 @@ resources :followed_tags, only: [:index] resources :lists, only: [:index, :create, :show, :update, :destroy] do - resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' - resource :tags, only: [:show, :create, :destroy], controller: 'lists/tags' + resource :accounts, only: [:show, :create, :destroy], controller: 'lists/accounts' + resource :tags, only: [:show, :create, :destroy], controller: 'lists/tags' end namespace :featured_tags do diff --git a/db/migrate/20240309162900_list_tags.rb b/db/migrate/20240309162900_list_tags.rb index d6d8033cfce302..eed4753ab46fa9 100644 --- a/db/migrate/20240309162900_list_tags.rb +++ b/db/migrate/20240309162900_list_tags.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + class ListTags < ActiveRecord::Migration[7.1] def change - create_table :list_tags do |t| - t.belongs_to :list, foreign_key: { on_delete: :cascade }, null: false - t.belongs_to :tag, foreign_key: { on_delete: :cascade }, null: false - end - - add_index :list_tags, [:tag_id, :list_id], unique: true - add_index :list_tags, [:list_id, :tag_id] + create_table :list_tags do |t| + t.belongs_to :list, foreign_key: { on_delete: :cascade }, null: false + t.belongs_to :tag, foreign_key: { on_delete: :cascade }, null: false + end + + add_index :list_tags, [:tag_id, :list_id], unique: true + add_index :list_tags, [:list_id, :tag_id] end end From c08dfb1e0c831da75ccb24ed35381c4434869a5e Mon Sep 17 00:00:00 2001 From: JS Moore Date: Tue, 12 Mar 2024 15:08:12 -0400 Subject: [PATCH 5/7] Linting is hard? --- .../api/v1/lists/tags_controller.rb | 2 +- .../glitch/features/list_editor/index.jsx | 22 ++++--------------- .../flavours/glitch/locales/en.json | 2 +- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/app/controllers/api/v1/lists/tags_controller.rb b/app/controllers/api/v1/lists/tags_controller.rb index 3dd77aa416b10d..ddcab4053e3b14 100644 --- a/app/controllers/api/v1/lists/tags_controller.rb +++ b/app/controllers/api/v1/lists/tags_controller.rb @@ -50,7 +50,7 @@ def list_tags ids.push(*existing_by_name.map { |t| t.id }) not_existing_by_name = names.reject { |n| existing_by_name.any? { |e| e.name == Tag.normalize(n) }} created = Tag.find_or_create_by_names(not_existing_by_name) - ids.push(*created.map(&:id) + ids.push(*created.map(&:id)) Tag.find(ids) end diff --git a/app/javascript/flavours/glitch/features/list_editor/index.jsx b/app/javascript/flavours/glitch/features/list_editor/index.jsx index 5b58e69a145f7c..2f427adbe4af3b 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/index.jsx @@ -37,10 +37,8 @@ const mapDispatchToProps = dispatch => ({ class ListEditor extends ImmutablePureComponent { state = { currentTab: 'accounts', - }; - + } static propTypes = { - intl: PropTypes.object.isRequired, listId: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -62,27 +60,15 @@ class ListEditor extends ImmutablePureComponent { onReset(); } - switchToAccounts() { - this.switchToTab('accounts'); - } - - switchToTags() { - this.switchToTab('tags'); - } - - switchToTab(tab) { - this.setState({ ...this.state, currentTab: tab }); - } - render() { const { accountIds, tags, searchAccountIds, onClear, intl } = this.props; const showSearch = searchAccountIds.size > 0; return ( -
+
{this.state.currentTab}
-
{intl.formatMessage(messages.account_tab)} ({accountIds.size})
-
{intl.formatMessage(messages.tag_tab)} ({tags.size})
+ +
diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 0e8750b47c250b..dbffb3cffde6f2 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -161,4 +161,4 @@ "status.local_only": "Only visible from your instance", "status.uncollapse": "Uncollapse", "suggestions.dismiss": "Dismiss suggestion" -} \ No newline at end of file +} From a34d5c1bfec0adfb8014cbbfab1079b942b8cdd8 Mon Sep 17 00:00:00 2001 From: JS Moore Date: Tue, 12 Mar 2024 16:51:31 -0400 Subject: [PATCH 6/7] More linting stuff -_- wish I could get this to run locally. --- .../api/v1/lists/tags_controller.rb | 8 +++---- .../glitch/features/list_editor/index.jsx | 21 ++++++++++++++++--- .../flavours/glitch/styles/components.scss | 3 +++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v1/lists/tags_controller.rb b/app/controllers/api/v1/lists/tags_controller.rb index ddcab4053e3b14..e0ab70a4f7d927 100644 --- a/app/controllers/api/v1/lists/tags_controller.rb +++ b/app/controllers/api/v1/lists/tags_controller.rb @@ -44,11 +44,11 @@ def load_tags end def list_tags - names = tag_ids.reject { |t| t =~ /\A[0-9]+\Z/ } - ids = tag_ids.select { |t| t =~ /\A[0-9]+\Z/ } + names = tag_ids.grep_v(/\A[0-9]+\Z/) + ids = tag_ids.grep(/\A[0-9]+\Z/) existing_by_name = Tag.where(name: names.map { |n| Tag.normalize(n) }).select(:id, :name) - ids.push(*existing_by_name.map { |t| t.id }) - not_existing_by_name = names.reject { |n| existing_by_name.any? { |e| e.name == Tag.normalize(n) }} + ids.push(*existing_by_name.map(&:id)) + not_existing_by_name = names.reject { |n| existing_by_name.any? { |e| e.name == Tag.normalize(n) } } created = Tag.find_or_create_by_names(not_existing_by_name) ids.push(*created.map(&:id)) Tag.find(ids) diff --git a/app/javascript/flavours/glitch/features/list_editor/index.jsx b/app/javascript/flavours/glitch/features/list_editor/index.jsx index 2f427adbe4af3b..1d5eaf8a3f62d9 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/index.jsx @@ -37,7 +37,8 @@ const mapDispatchToProps = dispatch => ({ class ListEditor extends ImmutablePureComponent { state = { currentTab: 'accounts', - } + }; + static propTypes = { listId: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, @@ -60,6 +61,20 @@ class ListEditor extends ImmutablePureComponent { onReset(); } + constructor(props) { + super(props); + this.switchToAccounts = this.switchToAccounts.bind(this); + this.switchToTags = this.switchToTags.bind(this); + } + + switchToAccounts() { + this.setState({ currentTab: 'accounts' }); + } + + switchToTags() { + this.setState({ currentTab: 'tags' }); + } + render() { const { accountIds, tags, searchAccountIds, onClear, intl } = this.props; const showSearch = searchAccountIds.size > 0; @@ -67,8 +82,8 @@ class ListEditor extends ImmutablePureComponent {
{this.state.currentTab}
- - + +
diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index cf2d5e00557086..e523ae62439dbc 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -7791,6 +7791,8 @@ noscript { padding: 5px; background: $ui-base-color; text-align: center; + border: 0; + color: $primary-text-color; } .tab.tab__active { @@ -7815,6 +7817,7 @@ noscript { display: flex; justify-content: space-between; align-items: center; + min-height: 36px; .list_tag__display-name { flex-grow: 1; From 4c3486815ef6f17dd35e6dc1a0dd1b1e72c06ab0 Mon Sep 17 00:00:00 2001 From: JS Moore Date: Tue, 12 Mar 2024 16:58:56 -0400 Subject: [PATCH 7/7] One last try. --- app/javascript/flavours/glitch/styles/components.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index e523ae62439dbc..dd1d3f154a7cf9 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -2042,7 +2042,8 @@ body > [data-popper-placement] { } } -.account__wrapper, .tag__wrapper { +.account__wrapper, +.tag__wrapper { display: flex; gap: 10px; align-items: center; @@ -2114,7 +2115,8 @@ a .account__avatar { } } -.account__relationship, .tag__relationship { +.account__relationship, +.tag__relationship { white-space: nowrap; display: flex; align-items: center;