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 diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index b8ff18fc0f..90c9dc8937 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -167,7 +167,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6 with: files: coverage/lcov/*.lcov env: diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000000..79b376c9ae --- /dev/null +++ b/.simplecov @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +SimpleCov.start 'rails' do + # During parallel runs, ensure unique names for post-run merge + command_name "job-#{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER'] + + if ENV['CI'] + require 'simplecov-lcov' + formatter SimpleCov::Formatter::LcovFormatter + formatter.config.report_with_single_file = true + else + formatter SimpleCov::Formatter::HTMLFormatter + end + + enable_coverage :branch + + add_filter 'lib/linter' + + add_group 'Libraries', 'lib' + add_group 'Policies', 'app/policies' + add_group 'Presenters', 'app/presenters' + add_group 'Search', 'app/chewy' + add_group 'Serializers', 'app/serializers' + add_group 'Services', 'app/services' + add_group 'Validators', 'app/validators' +end 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/javascript/flavours/glitch/components/account_header/banners.tsx b/app/javascript/flavours/glitch/components/account_header/banners.tsx new file mode 100644 index 0000000000..13e51b096e --- /dev/null +++ b/app/javascript/flavours/glitch/components/account_header/banners.tsx @@ -0,0 +1,145 @@ +import { useCallback } from 'react'; +import type { FC, ReactElement, ReactNode } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { + authorizeFollowRequest, + rejectFollowRequest, +} from '@/flavours/glitch/actions/accounts'; +import { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibility'; +import { useRelationship } from '@/flavours/glitch/hooks/useRelationship'; +import type { Account } from '@/flavours/glitch/models/account'; +import { useAppDispatch, useAppSelector } from '@/flavours/glitch/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 { Permalink } from '../permalink'; + +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/flavours/glitch/components/account_header/index.tsx b/app/javascript/flavours/glitch/components/account_header/index.tsx index 261c1a0114..f73f51855a 100644 --- a/app/javascript/flavours/glitch/components/account_header/index.tsx +++ b/app/javascript/flavours/glitch/components/account_header/index.tsx @@ -5,7 +5,6 @@ import classNames from 'classnames'; import { Helmet } from '@unhead/react/helmet'; import { openModal } from '@/flavours/glitch/actions/modal'; -import FollowRequestNoteContainer from '@/flavours/glitch/features/account/containers/follow_request_note_container'; import { useLayout } from '@/flavours/glitch/hooks/useLayout'; import { useVisibility } from '@/flavours/glitch/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/flavours/glitch/components/account_header/moved_note.tsx b/app/javascript/flavours/glitch/components/account_header/moved_note.tsx deleted file mode 100644 index ffb3e5bfed..0000000000 --- a/app/javascript/flavours/glitch/components/account_header/moved_note.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FormattedMessage } from 'react-intl'; - -import { useAppSelector } from 'flavours/glitch/store'; - -import { AvatarOverlay } from '../avatar_overlay'; -import { DisplayName } from '../display_name'; -import { Permalink } from '../permalink'; - -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/flavours/glitch/components/account_header/styles.module.scss b/app/javascript/flavours/glitch/components/account_header/styles.module.scss index 43c5b5d83b..a54ca18c8a 100644 --- a/app/javascript/flavours/glitch/components/account_header/styles.module.scss +++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/account_header/subscription_form.tsx b/app/javascript/flavours/glitch/components/account_header/subscription_form.tsx index ef1f5a7e85..1f27d8a3d1 100644 --- a/app/javascript/flavours/glitch/components/account_header/subscription_form.tsx +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/account/components/account_note.tsx b/app/javascript/flavours/glitch/features/account/components/account_note.tsx deleted file mode 100644 index 36840b6bad..0000000000 --- a/app/javascript/flavours/glitch/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 '@/flavours/glitch/actions/account_notes'; -import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator'; -import { useAppDispatch, useAppSelector } from '@/flavours/glitch/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 ? ( -
- -
- ) : ( -