Infer quote_id from links in status content

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-04-29 23:53:10 +02:00
parent 1d177831fe
commit 9668846ff0
6 changed files with 75 additions and 8 deletions

View file

@ -90,6 +90,8 @@ const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const;
const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const; const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const;
const COMPOSE_ADD_SUGGESTED_QUOTE = 'COMPOSE_ADD_SUGGESTED_QUOTE' as const;
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
const messages = defineMessages({ const messages = defineMessages({
@ -210,9 +212,9 @@ const quoteCompose = (status: Status) =>
dispatch(openModal('COMPOSE')); dispatch(openModal('COMPOSE'));
}; };
const cancelQuoteCompose = () => ({ const cancelQuoteCompose = (composeId: string) => ({
type: COMPOSE_QUOTE_CANCEL, type: COMPOSE_QUOTE_CANCEL,
id: 'compose-modal', id: composeId,
}); });
const groupComposeModal = (group: Group) => const groupComposeModal = (group: Group) =>
@ -868,6 +870,12 @@ const changeMediaOrder = (composeId: string, a: string, b: string) => ({
b, b,
}); });
const addSuggestedQuote = (composeId: string, quoteId: string) => ({
type: COMPOSE_ADD_SUGGESTED_QUOTE,
id: composeId,
quoteId: quoteId,
});
type ComposeAction = type ComposeAction =
ComposeSetStatusAction ComposeSetStatusAction
| ReturnType<typeof changeCompose> | ReturnType<typeof changeCompose>
@ -914,6 +922,7 @@ type ComposeAction =
| ComposeEventReplyAction | ComposeEventReplyAction
| ReturnType<typeof setEditorState> | ReturnType<typeof setEditorState>
| ReturnType<typeof changeMediaOrder> | ReturnType<typeof changeMediaOrder>
| ReturnType<typeof addSuggestedQuote>
export { export {
COMPOSE_CHANGE, COMPOSE_CHANGE,
@ -962,6 +971,7 @@ export {
COMPOSE_SET_STATUS, COMPOSE_SET_STATUS,
COMPOSE_EDITOR_STATE_SET, COMPOSE_EDITOR_STATE_SET,
COMPOSE_CHANGE_MEDIA_ORDER, COMPOSE_CHANGE_MEDIA_ORDER,
COMPOSE_ADD_SUGGESTED_QUOTE,
setComposeToStatus, setComposeToStatus,
changeCompose, changeCompose,
replyCompose, replyCompose,
@ -1017,5 +1027,6 @@ export {
eventDiscussionCompose, eventDiscussionCompose,
setEditorState, setEditorState,
changeMediaOrder, changeMediaOrder,
addSuggestedQuote,
type ComposeAction, type ComposeAction,
}; };

View file

@ -154,6 +154,8 @@ const Account = ({
); );
} }
if (!withRelationship) return null;
if (account.id !== me) { if (account.id !== me) {
return <ActionButton account={account} actionType={actionType} />; return <ActionButton account={account} actionType={actionType} />;
} }
@ -297,7 +299,7 @@ const Account = ({
</HStack> </HStack>
<div ref={actionRef}> <div ref={actionRef}>
{(withRelationship || action) ? renderAction() : null} {renderAction()}
</div> </div>
</HStack> </HStack>
</div> </div>

View file

@ -17,7 +17,7 @@ const QuotedStatusContainer: React.FC<IQuotedStatusContainer> = ({ composeId })
const status = useAppSelector(state => getStatus(state, { id: state.compose.get(composeId)?.quote! })); const status = useAppSelector(state => getStatus(state, { id: state.compose.get(composeId)?.quote! }));
const onCancel = () => { const onCancel = () => {
dispatch(cancelQuoteCompose()); dispatch(cancelQuoteCompose(composeId));
}; };
if (!status) { if (!status) {

View file

@ -1,9 +1,12 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot } from 'lexical'; import { $getRoot } from 'lexical';
import { useEffect } from 'react'; import debounce from 'lodash/debounce';
import { useCallback, useEffect } from 'react';
import { setEditorState } from 'soapbox/actions/compose'; import { addSuggestedQuote, setEditorState } from 'soapbox/actions/compose';
import { useAppDispatch } from 'soapbox/hooks'; import { fetchStatus } from 'soapbox/actions/statuses';
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import { getStatusIdsFromLinksInContent } from 'soapbox/utils/status';
interface IStatePlugin { interface IStatePlugin {
composeId: string; composeId: string;
@ -12,6 +15,38 @@ interface IStatePlugin {
const StatePlugin: React.FC<IStatePlugin> = ({ composeId }) => { const StatePlugin: React.FC<IStatePlugin> = ({ composeId }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
const features = useFeatures();
const getQuoteSuggestions = useCallback(debounce((text: string) => {
dispatch(async (_, getState) => {
const state = getState();
const compose = state.compose.get(composeId);
if (!features.quotePosts || compose?.quote) return;
const ids = getStatusIdsFromLinksInContent(text);
let quoteId: string | undefined;
for (const id of ids) {
if (compose?.dismissed_quotes.includes(id)) continue;
if (state.statuses.get(id)) {
quoteId = id;
break;
}
const status = await dispatch(fetchStatus(id));
if (status) {
quoteId = status.id;
break;
}
}
if (quoteId) dispatch(addSuggestedQuote(composeId, quoteId));
});
}, 2000), []);
useEffect(() => { useEffect(() => {
editor.registerUpdateListener(({ editorState }) => { editor.registerUpdateListener(({ editorState }) => {
@ -19,6 +54,7 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId }) => {
const isEmpty = text === ''; const isEmpty = text === '';
const data = isEmpty ? null : JSON.stringify(editorState.toJSON()); const data = isEmpty ? null : JSON.stringify(editorState.toJSON());
dispatch(setEditorState(composeId, data, text)); dispatch(setEditorState(composeId, data, text));
getQuoteSuggestions(text);
}); });
}, [editor]); }, [editor]);

View file

@ -54,6 +54,7 @@ import {
COMPOSE_EDITOR_STATE_SET, COMPOSE_EDITOR_STATE_SET,
ComposeAction, ComposeAction,
COMPOSE_CHANGE_MEDIA_ORDER, COMPOSE_CHANGE_MEDIA_ORDER,
COMPOSE_ADD_SUGGESTED_QUOTE,
} from '../actions/compose'; } from '../actions/compose';
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me';
@ -109,6 +110,7 @@ export const ReducerCompose = ImmutableRecord({
text: '', text: '',
to: ImmutableOrderedSet<string>(), to: ImmutableOrderedSet<string>(),
parent_reblogged_by: null as string | null, parent_reblogged_by: null as string | null,
dismissed_quotes: ImmutableOrderedSet<string>(),
}); });
type State = ImmutableMap<string, Compose>; type State = ImmutableMap<string, Compose>;
@ -362,7 +364,6 @@ export default function compose(state = initialState, action: ComposeAction | Ev
case COMPOSE_UPLOAD_CHANGE_REQUEST: case COMPOSE_UPLOAD_CHANGE_REQUEST:
return updateCompose(state, action.id, compose => compose.set('is_changing_upload', true)); return updateCompose(state, action.id, compose => compose.set('is_changing_upload', true));
case COMPOSE_REPLY_CANCEL: case COMPOSE_REPLY_CANCEL:
case COMPOSE_QUOTE_CANCEL:
case COMPOSE_RESET: case COMPOSE_RESET:
case COMPOSE_SUBMIT_SUCCESS: case COMPOSE_SUBMIT_SUCCESS:
return updateCompose(state, action.id, () => state.get('default')!.withMutations(map => { return updateCompose(state, action.id, () => state.get('default')!.withMutations(map => {
@ -543,6 +544,13 @@ export default function compose(state = initialState, action: ComposeAction | Ev
return list.splice(indexA, 1).splice(indexB, 0, moveItem); return list.splice(indexA, 1).splice(indexB, 0, moveItem);
})); }));
case COMPOSE_ADD_SUGGESTED_QUOTE:
return updateCompose(state, action.id, compose => compose
.set('quote', action.quoteId));
case COMPOSE_QUOTE_CANCEL:
return updateCompose(state, action.id, compose => compose
.update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes)
.set('quote', null));
default: default:
return state; return state;
} }

View file

@ -71,3 +71,13 @@ export const getActualStatus = <T extends { reblog: T | string | null }>(status:
return status; return status;
} }
}; };
export const getStatusIdsFromLinksInContent = (content: string): string[] => {
const urls = content.match(RegExp(`${window.location.origin}/@([a-z\\d_-]+(?:@[^@\\s]+)?)/posts/[a-z0-9]+(?!\\S)`, 'gi'));
if (!urls) return [];
return Array.from(new Set(urls
.map(url => url.split('/').at(-1) as string)
.filter(url => url)));
};