diff --git a/src/actions/compose.ts b/src/actions/compose.ts index eddd9ef7d..9ed746e95 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -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_ADD_SUGGESTED_QUOTE = 'COMPOSE_ADD_SUGGESTED_QUOTE' as const; + const getAccount = makeGetAccount(); const messages = defineMessages({ @@ -210,9 +212,9 @@ const quoteCompose = (status: Status) => dispatch(openModal('COMPOSE')); }; -const cancelQuoteCompose = () => ({ +const cancelQuoteCompose = (composeId: string) => ({ type: COMPOSE_QUOTE_CANCEL, - id: 'compose-modal', + id: composeId, }); const groupComposeModal = (group: Group) => @@ -868,6 +870,12 @@ const changeMediaOrder = (composeId: string, a: string, b: string) => ({ b, }); +const addSuggestedQuote = (composeId: string, quoteId: string) => ({ + type: COMPOSE_ADD_SUGGESTED_QUOTE, + id: composeId, + quoteId: quoteId, +}); + type ComposeAction = ComposeSetStatusAction | ReturnType @@ -914,6 +922,7 @@ type ComposeAction = | ComposeEventReplyAction | ReturnType | ReturnType + | ReturnType export { COMPOSE_CHANGE, @@ -962,6 +971,7 @@ export { COMPOSE_SET_STATUS, COMPOSE_EDITOR_STATE_SET, COMPOSE_CHANGE_MEDIA_ORDER, + COMPOSE_ADD_SUGGESTED_QUOTE, setComposeToStatus, changeCompose, replyCompose, @@ -1017,5 +1027,6 @@ export { eventDiscussionCompose, setEditorState, changeMediaOrder, + addSuggestedQuote, type ComposeAction, }; diff --git a/src/components/account.tsx b/src/components/account.tsx index e458dd4a5..89350420f 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -154,6 +154,8 @@ const Account = ({ ); } + if (!withRelationship) return null; + if (account.id !== me) { return ; } @@ -297,7 +299,7 @@ const Account = ({
- {(withRelationship || action) ? renderAction() : null} + {renderAction()}
diff --git a/src/features/compose/containers/quoted-status-container.tsx b/src/features/compose/containers/quoted-status-container.tsx index 5383aaf18..879d73267 100644 --- a/src/features/compose/containers/quoted-status-container.tsx +++ b/src/features/compose/containers/quoted-status-container.tsx @@ -17,7 +17,7 @@ const QuotedStatusContainer: React.FC = ({ composeId }) const status = useAppSelector(state => getStatus(state, { id: state.compose.get(composeId)?.quote! })); const onCancel = () => { - dispatch(cancelQuoteCompose()); + dispatch(cancelQuoteCompose(composeId)); }; if (!status) { diff --git a/src/features/compose/editor/plugins/state-plugin.tsx b/src/features/compose/editor/plugins/state-plugin.tsx index f8d198ac8..cb53a1104 100644 --- a/src/features/compose/editor/plugins/state-plugin.tsx +++ b/src/features/compose/editor/plugins/state-plugin.tsx @@ -1,9 +1,12 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 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 { useAppDispatch } from 'soapbox/hooks'; +import { addSuggestedQuote, setEditorState } from 'soapbox/actions/compose'; +import { fetchStatus } from 'soapbox/actions/statuses'; +import { useAppDispatch, useFeatures } from 'soapbox/hooks'; +import { getStatusIdsFromLinksInContent } from 'soapbox/utils/status'; interface IStatePlugin { composeId: string; @@ -12,6 +15,38 @@ interface IStatePlugin { const StatePlugin: React.FC = ({ composeId }) => { const dispatch = useAppDispatch(); 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(() => { editor.registerUpdateListener(({ editorState }) => { @@ -19,6 +54,7 @@ const StatePlugin: React.FC = ({ composeId }) => { const isEmpty = text === ''; const data = isEmpty ? null : JSON.stringify(editorState.toJSON()); dispatch(setEditorState(composeId, data, text)); + getQuoteSuggestions(text); }); }, [editor]); diff --git a/src/reducers/compose.ts b/src/reducers/compose.ts index 559ddfcad..c09ba48e1 100644 --- a/src/reducers/compose.ts +++ b/src/reducers/compose.ts @@ -54,6 +54,7 @@ import { COMPOSE_EDITOR_STATE_SET, ComposeAction, COMPOSE_CHANGE_MEDIA_ORDER, + COMPOSE_ADD_SUGGESTED_QUOTE, } from '../actions/compose'; import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, MeAction } from '../actions/me'; @@ -109,6 +110,7 @@ export const ReducerCompose = ImmutableRecord({ text: '', to: ImmutableOrderedSet(), parent_reblogged_by: null as string | null, + dismissed_quotes: ImmutableOrderedSet(), }); type State = ImmutableMap; @@ -362,7 +364,6 @@ export default function compose(state = initialState, action: ComposeAction | Ev case COMPOSE_UPLOAD_CHANGE_REQUEST: return updateCompose(state, action.id, compose => compose.set('is_changing_upload', true)); case COMPOSE_REPLY_CANCEL: - case COMPOSE_QUOTE_CANCEL: case COMPOSE_RESET: case COMPOSE_SUBMIT_SUCCESS: 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); })); + 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: return state; } diff --git a/src/utils/status.ts b/src/utils/status.ts index 79853920b..8ab54ae7c 100644 --- a/src/utils/status.ts +++ b/src/utils/status.ts @@ -71,3 +71,13 @@ export const getActualStatus = (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))); +};