From 7c05f56fe81f99d426997ce47cbe3e15b1a44cf2 Mon Sep 17 00:00:00 2001 From: "Pia B." Date: Tue, 12 May 2026 18:13:01 +0200 Subject: [PATCH 1/9] Add batch actions to collections and possibility to report multiple collections (#38991) --- .../admin/collections_controller.rb | 39 +++++++++++++++ app/models/admin/collection_batch_action.rb | 42 ++++++++++++++++ app/views/admin/collections/index.html.haml | 49 ++++++++++++++++++ app/views/admin/reports/show.html.haml | 8 +-- .../shared/_collection_batch_row.html.haml | 2 +- config/locales/en.yml | 7 +++ config/routes/admin.rb | 2 +- .../admin/collection_batch_action_spec.rb | 50 +++++++++++++++++++ spec/requests/admin/collections_spec.rb | 44 ++++++++++++++-- 9 files changed, 234 insertions(+), 9 deletions(-) create mode 100644 app/models/admin/collection_batch_action.rb create mode 100644 app/views/admin/collections/index.html.haml create mode 100644 spec/models/admin/collection_batch_action_spec.rb diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 4701500f9f..2698abbd45 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -4,13 +4,48 @@ module Admin class CollectionsController < BaseController before_action :set_account before_action :set_collection, only: :show + before_action :set_collections, except: :show + + PER_PAGE = 20 + + def index + authorize [:admin, :collection], :index? + @collection_batch_action = Admin::CollectionBatchAction.new + end def show authorize @collection, :show? end + def batch + authorize [:admin, :collection], :index? + + @collection_batch_action = Admin::CollectionBatchAction.new(admin_collection_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: 'report')) + + @collection_batch_action.save! + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.collections.no_collection_selected') + ensure + redirect_to after_create_redirect_path + end + private + def after_create_redirect_path + report_id = @collections_batch_action&.report_id || params[:report_id] + + if report_id.present? + admin_report_path(report_id) + else + admin_account_collections_path(params[:account_id], params[:page]) + end + end + + def admin_collection_batch_action_params + params + .expect(admin_collection_batch_action: [collection_ids: []]) + end + def set_account @account = Account.find(params[:account_id]) end @@ -18,5 +53,9 @@ module Admin def set_collection @collection = @account.collections.includes(accepted_collection_items: :account).find(params[:id]) end + + def set_collections + @collections = @account.collections.includes(accepted_collection_items: :account).page(params[:page]).per(PER_PAGE) + end end end diff --git a/app/models/admin/collection_batch_action.rb b/app/models/admin/collection_batch_action.rb new file mode 100644 index 0000000000..d08f17d747 --- /dev/null +++ b/app/models/admin/collection_batch_action.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class Admin::CollectionBatchAction < Admin::BaseAction + TYPES = %w( + report + remove_from_report + ).freeze + + attr_accessor :collection_ids + + private + + def process_action! + return unless collection_ids + + add_to_report! + end + + def add_to_report! + @report = Report.new(report_params) unless with_report? + + @report.collections = (@report.collections + selected_collections).uniq + @report.save! + @report_id = @report.id + end + + def selected_collections + @report.target_account.collections.where(id: collection_ids) + end + + def report_params + { account: current_account, target_account: target_account } + end + + def collection + @collection ||= Collection.where(id: collection_ids.first) + end + + def target_account + @target_account ||= collection.first.account + end +end diff --git a/app/views/admin/collections/index.html.haml b/app/views/admin/collections/index.html.haml new file mode 100644 index 0000000000..10c9e65b5e --- /dev/null +++ b/app/views/admin/collections/index.html.haml @@ -0,0 +1,49 @@ +- content_for :page_title do + = t('admin.collections.title', name: @account.pretty_acct) + +.filters + .filter-subset + %ul + %li= filter_link_to t('generic.all'), id: nil + .back-link + - if params[:report_id] + = link_to admin_report_path(params[:report_id].to_i) do + = material_symbol 'chevron_left' + = t('admin.collections.back_to_report') + - else + = link_to admin_account_path(@account.id) do + = material_symbol 'chevron_left' + = t('admin.collections.back_to_account') + +%hr.spacer/ + + += form_with model: @collection_batch_action, url: batch_admin_account_collections_path(@account.id) do |f| + = hidden_field_tag :page, params[:page] || 1 + = hidden_field_tag :report_id, params[:report_id] + + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + - unless @collections.empty? + - if params[:report_id] + = f.button safe_join([material_symbol('add'), t('admin.collections.batch.add_to_report', id: params[:report_id])]), + class: 'table-action-link', + data: { confirm: t('admin.reports.are_you_sure') }, + name: :report, + type: :submit + - else + = f.button safe_join([material_symbol('flag'), t('admin.collections.batch.report')]), + class: 'table-action-link', + data: { confirm: t('admin.reports.are_you_sure') }, + name: :report, + type: :submit + .batch-table__body + - if @collections.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'admin/shared/collection_batch_row', collection: @collections, as: :collection, locals: { f: f } + += paginate @collections diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 2d20b6a969..0aa2f69ba5 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -67,17 +67,19 @@ %summary = t 'admin.reports.collections', count: @report.collections.size - %form + = form_with model: @form, url: batch_admin_account_collections_path(@report.target_account_id, report_id: @report.id) do |f| .batch-table .batch-table__toolbar %label.batch-table__toolbar__select.batch-checkbox-all - -# = check_box_tag :batch_checkbox_all, nil, false .batch-table__toolbar__actions + = link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]), + admin_account_collections_path(@report.target_account_id, report_id: @report.id), + class: 'table-action-link' .batch-table__body - if @report.collections.empty? = nothing_here 'nothing-here--under-tabs' - else - = render partial: 'admin/shared/collection_batch_row', collection: @report.collections, as: :collection + = render partial: 'admin/shared/collection_batch_row', collection: @report.collections, as: :collection, locals: { f: f } - if @report.unresolved? %hr.spacer/ diff --git a/app/views/admin/shared/_collection_batch_row.html.haml b/app/views/admin/shared/_collection_batch_row.html.haml index 8bf7857e95..8f42ab8db4 100644 --- a/app/views/admin/shared/_collection_batch_row.html.haml +++ b/app/views/admin/shared/_collection_batch_row.html.haml @@ -1,5 +1,5 @@ .batch-table__row %label.batch-table__row__select.batch-checkbox - -# = f.check_box :collection_ids, { multiple: true, include_hidden: false }, collection.id + = f.check_box :collection_ids, { multiple: true, include_hidden: false }, collection.id .batch-table__row__content = render partial: 'admin/shared/collection', object: collection diff --git a/config/locales/en.yml b/config/locales/en.yml index b0533881ba..4a95e738b4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -345,12 +345,19 @@ en: updated_msg: Announcement successfully updated! collections: accounts: Accounts + back_to_account: Back to account page + back_to_report: Back to report page + batch: + add_to_report: 'Add to report #%{id}' + report: Report collection_title: Collection by %{name} contents: Contents + no_collection_selected: No collections were changed as none were selected number_of_accounts: one: 1 account other: "%{count} accounts" open: Open + title: Account collections - @%{name} view_publicly: View publicly critical_update_pending: Critical update pending custom_emojis: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 770a9cd777..a48defb3b9 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -157,7 +157,7 @@ namespace :admin do resource :reset, only: [:create] resource :action, only: [:new, :create], controller: 'account_actions' - resources :collections, only: [:show] + resources :collections, only: [:index, :show], concerns: :batch resources :statuses, only: [:index, :show], concerns: :batch resources :relationships, only: [:index] diff --git a/spec/models/admin/collection_batch_action_spec.rb b/spec/models/admin/collection_batch_action_spec.rb new file mode 100644 index 0000000000..0ed57817e0 --- /dev/null +++ b/spec/models/admin/collection_batch_action_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::CollectionBatchAction do + subject do + described_class.new( + current_account:, + collection_ids:, + report_id:, + type: + ) + end + + let(:account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } + let(:current_account) { Fabricate(:admin_user).account } + let(:collection) { Fabricate(:collection, account: account) } + let(:collection_ids) { [collection.id] } + let(:report) { Fabricate(:report, account: target_account, target_account: account) } + let(:type) { 'report' } + + before 'create collection report' do + report.collections << collection + report.save! + end + + describe '#save!' do + context 'when with_report? is true' do + let(:other_collection) { Fabricate(:collection, account: account) } + let(:report_id) { report.id } + + it 'creates no report and adds the collection to the existing report' do + collection_ids << other_collection.id + + expect { subject.save! }.to_not change(Report, :count) + expect(report.collections).to include(other_collection) + end + end + + context 'when with_report? is false' do + let(:report_id) { nil } + + it 'creates a new report from the collection_ids' do + expect { subject.save! }.to change(Report, :count).by(1) + expect(Report.last.collections).to include(collection) + end + end + end +end diff --git a/spec/requests/admin/collections_spec.rb b/spec/requests/admin/collections_spec.rb index 0e87e277ae..6cb3a1ec2d 100644 --- a/spec/requests/admin/collections_spec.rb +++ b/spec/requests/admin/collections_spec.rb @@ -3,13 +3,19 @@ require 'rails_helper' RSpec.describe 'Admin Collections' do + let(:account) { Fabricate(:account) } + let(:other_account) { Fabricate(:account) } + let(:collection) { Fabricate(:collection, account: account) } + let!(:second_collection) { Fabricate(:collection, account: account) } + let(:report) { Fabricate(:report, account: other_account, target_account: account) } + + before do + sign_in Fabricate(:admin_user) + end + describe 'GET /admin/accounts/:account_id/collections/:id' do let(:collection) { Fabricate(:collection) } - before do - sign_in Fabricate(:admin_user) - end - it 'returns success' do get admin_account_collection_path(collection.account_id, collection) @@ -17,4 +23,34 @@ RSpec.describe 'Admin Collections' do .to have_http_status(200) end end + + describe 'GET /admin/accounts/:account_id/collections/' do + it 'returns http success' do + get admin_account_collections_path(account_id: account.id, id: collection.id) + expect(response).to have_http_status(200) + end + end + + describe 'POST /admin/accounts/:account_id/collections/batch' do + subject { post batch_admin_account_collections_path(account_id: account.id, report_id: report_id, admin_collection_batch_action: { collection_ids: [collection.id, second_collection.id] }) } + + context 'with a valid report' do + let(:report_id) { report.id } + + it 'redirects to the report page' do + report.collections << collection + subject + expect(response).to redirect_to(admin_report_path(report.id)) + end + end + + context 'with an invalid report' do + let(:report_id) { nil } + + it 'redirects to the account collections page' do + subject + expect(response).to redirect_to(admin_account_collections_path(account.id)) + end + end + end end From cb5c5432b359649261b76617359d1212743b100e Mon Sep 17 00:00:00 2001 From: Michael Stanclift Date: Tue, 12 May 2026 11:16:55 -0500 Subject: [PATCH 2/9] Narrow scope of Docker build cache in Github workflows (#39014) --- .github/workflows/build-container-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml index ae85d9d5e6..a899e186b8 100644 --- a/.github/workflows/build-container-image.yml +++ b/.github/workflows/build-container-image.yml @@ -87,8 +87,8 @@ jobs: platforms: ${{ matrix.platform }} provenance: false push: ${{ inputs.push_to_images != '' }} - cache-from: ${{ inputs.cache && 'type=gha' || '' }} - cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }} + cache-from: ${{ inputs.cache && format('type=gha,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }} + cache-to: ${{ inputs.cache && format('type=gha,mode=max,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }} outputs: type=image,"name=${{ env.IMAGE_NAMES }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push_to_images != '' }} - name: Export digest From bbb3392dbe35da834b0f31a5d3b15b19480fb688 Mon Sep 17 00:00:00 2001 From: "Pia B." Date: Tue, 12 May 2026 19:46:24 +0200 Subject: [PATCH 3/9] add collections icon to admin report interface (#39009) --- app/views/admin/reports/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index 1049cf733e..070d8c8518 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -69,7 +69,7 @@ - if Mastodon::Feature.collections_enabled? %span.report-card__summary__item__content__icon{ title: t('admin.accounts.collections') } - = material_symbol('groups-fill') + = material_symbol('category') = report.collections.size - if report.forwarded? From 758db36ec7da7f3e402b5290f06a120e8517ed6d Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 13 May 2026 10:38:17 +0200 Subject: [PATCH 4/9] Refactor account header banners (#38921) --- .../components/account_header/banners.tsx | 140 ++++++++++++ .../components/account_header/index.tsx | 16 +- .../account_header/memorial_note.tsx | 12 - .../components/account_header/moved_note.tsx | 46 ---- .../account_header/styles.module.scss | 63 +++++- .../account_header/subscription_form.tsx | 30 ++- .../account/components/account_note.tsx | 131 ----------- .../account/components/domain_pill.tsx | 210 ------------------ .../components/follow_request_note.jsx | 41 ---- .../follow_request_note_container.js | 17 -- app/javascript/mastodon/locales/en.json | 15 -- .../styles/mastodon/components.scss | 41 ---- 12 files changed, 215 insertions(+), 547 deletions(-) create mode 100644 app/javascript/mastodon/components/account_header/banners.tsx delete mode 100644 app/javascript/mastodon/components/account_header/memorial_note.tsx delete mode 100644 app/javascript/mastodon/components/account_header/moved_note.tsx delete mode 100644 app/javascript/mastodon/features/account/components/account_note.tsx delete mode 100644 app/javascript/mastodon/features/account/components/domain_pill.tsx delete mode 100644 app/javascript/mastodon/features/account/components/follow_request_note.jsx delete mode 100644 app/javascript/mastodon/features/account/containers/follow_request_note_container.js diff --git a/app/javascript/mastodon/components/account_header/banners.tsx b/app/javascript/mastodon/components/account_header/banners.tsx new file mode 100644 index 0000000000..269f65b8d1 --- /dev/null +++ b/app/javascript/mastodon/components/account_header/banners.tsx @@ -0,0 +1,140 @@ +import { useCallback } from 'react'; +import type { FC, ReactElement, ReactNode } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { + authorizeFollowRequest, + rejectFollowRequest, +} from '@/mastodon/actions/accounts'; +import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility'; +import { useRelationship } from '@/mastodon/hooks/useRelationship'; +import type { Account } from '@/mastodon/models/account'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; + +import { AvatarOverlay } from '../avatar_overlay'; +import { Button } from '../button'; +import { DisplayName } from '../display_name'; +import { Icon } from '../icon'; + +import classes from './styles.module.scss'; + +export const AccountBanners: FC<{ account: Account }> = ({ account }) => { + const { suspended, hidden } = useAccountVisibility(account.id); + const relationship = useRelationship(account.id); + + if (hidden) { + return null; + } + + let banner: ReactNode = null; + + if (account.memorial) { + banner = ( + + + + ); + } + + if (account.moved) { + banner = ; + } + + if (!suspended && relationship?.requested_by) { + banner = ; + } + + if (!banner) { + return null; + } + + return
{banner}
; +}; + +const FollowRequestNote: FC<{ account: Account }> = ({ account }) => { + const accountId = account.id; + const dispatch = useAppDispatch(); + const handleAuthorize = useCallback(() => { + dispatch(authorizeFollowRequest(accountId)); + }, [accountId, dispatch]); + const handleReject = useCallback(() => { + dispatch(rejectFollowRequest(accountId)); + }, [accountId, dispatch]); + + return ( + <> + + }} + /> + + +
+ + + +
+ + ); +}; + +const MovedNote: React.FC<{ + account: Account; + targetAccountId: string; +}> = ({ account: from, targetAccountId }) => { + const to = useAppSelector((state) => state.accounts.get(targetAccountId)); + + return ( + <> + + , + }} + /> + + +
+ + + + + + + + +
+ + ); +}; + +const MessageText: React.FC<{ children: ReactElement }> = ({ children }) => ( +
{children}
+); diff --git a/app/javascript/mastodon/components/account_header/index.tsx b/app/javascript/mastodon/components/account_header/index.tsx index 33c36ae1c9..df2493c7e9 100644 --- a/app/javascript/mastodon/components/account_header/index.tsx +++ b/app/javascript/mastodon/components/account_header/index.tsx @@ -5,7 +5,6 @@ import classNames from 'classnames'; import { Helmet } from '@unhead/react/helmet'; import { openModal } from '@/mastodon/actions/modal'; -import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container'; import { useLayout } from '@/mastodon/hooks/useLayout'; import { useVisibility } from '@/mastodon/hooks/useVisibility'; import { @@ -22,10 +21,9 @@ import { Avatar } from '../avatar'; import { AnimateEmojiProvider } from '../emoji/context'; import { FamiliarFollowers } from '../familiar_followers'; +import { AccountBanners } from './banners'; import { AccountButtons } from './buttons'; import { AccountHeaderFields } from './fields'; -import { MemorialNote } from './memorial_note'; -import { MovedNote } from './moved_note'; import { AccountName } from './name'; import { AccountNote } from './note'; import { AccountNumberFields } from './number_fields'; @@ -51,9 +49,6 @@ export const AccountHeader: React.FC<{ }> = ({ accountId, hideTabs }) => { const dispatch = useAppDispatch(); const account = useAppSelector((state) => state.accounts.get(accountId)); - const relationship = useAppSelector((state) => - state.relationships.get(accountId), - ); const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); const handleOpenAvatar = useCallback( @@ -98,18 +93,11 @@ export const AccountHeader: React.FC<{ return (
- {!hidden && account.memorial && } - {!hidden && account.moved && ( - - )} + - {!suspendedOrHidden && !account.moved && relationship?.requested_by && ( - - )} -
{!suspendedOrHidden && ( ( -
-
- -
-
-); diff --git a/app/javascript/mastodon/components/account_header/moved_note.tsx b/app/javascript/mastodon/components/account_header/moved_note.tsx deleted file mode 100644 index 153c206bbc..0000000000 --- a/app/javascript/mastodon/components/account_header/moved_note.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useAppSelector } from '@/mastodon/store'; - -import { AvatarOverlay } from '../avatar_overlay'; -import { DisplayName } from '../display_name'; - -export const MovedNote: React.FC<{ - accountId: string; - targetAccountId: string; -}> = ({ accountId, targetAccountId }) => { - const from = useAppSelector((state) => state.accounts.get(accountId)); - const to = useAppSelector((state) => state.accounts.get(targetAccountId)); - - return ( -
-
- , - }} - /> -
- -
- -
- -
- - - - - - -
-
- ); -}; diff --git a/app/javascript/mastodon/components/account_header/styles.module.scss b/app/javascript/mastodon/components/account_header/styles.module.scss index 43c5b5d83b..a54ca18c8a 100644 --- a/app/javascript/mastodon/components/account_header/styles.module.scss +++ b/app/javascript/mastodon/components/account_header/styles.module.scss @@ -453,13 +453,24 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; border-bottom: 1px solid var(--color-border-primary); } +// Banners + +.bannerWrapper, .bannerBase { box-sizing: border-box; - padding: 16px; - border-radius: 12px; - background: var(--color-bg-secondary); display: flex; flex-direction: column; +} + +.bannerWrapper { + background: var(--color-bg-tertiary); + padding: 16px; + align-items: center; +} + +.bannerBase { + border-radius: 12px; + background: var(--color-bg-secondary); gap: 12px; justify-content: center; align-items: flex-start; @@ -475,6 +486,13 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; } } +.bannerText { + color: var(--color-text-secondary); + font-size: 14px; + font-weight: 500; + text-align: center; +} + .bannerTextAndActions { display: flex; flex-direction: column; @@ -489,6 +507,19 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; } } +.bannerActions { + display: flex; + justify-content: space-between; + align-items: center; + gap: 15px; + width: 100%; + margin-top: 16px; + + button { + width: 100%; + } +} + .bannerDisclaimer { a { color: inherit; @@ -519,6 +550,32 @@ $button-fallback-breakpoint: $button-breakpoint + 55px; } } +.bannerActionsDisplayName { + color: var(--color-text-secondary); + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + line-height: 22px; + overflow: hidden; + text-decoration: none; + + &:hover strong { + text-decoration: underline; + } + + strong, + span { + display: block; + text-overflow: ellipsis; + overflow: hidden; + } + + strong { + color: var(--color-text-primary); + } +} + // Buttons .followButton { diff --git a/app/javascript/mastodon/components/account_header/subscription_form.tsx b/app/javascript/mastodon/components/account_header/subscription_form.tsx index f78077bef7..e09e7d4b14 100644 --- a/app/javascript/mastodon/components/account_header/subscription_form.tsx +++ b/app/javascript/mastodon/components/account_header/subscription_form.tsx @@ -104,8 +104,6 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({ .then(() => { setSubmitting(false); setSubmitted(true); - - return ''; }) .catch((err: unknown) => { setSubmitting(false); @@ -133,12 +131,11 @@ export const AccountSubscriptionForm: React.FC<{ accountId: string }> = ({ className={classNames(classes.bannerBase, classes.bannerBaseCentered)} >
-

- -

+ = ({ return (
-

- , - }} - /> -

+ , + }} + />
diff --git a/app/javascript/mastodon/features/account/components/account_note.tsx b/app/javascript/mastodon/features/account/components/account_note.tsx deleted file mode 100644 index ea3a4cdaca..0000000000 --- a/app/javascript/mastodon/features/account/components/account_note.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import type { ChangeEventHandler, KeyboardEventHandler } from 'react'; -import { useState, useRef, useCallback, useId } from 'react'; - -import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; - -import Textarea from 'react-textarea-autosize'; - -import { submitAccountNote } from '@/mastodon/actions/account_notes'; -import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; - -const messages = defineMessages({ - placeholder: { - id: 'account_note.placeholder', - defaultMessage: 'Click to add a note', - }, -}); - -const AccountNoteUI: React.FC<{ - initialValue: string | undefined; - onSubmit: (newNote: string) => void; - wasSaved: boolean; -}> = ({ initialValue, onSubmit, wasSaved }) => { - const intl = useIntl(); - const uniqueId = useId(); - const [value, setValue] = useState(initialValue ?? ''); - const isLoading = initialValue === undefined; - const canSubmitOnBlurRef = useRef(true); - - const handleChange = useCallback>( - (e) => { - setValue(e.target.value); - }, - [], - ); - - const handleKeyDown = useCallback>( - (e) => { - if (e.key === 'Escape') { - e.preventDefault(); - - setValue(initialValue ?? ''); - - canSubmitOnBlurRef.current = false; - e.currentTarget.blur(); - } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - - onSubmit(value); - - canSubmitOnBlurRef.current = false; - e.currentTarget.blur(); - } - }, - [initialValue, onSubmit, value], - ); - - const handleBlur = useCallback(() => { - if (initialValue !== value && canSubmitOnBlurRef.current) { - onSubmit(value); - } - canSubmitOnBlurRef.current = true; - }, [initialValue, onSubmit, value]); - - return ( -
- - {isLoading ? ( -
- -
- ) : ( -