From 9a74a3cbebe77497d9780ea28ed4458363922c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Tue, 8 Apr 2025 14:51:34 +0200 Subject: [PATCH] pl-fe: allow specifying boost visibility 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 | 2 ++ packages/pl-fe/src/actions/interactions.ts | 8 ++--- .../src/components/status-action-bar.tsx | 34 +++++++++++++++++-- packages/pl-fe/src/components/status.tsx | 26 +++++++++----- .../notifications/components/notification.tsx | 2 +- .../src/features/status/components/thread.tsx | 2 +- .../ui/components/modals/boost-modal.tsx | 14 +++++--- packages/pl-fe/src/locales/en.json | 7 ++++ 8 files changed, 73 insertions(+), 22 deletions(-) diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index a56df96a1..e47c18dc9 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -2151,6 +2151,8 @@ class PlApiClient { * Boost a status * Reshare a status on your own profile. * @see {@link https://docs.joinmastodon.org/methods/statuses/#reblog} + * + * Specifying reblog visibility requires features{@link Features['reblogVisibility']}. */ reblogStatus: async (statusId: string, visibility?: string) => { const response = await this.request(`/api/v1/statuses/${statusId}/reblog`, { method: 'POST', body: { visibility } }); diff --git a/packages/pl-fe/src/actions/interactions.ts b/packages/pl-fe/src/actions/interactions.ts index 7d2cf8b34..66a3d22cd 100644 --- a/packages/pl-fe/src/actions/interactions.ts +++ b/packages/pl-fe/src/actions/interactions.ts @@ -47,13 +47,13 @@ const messages = defineMessages({ selectFolder: { id: 'status.bookmark.select_folder', defaultMessage: 'Select folder' }, }); -const reblog = (status: Pick) => +const reblog = (status: Pick, visibility?: string) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return noOp(); dispatch(reblogRequest(status.id)); - return getClient(getState()).statuses.reblogStatus(status.id).then((response) => { + return getClient(getState()).statuses.reblogStatus(status.id, visibility).then((response) => { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper if (response.reblog) dispatch(importEntities({ statuses: [response.reblog] })); @@ -73,11 +73,11 @@ const unreblog = (status: Pick) => }); }; -const toggleReblog = (status: Pick) => { +const toggleReblog = (status: Pick, visibility?: string) => { if (status.reblogged) { return unreblog(status); } else { - return reblog(status); + return reblog(status, visibility); } }; diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 030a1d303..b98132214 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -94,6 +94,10 @@ const messages = defineMessages({ quotePost: { id: 'status.quote', defaultMessage: 'Quote post' }, reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, + reblog_visibility: { id: 'status.reblog_visibility', defaultMessage: 'Repost to specific audience' }, + reblog_visibility_public: { id: 'status.reblog_visibility_public', defaultMessage: 'Public repost' }, + reblog_visibility_unlisted: { id: 'status.reblog_visibility_unlisted', defaultMessage: 'Unlisted repost' }, + reblog_visibility_private: { id: 'status.reblog_visibility_private', defaultMessage: 'Followers-only repost' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' }, @@ -658,12 +662,12 @@ const MenuButton: React.FC = ({ dispatch(togglePin(status)); }; - const handleReblogClick: React.EventHandler = (e) => { - const modalReblog = () => dispatch(toggleReblog(status)); + const handleReblogClick = (e: React.MouseEvent | React.KeyboardEvent, visibility?: string) => { + const modalReblog = () => dispatch(toggleReblog(status, visibility)); if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { - openModal('BOOST', { statusId: status.id, onReblog: modalReblog }); + openModal('BOOST', { statusId: status.id, onReblog: modalReblog, visibility }); } }; @@ -861,6 +865,30 @@ const MenuButton: React.FC = ({ menu.push(null); + if (publicStatus && !status.reblogged && features.reblogVisibility) { + menu.push({ + text: intl.formatMessage(messages.reblog_visibility), + icon: require('@tabler/icons/outline/repeat.svg'), + items: [ + { + text: intl.formatMessage(messages.reblog_visibility_public), + action: (e) => handleReblogClick(e, 'public'), + icon: require('@tabler/icons/outline/world.svg'), + }, + { + text: intl.formatMessage(messages.reblog_visibility_unlisted), + action: (e) => handleReblogClick(e, 'unlisted'), + icon: require('@tabler/icons/outline/lock-open.svg'), + }, + { + text: intl.formatMessage(messages.reblog_visibility_private), + action: (e) => handleReblogClick(e, 'private'), + icon: require('@tabler/icons/outline/lock.svg'), + }, + ], + }); + } + if (ownAccount) { if (publicStatus) { menu.push({ diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index 8741ca1be..a06e96c82 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -245,19 +245,29 @@ const Status: React.FC = (props) => { ); } + const values = { + name: , + count: accounts.length, + }; + return ( } text={ - , - count: accounts.length, - }} - /> + status.visibility === 'private' ? ( + + ) : ( + + ) } /> ); diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index b8d557b78..be3e36243 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -270,7 +270,7 @@ const Notification: React.FC = (props) => { } else { openModal('BOOST', { statusId: status.id, - onReblog: (status) => { + onReblog: () => { dispatch(reblog(status)); }, }); diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index 48e0fd886..b135cc15d 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -150,7 +150,7 @@ const Thread: React.FC = ({ if ((e && e.shiftKey) || !boostModal) { handleModalReblog(status); } else { - openModal('BOOST', { statusId: status.id, onReblog: handleModalReblog }); + openModal('BOOST', { statusId: status.id, onReblog: () => handleModalReblog(status) }); } } }; diff --git a/packages/pl-fe/src/features/ui/components/modals/boost-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/boost-modal.tsx index fac3b1982..ab085a201 100644 --- a/packages/pl-fe/src/features/ui/components/modals/boost-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/boost-modal.tsx @@ -10,7 +10,6 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { makeGetStatus } from 'pl-fe/selectors'; import type { BaseModalProps } from '../modal-root'; -import type { Status as StatusEntity } from 'pl-fe/normalizers/status'; const messages = defineMessages({ cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, @@ -19,17 +18,18 @@ const messages = defineMessages({ interface BoostModalProps { statusId: string; - onReblog: (status: Pick) => void; + onReblog: () => void; + visibility?: string; } -const BoostModal: React.FC = ({ statusId, onReblog, onClose }) => { +const BoostModal: React.FC = ({ statusId, onReblog, visibility, onClose }) => { const getStatus = useCallback(makeGetStatus(), []); const intl = useIntl(); const status = useAppSelector(state => getStatus(state, { id: statusId }))!; const handleReblog = () => { - onReblog(status); + onReblog(); onClose('BOOST'); }; @@ -37,7 +37,11 @@ const BoostModal: React.FC = ({ statusId, onRe return ( } + title={visibility === 'unlisted' + ? + : visibility === 'private' + ? + : } confirmationAction={handleReblog} confirmationText={intl.formatMessage(buttonText)} > diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index ff0e37c29..d56a13f52 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -256,6 +256,8 @@ "bookmarks.edit_folder": "Edit folder", "boost_modal.combo": "You can press {combo} to skip this next time", "boost_modal.title": "Repost?", + "boost_modal.title.private": "Repost privately?", + "boost_modal.title.unlisted": "Repost unlisted?", "bundle_column_error.body": "Something went wrong while loading this page.", "bundle_column_error.retry": "Try again", "bundle_column_error.title": "Network error", @@ -1539,7 +1541,12 @@ "status.read_more": "Read more", "status.reblog": "Repost", "status.reblog_private": "Repost to original audience", + "status.reblog_visibility": "Repost to specific audience", + "status.reblog_visibility_private": "Followers-only repost", + "status.reblog_visibility_public": "Public repost", + "status.reblog_visibility_unlisted": "Unlisted repost", "status.reblogged_by": "{name} reposted", + "status.reblogged_by_private": "{name} reposted to followers", "status.reblogged_by_with_group": "{name} reposted from {group}", "status.reblogs.empty": "No one has reposted this post yet. When someone does, they will show up here.", "status.redraft": "Delete & re-draft",