pl-fe: allow specifying boost visibility

Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
Nicole Mikołajczyk
2025-04-08 14:51:34 +02:00
parent df9d4a6aaf
commit 9a74a3cbeb
8 changed files with 73 additions and 22 deletions

View File

@ -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 } });

View File

@ -47,13 +47,13 @@ const messages = defineMessages({
selectFolder: { id: 'status.bookmark.select_folder', defaultMessage: 'Select folder' },
});
const reblog = (status: Pick<Status, 'id'>) =>
const reblog = (status: Pick<Status, 'id'>, 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<Status, 'id'>) =>
});
};
const toggleReblog = (status: Pick<Status, 'id' | 'reblogged'>) => {
const toggleReblog = (status: Pick<Status, 'id' | 'reblogged'>, visibility?: string) => {
if (status.reblogged) {
return unreblog(status);
} else {
return reblog(status);
return reblog(status, visibility);
}
};

View File

@ -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<IMenuButton> = ({
dispatch(togglePin(status));
};
const handleReblogClick: React.EventHandler<React.MouseEvent> = (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<IMenuButton> = ({
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({

View File

@ -245,19 +245,29 @@ const Status: React.FC<IStatus> = (props) => {
);
}
const values = {
name: <FormattedList type='conjunction' value={renderedAccounts} />,
count: accounts.length,
};
return (
<StatusInfo
avatarSize={avatarSize}
icon={<Icon src={require('@tabler/icons/outline/repeat.svg')} className='size-4 text-green-600' />}
text={
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} reposted'
values={{
name: <FormattedList type='conjunction' value={renderedAccounts} />,
count: accounts.length,
}}
/>
status.visibility === 'private' ? (
<FormattedMessage
id='status.reblogged_by_private'
defaultMessage='{name} reposted to followers'
values={values}
/>
) : (
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} reposted'
values={values}
/>
)
}
/>
);

View File

@ -270,7 +270,7 @@ const Notification: React.FC<INotification> = (props) => {
} else {
openModal('BOOST', {
statusId: status.id,
onReblog: (status) => {
onReblog: () => {
dispatch(reblog(status));
},
});

View File

@ -150,7 +150,7 @@ const Thread: React.FC<IThread> = ({
if ((e && e.shiftKey) || !boostModal) {
handleModalReblog(status);
} else {
openModal('BOOST', { statusId: status.id, onReblog: handleModalReblog });
openModal('BOOST', { statusId: status.id, onReblog: () => handleModalReblog(status) });
}
}
};

View File

@ -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<StatusEntity, 'id' | 'reblogged'>) => void;
onReblog: () => void;
visibility?: string;
}
const BoostModal: React.FC<BaseModalProps & BoostModalProps> = ({ statusId, onReblog, onClose }) => {
const BoostModal: React.FC<BaseModalProps & BoostModalProps> = ({ 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<BaseModalProps & BoostModalProps> = ({ statusId, onRe
return (
<Modal
title={<FormattedMessage id='boost_modal.title' defaultMessage='Repost?' />}
title={visibility === 'unlisted'
? <FormattedMessage id='boost_modal.title.unlisted' defaultMessage='Repost unlisted?' />
: visibility === 'private'
? <FormattedMessage id='boost_modal.title.private' defaultMessage='Repost privately?' />
: <FormattedMessage id='boost_modal.title' defaultMessage='Repost?' />}
confirmationAction={handleReblog}
confirmationText={intl.formatMessage(buttonText)}
>

View File

@ -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",