Mention repost author when replying to a reposted status

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-04-29 23:38:49 +02:00
parent 775d272254
commit 9c5e3a98e0
6 changed files with 29 additions and 18 deletions

View file

@ -152,9 +152,10 @@ interface ComposeReplyAction {
account: Account; account: Account;
explicitAddressing: boolean; explicitAddressing: boolean;
preserveSpoilers: boolean; preserveSpoilers: boolean;
rebloggedBy?: Account;
} }
const replyCompose = (status: Status) => const replyCompose = (status: Status, rebloggedBy?: Account) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const instance = state.instance; const instance = state.instance;
@ -171,6 +172,7 @@ const replyCompose = (status: Status) =>
account, account,
explicitAddressing, explicitAddressing,
preserveSpoilers, preserveSpoilers,
rebloggedBy: rebloggedBy,
}; };
dispatch(action); dispatch(action);

View file

@ -28,7 +28,7 @@ import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts';
import GroupPopover from './groups/popover/group-popover'; import GroupPopover from './groups/popover/group-popover';
import type { Menu } from 'soapbox/components/dropdown-menu'; import type { Menu } from 'soapbox/components/dropdown-menu';
import type { Group, Status } from 'soapbox/types/entities'; import type { Account, Group, Status } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
@ -99,6 +99,7 @@ const messages = defineMessages({
interface IStatusActionBar { interface IStatusActionBar {
status: Status; status: Status;
rebloggedBy?: Account;
withLabels?: boolean; withLabels?: boolean;
expandable?: boolean; expandable?: boolean;
space?: 'sm' | 'md' | 'lg'; space?: 'sm' | 'md' | 'lg';
@ -113,6 +114,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
space = 'sm', space = 'sm',
statusActionButtonTheme = 'default', statusActionButtonTheme = 'default',
fromBookmarks = false, fromBookmarks = false,
rebloggedBy,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
@ -148,7 +150,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const handleReplyClick: React.MouseEventHandler = (e) => { const handleReplyClick: React.MouseEventHandler = (e) => {
if (me) { if (me) {
dispatch(replyCompose(status)); dispatch(replyCompose(status, rebloggedBy));
} else { } else {
onOpenUnauthorizedModal('REPLY'); onOpenUnauthorizedModal('REPLY');
} }
@ -211,7 +213,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}; };
const doDeleteStatus = (withRedraft = false) => { const doDeleteStatus = (withRedraft = false) => {
dispatch((_, getState) => { dispatch((_) => {
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.id, withRedraft)); dispatch(deleteStatus(status.id, withRedraft));
} else { } else {

View file

@ -138,7 +138,7 @@ const Status: React.FC<IStatus> = (props) => {
const handleHotkeyReply = (e?: KeyboardEvent): void => { const handleHotkeyReply = (e?: KeyboardEvent): void => {
e?.preventDefault(); e?.preventDefault();
dispatch(replyCompose(actualStatus)); dispatch(replyCompose(actualStatus, status.reblog && typeof status.reblog === 'object' ? status.account : undefined));
}; };
const handleHotkeyFavourite = (): void => { const handleHotkeyFavourite = (): void => {
@ -458,7 +458,7 @@ const Status: React.FC<IStatus> = (props) => {
{!hideActionBar && ( {!hideActionBar && (
<div className='pt-4'> <div className='pt-4'>
<StatusActionBar status={actualStatus} fromBookmarks={fromBookmarks} /> <StatusActionBar status={actualStatus} rebloggedBy={isReblog ? status.account : undefined} fromBookmarks={fromBookmarks} />
</div> </div>
)} )}
</div> </div>

View file

@ -2,8 +2,7 @@ import React, { useCallback } from 'react';
import { FormattedList, FormattedMessage } from 'react-intl'; import { FormattedList, FormattedMessage } from 'react-intl';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useOwnAccount } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors'; import { makeGetStatus } from 'soapbox/selectors';
import type { Status as StatusEntity } from 'soapbox/types/entities'; import type { Status as StatusEntity } from 'soapbox/types/entities';
@ -20,14 +19,11 @@ const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
const getStatus = useCallback(makeGetStatus(), []); const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! })); const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! }));
const to = compose.to; const to = compose.to;
const { account } = useOwnAccount();
if (!features.explicitAddressing || !status || !to) { if (!features.explicitAddressing || !status || !to) {
return null; return null;
} }
const parentTo = status && statusToMentionsAccountIdsArray(status, account!);
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => { const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();
@ -36,7 +32,7 @@ const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {
})); }));
}; };
if (!parentTo || (parentTo.size === 0)) { if (!compose.parent_reblogged_by && to.size === 0) {
return null; return null;
} }

View file

@ -22,7 +22,7 @@ const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ composeId, onClose
const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! })); const status = useAppSelector<StatusEntity | null>(state => getStatus(state, { id: compose.in_reply_to! }));
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const mentions = statusToMentionsAccountIdsArray(status!, account!); const mentions = statusToMentionsAccountIdsArray(status!, account!, compose.parent_reblogged_by);
const author = (status?.account as AccountEntity).id; const author = (status?.account as AccountEntity).id;
const onClickClose = () => { const onClickClose = () => {

View file

@ -108,6 +108,7 @@ export const ReducerCompose = ImmutableRecord({
tagHistory: ImmutableList<string>(), tagHistory: ImmutableList<string>(),
text: '', text: '',
to: ImmutableOrderedSet<string>(), to: ImmutableOrderedSet<string>(),
parent_reblogged_by: null as string | null,
}); });
type State = ImmutableMap<string, Compose>; type State = ImmutableMap<string, Compose>;
@ -125,19 +126,21 @@ const statusToTextMentions = (status: Status, account: Account) => {
.join(''); .join('');
}; };
export const statusToMentionsArray = (status: Status, account: Account) => { export const statusToMentionsArray = (status: Status, account: Account, rebloggedBy?: Account) => {
const author = status.getIn(['account', 'acct']) as string; const author = status.getIn(['account', 'acct']) as string;
const mentions = status.get('mentions')?.map((m) => m.acct) || []; const mentions = status.get('mentions')?.map((m) => m.acct) || [];
return ImmutableOrderedSet<string>([author]) return ImmutableOrderedSet<string>([author])
.concat(rebloggedBy ? [rebloggedBy.acct] : [])
.concat(mentions) .concat(mentions)
.delete(account.acct) as ImmutableOrderedSet<string>; .delete(account.acct) as ImmutableOrderedSet<string>;
}; };
export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: Account) => { export const statusToMentionsAccountIdsArray = (status: StatusEntity, account: Account, parentRebloggedBy?: string | null) => {
const mentions = status.mentions.map((m) => m.id); const mentions = status.mentions.map((m) => m.id);
return ImmutableOrderedSet<string>([account.id]) return ImmutableOrderedSet<string>([status.account.id])
.concat(parentRebloggedBy ? [parentRebloggedBy] : [])
.concat(mentions) .concat(mentions)
.delete(account.id); .delete(account.id);
}; };
@ -306,9 +309,14 @@ export default function compose(state = initialState, action: ComposeAction | Ev
return updateCompose(state, action.id, compose => compose.withMutations(map => { return updateCompose(state, action.id, compose => compose.withMutations(map => {
const defaultCompose = state.get('default')!; const defaultCompose = state.get('default')!;
const to = action.explicitAddressing
? statusToMentionsArray(action.status, action.account, action.rebloggedBy)
: ImmutableOrderedSet<string>();
map.set('group_id', action.status.getIn(['group', 'id']) as string); map.set('group_id', action.status.getIn(['group', 'id']) as string);
map.set('in_reply_to', action.status.get('id')); map.set('in_reply_to', action.status.get('id'));
map.set('to', action.explicitAddressing ? statusToMentionsArray(action.status, action.account) : ImmutableOrderedSet<string>()); map.set('to', to);
map.set('parent_reblogged_by', action.rebloggedBy?.id || null);
map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : ''); map.set('text', !action.explicitAddressing ? statusToTextMentions(action.status, action.account) : '');
map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy)); map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy));
map.set('focusDate', new Date()); map.set('focusDate', new Date());
@ -334,6 +342,7 @@ export default function compose(state = initialState, action: ComposeAction | Ev
map.set('quote', action.status.get('id')); map.set('quote', action.status.get('id'));
map.set('to', ImmutableOrderedSet<string>([author])); map.set('to', ImmutableOrderedSet<string>([author]));
map.set('parent_reblogged_by', null);
map.set('text', ''); map.set('text', '');
map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy)); map.set('privacy', privacyPreference(action.status.visibility, defaultCompose.privacy));
map.set('focusDate', new Date()); map.set('focusDate', new Date());
@ -435,11 +444,13 @@ export default function compose(state = initialState, action: ComposeAction | Ev
}))); })));
case COMPOSE_SET_STATUS: case COMPOSE_SET_STATUS:
return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => {
const to = action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet<string>();
if (!action.withRedraft && !action.draftId) { if (!action.withRedraft && !action.draftId) {
map.set('id', action.status.id); map.set('id', action.status.id);
} }
map.set('text', action.rawText || unescapeHTML(expandMentions(action.status))); map.set('text', action.rawText || unescapeHTML(expandMentions(action.status)));
map.set('to', action.explicitAddressing ? getExplicitMentions(action.status.account.id, action.status) : ImmutableOrderedSet<string>()); map.set('to', to);
map.set('parent_reblogged_by', null);
map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('privacy', action.status.get('visibility')); map.set('privacy', action.status.get('visibility'));
map.set('focusDate', new Date()); map.set('focusDate', new Date());