From a0401fbbd4e7cd4cb4eaf8674317fdac11ea5f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 25 Dec 2025 20:31:38 +0100 Subject: [PATCH 01/10] =?UTF-8?q?Copilot=20is=20thinking=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot is thinking… --- packages/pl-fe/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-fe/src/main.tsx b/packages/pl-fe/src/main.tsx index f380feae7..709e45090 100644 --- a/packages/pl-fe/src/main.tsx +++ b/packages/pl-fe/src/main.tsx @@ -1,5 +1,5 @@ /// -(window as any).__PL_API_FALLBACK_ACCOUNT = { id: '', acct: 'undefined', url: location.host }; +(window as any).__PL_API_FALLBACK_ACCOUNT = { id: '', acct: 'undefined', url: location.origin }; import './polyfills'; From 664c7e91a7885e59869711ef622330fb5ede80b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 29 Dec 2025 00:29:15 +0100 Subject: [PATCH 02/10] pl-fe: Support block auto-expiration on Pleroma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/reports.ts | 7 +- .../src/components/status-action-bar.tsx | 23 +-- .../features/account/components/header.tsx | 18 +- .../chat-page/components/chat-page-main.tsx | 11 +- .../components/chat-widget/chat-settings.tsx | 11 +- .../event/components/event-header.tsx | 18 +- .../features/ui/components/action-button.tsx | 4 +- .../src/features/ui/components/modal-root.tsx | 2 +- packages/pl-fe/src/locales/en.json | 1 + .../pl-fe/src/modals/block-mute-modal.tsx | 159 ++++++++++++++++++ packages/pl-fe/src/modals/mute-modal.tsx | 122 -------------- .../pl-fe/src/modals/report-modal/index.tsx | 2 +- packages/pl-fe/src/pages/statuses/status.tsx | 2 - .../src/queries/accounts/use-relationship.ts | 4 +- packages/pl-fe/src/stores/modals.ts | 4 +- 15 files changed, 187 insertions(+), 201 deletions(-) create mode 100644 packages/pl-fe/src/modals/block-mute-modal.tsx delete mode 100644 packages/pl-fe/src/modals/mute-modal.tsx diff --git a/packages/pl-fe/src/actions/reports.ts b/packages/pl-fe/src/actions/reports.ts index a6391b729..3b605a65b 100644 --- a/packages/pl-fe/src/actions/reports.ts +++ b/packages/pl-fe/src/actions/reports.ts @@ -12,16 +12,17 @@ enum ReportableEntities { } type ReportedEntity = { - status?: Pick; + status?: Pick; + statusId?: string; } const initReport = (entityType: ReportableEntities, account: Pick, entities?: ReportedEntity) => (dispatch: AppDispatch) => { - const { status } = entities || {}; + const { status, statusId } = entities || {}; return useModalsStore.getState().actions.openModal('REPORT', { accountId: account.id, entityType, - statusIds: status ? [status.id] : [], + statusIds: [status?.id, statusId].filter((id): id is string => !!id), }); }; diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 824c0598d..5cb804f32 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -1,6 +1,6 @@ import { type Account, type CustomEmoji, type Group, GroupRoles } from 'pl-api'; import React, { useCallback, useMemo } from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { useHistory, useRouteMatch } from 'react-router-dom'; import { redactStatus } from 'pl-fe/actions/admin'; @@ -23,7 +23,7 @@ import { useClient } from 'pl-fe/hooks/use-client'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; -import { useBlockAccountMutation, useUnblockAccountMutation } from 'pl-fe/queries/accounts/use-relationship'; +import { useUnblockAccountMutation } from 'pl-fe/queries/accounts/use-relationship'; import { useChats } from 'pl-fe/queries/chats'; import { useBlockGroupUserMutation } from 'pl-fe/queries/groups/use-group-blocks'; import { useCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis'; @@ -50,8 +50,6 @@ const messages = defineMessages({ block: { id: 'account.block', defaultMessage: 'Block @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, bookmarkSetFolder: { id: 'status.bookmark_folder', defaultMessage: 'Set bookmark folder' }, bookmarkChangeFolder: { id: 'status.bookmark_folder_change', defaultMessage: 'Change bookmark folder' }, @@ -578,7 +576,6 @@ const MenuButton: React.FC = ({ const { mutate: unbookmarkStatus } = useUnbookmarkStatus(status.id); const { mutate: pinStatus } = usePinStatus(status?.id!); const { mutate: unpinStatus } = useUnpinStatus(status?.id!); - const { mutate: blockAccount } = useBlockAccountMutation(status.account_id); const { mutate: unblockAccount } = useUnblockAccountMutation(status.account_id); const { groupRelationship } = useGroupRelationship(status.group_id || undefined); @@ -682,23 +679,11 @@ const MenuButton: React.FC = ({ }; const handleMuteClick: React.EventHandler = (e) => { - openModal('MUTE', { accountId: status.account.id }); + openModal('BLOCK_MUTE', { accountId: status.account.id, action: 'MUTE' }); }; const handleBlockClick: React.EventHandler = (e) => { - const account = status.account; - - openModal('CONFIRM', { - heading: , - message: @{account.acct} }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => blockAccount(), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - blockAccount(); - dispatch(initReport(ReportableEntities.STATUS, account, { status })); - }, - }); + openModal('BLOCK_MUTE', { accountId: status.account.id, statusId: status.id, action: 'BLOCK' }); }; const handleUnblockClick: React.EventHandler = (e) => { diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index a5fb7cb6a..e37489fc1 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -29,7 +29,6 @@ import { useClient } from 'pl-fe/hooks/use-client'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; import { - useBlockAccountMutation, useFollowAccountMutation, usePinAccountMutation, useRemoveAccountFromFollowersMutation, @@ -82,9 +81,7 @@ const messages = defineMessages({ search: { id: 'account.search', defaultMessage: 'Search from @{name}' }, searchSelf: { id: 'account.search_self', defaultMessage: 'Search your posts' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' }, removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' }, userEndorsed: { id: 'account.endorse.success', defaultMessage: 'You are now featuring @{acct} on your profile' }, userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' }, @@ -146,7 +143,6 @@ const Header: React.FC = ({ account }) => { const features = useFeatures(); const { account: ownAccount } = useOwnAccount(); const { mutate: followAccount } = useFollowAccountMutation(account?.id!); - const { mutate: blockAccount } = useBlockAccountMutation(account?.id!); const { mutate: unblockAccount } = useUnblockAccountMutation(account?.id!); const { mutate: unmuteAccount } = useUnmuteAccountMutation(account?.id!); const { mutate: pinAccount } = usePinAccountMutation(account?.id!); @@ -201,17 +197,7 @@ const Header: React.FC = ({ account }) => { if (account.relationship?.blocking) { unblockAccount(); } else { - openModal('CONFIRM', { - heading: , - message: @{account.acct} }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => blockAccount(), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - blockAccount(); - dispatch(initReport(ReportableEntities.ACCOUNT, account)); - }, - }); + openModal('BLOCK_MUTE', { accountId: account.id, action: 'BLOCK' }); } }; @@ -276,7 +262,7 @@ const Header: React.FC = ({ account }) => { if (account.relationship?.muting) { unmuteAccount(); } else { - openModal('MUTE', { accountId: account.id }); + openModal('BLOCK_MUTE', { accountId: account.id, action: 'MUTE' }); } }; diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx index ba6b2fe1b..600da4b81 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-main.tsx @@ -11,7 +11,7 @@ import Text from 'pl-fe/components/ui/text'; import VerificationBadge from 'pl-fe/components/verification-badge'; import { useChatContext } from 'pl-fe/contexts/chat-context'; import { useFeatures } from 'pl-fe/hooks/use-features'; -import { useBlockAccountMutation, useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship'; +import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship'; import { useChat, useChatActions, useChats } from 'pl-fe/queries/chats'; import { useModalsActions } from 'pl-fe/stores/modals'; @@ -47,7 +47,6 @@ const ChatPageMain = () => { const { currentChatId } = useChatContext(); const { chatsQuery: { data: chats, isLoading } } = useChats(); - const { mutate: blockAccount } = useBlockAccountMutation(chat?.account.id!); const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!); const inputRef = useRef(null); @@ -57,11 +56,9 @@ const ChatPageMain = () => { const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by; const handleBlockUser = () => { - openModal('CONFIRM', { - heading: intl.formatMessage(messages.blockHeading, { acct: chat?.account.acct }), - message: intl.formatMessage(messages.blockMessage), - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => blockAccount(), + openModal('BLOCK_MUTE', { + accountId: chat!.account.id, + action: 'BLOCK', }); }; diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx index fbe3080ad..6fa6215e1 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-settings.tsx @@ -8,7 +8,7 @@ import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context'; import { useFeatures } from 'pl-fe/hooks/use-features'; -import { useBlockAccountMutation, useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship'; +import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship'; import { useChatActions } from 'pl-fe/queries/chats'; import { useModalsActions } from 'pl-fe/stores/modals'; @@ -38,7 +38,6 @@ const ChatSettings = () => { const { chat, changeScreen, toggleChatPane } = useChatContext(); const { deleteChat } = useChatActions(chat?.id as string); - const { mutate: blockAccount } = useBlockAccountMutation(chat?.account.id!); const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!); const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by; @@ -53,11 +52,9 @@ const ChatSettings = () => { }; const handleBlockUser = () => { - openModal('CONFIRM', { - heading: intl.formatMessage(messages.blockHeading, { acct: chat?.account.acct }), - message: intl.formatMessage(messages.blockMessage), - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => blockAccount(), + openModal('BLOCK_MUTE', { + accountId: chat?.account.id!, + action: 'BLOCK', }); }; diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index 03627ac49..25e7b1dd5 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -20,7 +20,6 @@ import Emojify from 'pl-fe/features/emoji/emojify'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; -import { useBlockAccountMutation } from 'pl-fe/queries/accounts/use-relationship'; import { useChats } from 'pl-fe/queries/chats'; import { useBookmarkStatus, usePinStatus, useReblogStatus, useUnbookmarkStatus, useUnpinStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { useModalsActions } from 'pl-fe/stores/modals'; @@ -64,8 +63,6 @@ const messages = defineMessages({ markStatusSensitive: { id: 'admin.statuses.actions.mark_status_sensitive', defaultMessage: 'Mark post sensitive' }, markStatusNotSensitive: { id: 'admin.statuses.actions.mark_status_not_sensitive', defaultMessage: 'Mark post not sensitive' }, deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' }, deleteConfirm: { id: 'confirmations.delete_event.confirm', defaultMessage: 'Delete' }, deleteHeading: { id: 'confirmations.delete_event.heading', defaultMessage: 'Delete event' }, deleteMessage: { id: 'confirmations.delete_event.message', defaultMessage: 'Are you sure you want to delete this event?' }, @@ -95,7 +92,6 @@ const EventHeader: React.FC = ({ status }) => { const { mutate: unbookmarkStatus } = useUnbookmarkStatus(status?.id!); const { mutate: pinStatus } = usePinStatus(status?.id!); const { mutate: unpinStatus } = useUnpinStatus(status?.id!); - const { mutate: blockAccount } = useBlockAccountMutation(status?.account.id!); if (!status || !status.event) { return ( @@ -184,21 +180,11 @@ const EventHeader: React.FC = ({ status }) => { }; const handleMuteClick = () => { - openModal('MUTE', { accountId: account.id }); + openModal('BLOCK_MUTE', { accountId: account.id, action: 'MUTE' }); }; const handleBlockClick = () => { - openModal('CONFIRM', { - heading: , - message: @{account.acct} }} />, - confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => blockAccount(), - secondary: intl.formatMessage(messages.blockAndReport), - onSecondary: () => { - blockAccount(); - dispatch(initReport(ReportableEntities.STATUS, account, { status })); - }, - }); + openModal('BLOCK_MUTE', { accountId: account.id, action: 'BLOCK' }); }; const handleReport = () => { diff --git a/packages/pl-fe/src/features/ui/components/action-button.tsx b/packages/pl-fe/src/features/ui/components/action-button.tsx index 813a68840..18a6e7810 100644 --- a/packages/pl-fe/src/features/ui/components/action-button.tsx +++ b/packages/pl-fe/src/features/ui/components/action-button.tsx @@ -10,7 +10,6 @@ import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; import { useAcceptFollowRequestMutation, useRejectFollowRequestMutation } from 'pl-fe/queries/accounts/use-follow-requests'; import { useRelationshipQuery, - useBlockAccountMutation, useUnblockAccountMutation, useMuteAccountMutation, useUnmuteAccountMutation, @@ -65,7 +64,6 @@ const ActionButton: React.FC = ({ account, actionType, small = tr const { mutate: followAccount, isPending: isPendingFollow } = useFollowAccountMutation(account.id); const { mutate: unfollowAccount, isPending: isPendingUnfollow } = useUnfollowAccountMutation(account.id); - const { mutate: blockAccount } = useBlockAccountMutation(account.id); const { mutate: unblockAccount } = useUnblockAccountMutation(account.id); const { mutate: muteAccount } = useMuteAccountMutation(account.id); const { mutate: unmuteAccount } = useUnmuteAccountMutation(account.id); @@ -87,7 +85,7 @@ const ActionButton: React.FC = ({ account, actionType, small = tr if (relationship?.blocking) { unblockAccount(); } else { - blockAccount(); + openModal('BLOCK_MUTE', { accountId: account.id, action: 'BLOCK' }); } }; diff --git a/packages/pl-fe/src/features/ui/components/modal-root.tsx b/packages/pl-fe/src/features/ui/components/modal-root.tsx index 08d1a60f3..a58049b40 100644 --- a/packages/pl-fe/src/features/ui/components/modal-root.tsx +++ b/packages/pl-fe/src/features/ui/components/modal-root.tsx @@ -11,6 +11,7 @@ import ModalLoading from './modal-loading'; const MODAL_COMPONENTS = { ALT_TEXT: lazy(() => import('pl-fe/modals/alt-text-modal')), BIRTHDAYS: lazy(() => import('pl-fe/modals/birthdays-modal')), + BLOCK_MUTE: lazy(() => import('pl-fe/modals/block-mute-modal')), BOOST: lazy(() => import('pl-fe/modals/boost-modal')), CIRCLE_EDITOR: lazy(() => import('pl-fe/modals/circle-editor-modal')), COMPARE_HISTORY: lazy(() => import('pl-fe/modals/compare-history-modal')), @@ -39,7 +40,6 @@ const MODAL_COMPONENTS = { MEDIA: lazy(() => import('pl-fe/modals/media-modal')), MENTIONS: lazy(() => import('pl-fe/modals/mentions-modal')), MISSING_DESCRIPTION: lazy(() => import('pl-fe/modals/missing-description-modal')), - MUTE: lazy(() => import('pl-fe/modals/mute-modal')), REACTIONS: lazy(() => import('pl-fe/modals/reactions-modal')), REBLOGS: lazy(() => import('pl-fe/modals/reblogs-modal')), REPLY_MENTIONS: lazy(() => import('pl-fe/modals/reply-mentions-modal')), diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index d5ab4a7a8..e4c9f8bfb 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -283,6 +283,7 @@ "badge_input.placeholder": "Enter a badge…", "birthday_panel.title": "Birthdays", "birthdays_modal.empty": "None of your friends have birthday today.", + "block_modal.auto_expire": "Automatically expire block?", "bookmark_folders.add.fail": "Failed to create bookmark folder", "bookmark_folders.add.success": "Bookmark folder created successfully", "bookmark_folders.all_bookmarks": "All bookmarks", diff --git a/packages/pl-fe/src/modals/block-mute-modal.tsx b/packages/pl-fe/src/modals/block-mute-modal.tsx new file mode 100644 index 000000000..c90f6c4b4 --- /dev/null +++ b/packages/pl-fe/src/modals/block-mute-modal.tsx @@ -0,0 +1,159 @@ +import React, { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; +import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; +import HStack from 'pl-fe/components/ui/hstack'; +import Modal from 'pl-fe/components/ui/modal'; +import Stack from 'pl-fe/components/ui/stack'; +import Text from 'pl-fe/components/ui/text'; +import Toggle from 'pl-fe/components/ui/toggle'; +import DurationSelector from 'pl-fe/features/compose/components/polls/duration-selector'; +import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useFeatures } from 'pl-fe/hooks/use-features'; +import { useBlockAccountMutation, useMuteAccountMutation } from 'pl-fe/queries/accounts/use-relationship'; + +import type { BlockAccountParams, MuteAccountParams } from 'pl-api'; +import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root'; + +interface BlockMuteModalProps { + action: 'BLOCK' | 'MUTE'; + accountId: string; + statusId?: string; +} + +const BlockMuteModal: React.FC = ({ accountId, statusId, onClose, action }) => { + const dispatch = useAppDispatch(); + + const { account } = useAccount(accountId || undefined); + const [notifications, setNotifications] = useState(true); + const [duration, setDuration] = useState(0); + const [isSubmitting, setIsSubmitting] = useState(false); + const { blocksDuration, mutesDuration } = useFeatures(); + const canSetDuration = action === 'MUTE' ? mutesDuration : blocksDuration; + + const { mutate: muteAccount } = useMuteAccountMutation(accountId); + const { mutate: blockAccount } = useBlockAccountMutation(accountId); + + if (!account) return null; + + const handleClick = () => { + setIsSubmitting(true); + const params: MuteAccountParams | BlockAccountParams = { duration: duration || undefined }; + if (action === 'MUTE') { + (params as MuteAccountParams).notifications = notifications; + } + (action === 'MUTE' ? muteAccount : blockAccount)(params, { + onSuccess: () => { + setIsSubmitting(false); + onClose('BLOCK_MUTE'); + }, + }); + }; + + const handleBlockAndReport = () => { + handleClick(); + dispatch(initReport(ReportableEntities.STATUS, account, { statusId })); + }; + + const handleCancel = () => { + onClose('BLOCK_MUTE'); + }; + + const toggleNotifications = () => { + setNotifications(notifications => !notifications); + }; + + const handleChangeMuteDuration = (expiresIn: number): void => { + setDuration(expiresIn); + }; + + const toggleAutoExpire = () => setDuration(duration ? 0 : 2 * 60 * 60 * 24); + + return ( + + ) : ( + + )} + onClose={handleCancel} + confirmationAction={handleClick} + confirmationText={action === 'MUTE' ? ( + + ) : ( + + )} + confirmationDisabled={isSubmitting} + secondaryAction={action === 'BLOCK' ? handleBlockAndReport : undefined} + secondaryText={} + secondaryDisabled={isSubmitting} + cancelText={} + cancelAction={handleCancel} + > + + + {action === 'MUTE' ? ( + @{account.acct} }} + /> + ) : ( + @{account.acct} }} + /> + )} + + + {action === 'MUTE' && ( + + )} + + {canSetDuration && ( + <> + + + {duration !== 0 && ( + + : + + + + )} + + )} + + + ); +}; + +export { BlockMuteModal as default, type BlockMuteModalProps }; diff --git a/packages/pl-fe/src/modals/mute-modal.tsx b/packages/pl-fe/src/modals/mute-modal.tsx deleted file mode 100644 index 7729cff7b..000000000 --- a/packages/pl-fe/src/modals/mute-modal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; - -import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; -import HStack from 'pl-fe/components/ui/hstack'; -import Modal from 'pl-fe/components/ui/modal'; -import Stack from 'pl-fe/components/ui/stack'; -import Text from 'pl-fe/components/ui/text'; -import Toggle from 'pl-fe/components/ui/toggle'; -import DurationSelector from 'pl-fe/features/compose/components/polls/duration-selector'; -import { useFeatures } from 'pl-fe/hooks/use-features'; -import { useMuteAccountMutation } from 'pl-fe/queries/accounts/use-relationship'; - -import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root'; - -interface MuteModalProps { - accountId: string; -} - -const MuteModal: React.FC = ({ accountId, onClose }) => { - const { account } = useAccount(accountId || undefined); - const [notifications, setNotifications] = useState(true); - const [duration, setDuration] = useState(0); - const [isSubmitting, setIsSubmitting] = useState(false); - const mutesDuration = useFeatures().mutesDuration; - - const { mutate: muteAccount } = useMuteAccountMutation(accountId); - - if (!account) return null; - - const handleClick = () => { - setIsSubmitting(true); - muteAccount({ notifications, duration }, { - onSuccess: () => { - setIsSubmitting(false); - onClose('MUTE'); - }, - }); - }; - - const handleCancel = () => { - onClose('MUTE'); - }; - - const toggleNotifications = () => { - setNotifications(notifications => !notifications); - }; - - const handleChangeMuteDuration = (expiresIn: number): void => { - setDuration(expiresIn); - }; - - const toggleAutoExpire = () => setDuration(duration ? 0 : 2 * 60 * 60 * 24); - - return ( - - } - onClose={handleCancel} - confirmationAction={handleClick} - confirmationText={} - confirmationDisabled={isSubmitting} - cancelText={} - cancelAction={handleCancel} - > - - - @{account.acct} }} - /> - - - - - {mutesDuration && ( - <> - - - {duration !== 0 && ( - - : - - - - )} - - )} - - - ); -}; - -export { MuteModal as default, type MuteModalProps }; diff --git a/packages/pl-fe/src/modals/report-modal/index.tsx b/packages/pl-fe/src/modals/report-modal/index.tsx index b68d34706..4f02a5118 100644 --- a/packages/pl-fe/src/modals/report-modal/index.tsx +++ b/packages/pl-fe/src/modals/report-modal/index.tsx @@ -111,7 +111,7 @@ const ReportModal: React.FC = ({ onClose, acc }); if (block && account) { - blockAccount(); + blockAccount(undefined); } }; diff --git a/packages/pl-fe/src/pages/statuses/status.tsx b/packages/pl-fe/src/pages/statuses/status.tsx index 5cfd5290e..5caed3964 100644 --- a/packages/pl-fe/src/pages/statuses/status.tsx +++ b/packages/pl-fe/src/pages/statuses/status.tsx @@ -25,13 +25,11 @@ const messages = defineMessages({ redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' }, - blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, - blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' }, treeView: { id: 'status.thread.tree_view', defaultMessage: 'Tree view' }, linearView: { id: 'status.thread.linear_view', defaultMessage: 'Linear view' }, expandAll: { id: 'status.thread.expand_all', defaultMessage: 'Expand all posts' }, diff --git a/packages/pl-fe/src/queries/accounts/use-relationship.ts b/packages/pl-fe/src/queries/accounts/use-relationship.ts index cabb3c8e3..12c9d581a 100644 --- a/packages/pl-fe/src/queries/accounts/use-relationship.ts +++ b/packages/pl-fe/src/queries/accounts/use-relationship.ts @@ -7,7 +7,7 @@ import { useClient } from 'pl-fe/hooks/use-client'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; import type { MinifiedSuggestion } from '../trends/use-suggested-accounts'; -import type { FollowAccountParams, MuteAccountParams, Relationship } from 'pl-api'; +import type { BlockAccountParams, FollowAccountParams, MuteAccountParams, Relationship } from 'pl-api'; const updateRelationship = (accountId: string, changes: Partial | ((relationship: Relationship) => Relationship), queryClient: ReturnType) => { const previousRelationship = queryClient.getQueryData(['accountRelationships', accountId]); @@ -89,7 +89,7 @@ const useBlockAccountMutation = (accountId: string) => { return useMutation({ mutationKey: ['accountRelationships', accountId], - mutationFn: () => client.filtering.blockAccount(accountId), + mutationFn: (params?: BlockAccountParams) => client.filtering.blockAccount(accountId, params), onMutate: () => updateRelationship(accountId, { blocking: true, followed_by: false, diff --git a/packages/pl-fe/src/stores/modals.ts b/packages/pl-fe/src/stores/modals.ts index b9fd2834a..fd5d51b16 100644 --- a/packages/pl-fe/src/stores/modals.ts +++ b/packages/pl-fe/src/stores/modals.ts @@ -4,6 +4,7 @@ import { mutative } from 'zustand-mutative'; import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; import type { AltTextModalProps } from 'pl-fe/modals/alt-text-modal'; +import type { BlockMuteModalProps } from 'pl-fe/modals/block-mute-modal'; import type { BoostModalProps } from 'pl-fe/modals/boost-modal'; import type { CircleEditorModalProps } from 'pl-fe/modals/circle-editor-modal'; import type { CompareHistoryModalProps } from 'pl-fe/modals/compare-history-modal'; @@ -29,7 +30,6 @@ import type { ListEditorModalProps } from 'pl-fe/modals/list-editor-modal'; import type { MediaModalProps } from 'pl-fe/modals/media-modal'; import type { MentionsModalProps } from 'pl-fe/modals/mentions-modal'; import type { MissingDescriptionModalProps } from 'pl-fe/modals/missing-description-modal'; -import type { MuteModalProps } from 'pl-fe/modals/mute-modal'; import type { ReactionsModalProps } from 'pl-fe/modals/reactions-modal'; import type { ReblogsModalProps } from 'pl-fe/modals/reblogs-modal'; import type { ReplyMentionsModalProps } from 'pl-fe/modals/reply-mentions-modal'; @@ -68,7 +68,7 @@ type OpenModalProps = | [type: 'MEDIA', props: MediaModalProps] | [type: 'MENTIONS', props: MentionsModalProps] | [type: 'MISSING_DESCRIPTION', props: MissingDescriptionModalProps] - | [type: 'MUTE', props: MuteModalProps] + | [type: 'BLOCK_MUTE', props: BlockMuteModalProps] | [type: 'REACTIONS', props: ReactionsModalProps] | [type: 'REBLOGS', props: ReblogsModalProps] | [type: 'REPLY_MENTIONS', props: ReplyMentionsModalProps] From 11e3a5417d0af400f1fb65c5d92fd378f2a9abbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 29 Dec 2025 00:33:17 +0100 Subject: [PATCH 03/10] pl-fe: Display block expiration date in blocks list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/client.ts | 3 ++- packages/pl-api/lib/entities/account.ts | 17 +++++++++++++++++ packages/pl-fe/src/components/account.tsx | 10 ++++++++++ packages/pl-fe/src/pages/settings/blocks.tsx | 4 ++-- .../src/queries/account-lists/use-blocks.ts | 4 ++-- packages/pl-fe/src/queries/utils/minify-list.ts | 9 +++++++-- 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 1f09eec75..47b40adbf 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -28,6 +28,7 @@ import { asyncRefreshSchema, authorizationServerMetadataSchema, backupSchema, + blockedAccountSchema, bookmarkFolderSchema, chatMessageSchema, chatSchema, @@ -2126,7 +2127,7 @@ class PlApiClient { * @see {@link https://docs.joinmastodon.org/methods/blocks/#get} */ getBlocks: async (params?: GetBlocksParams) => - this.#paginatedGet('/api/v1/blocks', { params }, accountSchema), + this.#paginatedGet('/api/v1/blocks', { params }, blockedAccountSchema), /** * Get domain blocks diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 3680c77dc..3be5bc522 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -265,6 +265,21 @@ type CredentialAccount = v.InferOutput & */ const credentialAccountSchema: v.BaseSchema> = untypedCredentialAccountSchema as any; +const untypedBlockedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ + ...accountWithMovedAccountSchema.entries, + block_expires_at: v.fallback(v.nullable(datetimeSchema), null), +})); + +/** + * @category Entity types + */ +type BlockedAccount = v.InferOutput & WithMoved; + +/** + * @category Schemas + */ +const blockedAccountSchema: v.BaseSchema> = untypedBlockedAccountSchema as any; + const untypedMutedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({ ...accountWithMovedAccountSchema.entries, mute_expires_at: v.fallback(v.nullable(datetimeSchema), null), @@ -283,8 +298,10 @@ const mutedAccountSchema: v.BaseSchema> export { accountSchema, credentialAccountSchema, + blockedAccountSchema, mutedAccountSchema, type Account, type CredentialAccount, + type BlockedAccount, type MutedAccount, }; diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 767f6e749..542e4d43f 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -110,6 +110,7 @@ interface IAccount { items?: React.ReactNode; disabled?: boolean; muteExpiresAt?: string | null; + blockExpiresAt?: string | null; } const Account = ({ @@ -138,6 +139,7 @@ const Account = ({ items, disabled, muteExpiresAt, + blockExpiresAt, }: IAccount) => { const overflowRef = useRef(null); const actionRef = useRef(null); @@ -376,6 +378,14 @@ const Account = ({ )} + {actionType === 'blocking' && blockExpiresAt ? ( + <> + + + + + ) : null} + {actionType === 'muting' && muteExpiresAt ? ( <> diff --git a/packages/pl-fe/src/pages/settings/blocks.tsx b/packages/pl-fe/src/pages/settings/blocks.tsx index 0b300ca6b..06f24dd79 100644 --- a/packages/pl-fe/src/pages/settings/blocks.tsx +++ b/packages/pl-fe/src/pages/settings/blocks.tsx @@ -43,8 +43,8 @@ const BlocksPage: React.FC = () => { itemClassName={clsx('pb-4', { 'last:pb-0': !hasNextPage })} isLoading={isFetching} > - {data.map((accountId) => ( - + {data.map(([accountId, blockExpiresAt]) => ( + ))} diff --git a/packages/pl-fe/src/queries/account-lists/use-blocks.ts b/packages/pl-fe/src/queries/account-lists/use-blocks.ts index 59387ade9..123e61009 100644 --- a/packages/pl-fe/src/queries/account-lists/use-blocks.ts +++ b/packages/pl-fe/src/queries/account-lists/use-blocks.ts @@ -1,9 +1,9 @@ import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query'; -import { minifyAccountList, minifyMutedAccountList } from '../utils/minify-list'; +import { minifyBlockedAccountList, minifyMutedAccountList } from '../utils/minify-list'; const useBlocks = makePaginatedResponseQuery( ['accountsLists', 'blocked'], - (client) => client.filtering.getBlocks({ with_relationships: true }).then(minifyAccountList), + (client) => client.filtering.getBlocks({ with_relationships: true }).then(minifyBlockedAccountList), ); const useMutes = makePaginatedResponseQuery( diff --git a/packages/pl-fe/src/queries/utils/minify-list.ts b/packages/pl-fe/src/queries/utils/minify-list.ts index dc3a075c4..a30d76630 100644 --- a/packages/pl-fe/src/queries/utils/minify-list.ts +++ b/packages/pl-fe/src/queries/utils/minify-list.ts @@ -3,7 +3,7 @@ import { store } from 'pl-fe/store'; import { queryClient } from '../client'; -import type { Account, AdminAccount, AdminReport, MutedAccount, PaginatedResponse, Status } from 'pl-api'; +import type { Account, AdminAccount, AdminReport, BlockedAccount, MutedAccount, PaginatedResponse, Status } from 'pl-api'; const minifyList = ({ previous, next, items, ...response }: PaginatedResponse, minifier: (value: T1) => T2, importer?: (items: Array) => void): PaginatedResponse => { importer?.(items); @@ -26,6 +26,11 @@ const minifyAccountList = (response: PaginatedResponse): PaginatedRespo store.dispatch(importEntities({ accounts }) as any); }); +const minifyBlockedAccountList = (response: PaginatedResponse): PaginatedResponse<[string, string | null]> => + minifyList(response, (account) => [account.id, account.block_expires_at], (accounts) => { + store.dispatch(importEntities({ accounts }) as any); + }); + const minifyMutedAccountList = (response: PaginatedResponse): PaginatedResponse<[string, string | null]> => minifyList(response, (account) => [account.id, account.mute_expires_at], (accounts) => { store.dispatch(importEntities({ accounts }) as any); @@ -75,4 +80,4 @@ const minifyAdminReportList = (response: PaginatedResponse) => } }); -export { minifyList, minifyAccountList, minifyMutedAccountList, minifyStatusList, minifyAdminAccount, minifyAdminAccountList, minifyAdminReport, minifyAdminReportList }; +export { minifyList, minifyAccountList, minifyBlockedAccountList, minifyMutedAccountList, minifyStatusList, minifyAdminAccount, minifyAdminAccountList, minifyAdminReport, minifyAdminReportList }; From 1829645ee05805c357575553d8ddd673bdf9f168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 29 Dec 2025 00:52:13 +0100 Subject: [PATCH 04/10] pl-fe: Ask for updating account note when blocking/muting accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/locales/en.json | 4 ++ .../pl-fe/src/modals/block-mute-modal.tsx | 59 +++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index e4c9f8bfb..57410cf6a 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -284,6 +284,7 @@ "birthday_panel.title": "Birthdays", "birthdays_modal.empty": "None of your friends have birthday today.", "block_modal.auto_expire": "Automatically expire block?", + "block_modal.note.hint": "You can leave an optional note to remember why you blocked this account. This note is only visible to you.", "bookmark_folders.add.fail": "Failed to create bookmark folder", "bookmark_folders.add.success": "Bookmark folder created successfully", "bookmark_folders.all_bookmarks": "All bookmarks", @@ -1273,6 +1274,9 @@ "mute_modal.auto_expire": "Automatically expire mute?", "mute_modal.duration": "Duration", "mute_modal.hide_notifications": "Hide notifications from this user?", + "mute_modal.note.hint": "You can leave an optional note to remember why you muted this account. This note is only visible to you.", + "mute_modal.note.label.add": "Add account note", + "mute_modal.note.label.edit": "Edit account note", "my_groups_panel.title": "My groups", "navigation.chats": "Chats", "navigation.compose": "Compose", diff --git a/packages/pl-fe/src/modals/block-mute-modal.tsx b/packages/pl-fe/src/modals/block-mute-modal.tsx index c90f6c4b4..329e845ee 100644 --- a/packages/pl-fe/src/modals/block-mute-modal.tsx +++ b/packages/pl-fe/src/modals/block-mute-modal.tsx @@ -1,21 +1,29 @@ import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; +import FormGroup from 'pl-fe/components/ui/form-group'; import HStack from 'pl-fe/components/ui/hstack'; import Modal from 'pl-fe/components/ui/modal'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; +import Textarea from 'pl-fe/components/ui/textarea'; import Toggle from 'pl-fe/components/ui/toggle'; import DurationSelector from 'pl-fe/features/compose/components/polls/duration-selector'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useFeatures } from 'pl-fe/hooks/use-features'; -import { useBlockAccountMutation, useMuteAccountMutation } from 'pl-fe/queries/accounts/use-relationship'; +import { useBlockAccountMutation, useMuteAccountMutation, useUpdateAccountNoteMutation } from 'pl-fe/queries/accounts/use-relationship'; +import toast from 'pl-fe/toast'; import type { BlockAccountParams, MuteAccountParams } from 'pl-api'; import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root'; +const messages = defineMessages({ + notePlaceholder: { id: 'account_note.placeholder', defaultMessage: 'Add a note' }, + noteSaveFailed: { id: 'account_note.fail', defaultMessage: 'Failed to save note' }, +}); + interface BlockMuteModalProps { action: 'BLOCK' | 'MUTE'; accountId: string; @@ -24,16 +32,21 @@ interface BlockMuteModalProps { const BlockMuteModal: React.FC = ({ accountId, statusId, onClose, action }) => { const dispatch = useAppDispatch(); + const intl = useIntl(); - const { account } = useAccount(accountId || undefined); + const { account } = useAccount(accountId || undefined, { withRelationship: true }); const [notifications, setNotifications] = useState(true); const [duration, setDuration] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); - const { blocksDuration, mutesDuration } = useFeatures(); + const [note, setNote] = useState(undefined); + const { notes, blocksDuration, mutesDuration } = useFeatures(); const canSetDuration = action === 'MUTE' ? mutesDuration : blocksDuration; + const currentNote = account?.relationship?.note; + const { mutate: muteAccount } = useMuteAccountMutation(accountId); const { mutate: blockAccount } = useBlockAccountMutation(accountId); + const { mutate: updateAccountNote } = useUpdateAccountNoteMutation(accountId); if (!account) return null; @@ -49,6 +62,11 @@ const BlockMuteModal: React.FC = ({ accoun onClose('BLOCK_MUTE'); }, }); + if (notes && note !== undefined && note !== currentNote) { + updateAccountNote(note, { + onError: () => toast.error(messages.noteSaveFailed), + }); + } }; const handleBlockAndReport = () => { @@ -123,6 +141,39 @@ const BlockMuteModal: React.FC = ({ accoun )} + {notes && ( + + ) : ( + + ) + )} + hintText={ + action === 'MUTE' ? ( + + ) : ( + + ) + } + > +