Allow removing deleted quotes from composer (#36080)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -13,7 +13,9 @@ import type { RootState } from 'mastodon/store';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { fetchStatus } from '../actions/statuses';
|
||||
import { makeGetStatus } from '../selectors';
|
||||
import { makeGetStatusWithExtraInfo } from '../selectors';
|
||||
|
||||
import { Button } from './button';
|
||||
|
||||
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
|
||||
|
||||
@@ -55,11 +57,15 @@ const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => {
|
||||
);
|
||||
};
|
||||
|
||||
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
|
||||
type GetStatusSelector = (
|
||||
state: RootState,
|
||||
props: { id?: string | null; contextType?: string },
|
||||
) => Status | null;
|
||||
) => {
|
||||
status: Status | null;
|
||||
loadingState: 'not-found' | 'loading' | 'filtered' | 'complete';
|
||||
};
|
||||
|
||||
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
|
||||
|
||||
interface QuotedStatusProps {
|
||||
quote: QuoteMap;
|
||||
@@ -86,31 +92,41 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
);
|
||||
|
||||
const quotedStatusId = quote.get('quoted_status');
|
||||
const status = useAppSelector((state) =>
|
||||
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
||||
const getStatusSelector = useMemo(
|
||||
() => makeGetStatusWithExtraInfo() as GetStatusSelector,
|
||||
[],
|
||||
);
|
||||
const { status, loadingState } = useAppSelector((state) =>
|
||||
getStatusSelector(state, { id: quotedStatusId, contextType }),
|
||||
);
|
||||
|
||||
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
|
||||
const shouldFetchQuote =
|
||||
!status?.get('isLoading') &&
|
||||
quoteState !== 'deleted' &&
|
||||
loadingState === 'not-found';
|
||||
const isLoaded = loadingState === 'complete';
|
||||
|
||||
const isFetchingQuoteRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldLoadQuote && quotedStatusId) {
|
||||
if (isLoaded) {
|
||||
isFetchingQuoteRef.current = false;
|
||||
}
|
||||
}, [isLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFetchQuote && quotedStatusId && !isFetchingQuoteRef.current) {
|
||||
dispatch(
|
||||
fetchStatus(quotedStatusId, {
|
||||
parentQuotePostId,
|
||||
alsoFetchContext: false,
|
||||
}),
|
||||
);
|
||||
isFetchingQuoteRef.current = true;
|
||||
}
|
||||
}, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]);
|
||||
}, [shouldFetchQuote, quotedStatusId, parentQuotePostId, dispatch]);
|
||||
|
||||
// In order to find out whether the quoted post should be completely hidden
|
||||
// due to a matching filter, we run it through the selector used by `status_container`.
|
||||
// If this returns null even though `status` exists, it's because it's filtered.
|
||||
const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector;
|
||||
const statusWithExtraData = useAppSelector((state) =>
|
||||
getStatus(state, { id: quotedStatusId, contextType }),
|
||||
);
|
||||
const isFilteredAndHidden = status && statusWithExtraData === null;
|
||||
const isFilteredAndHidden = loadingState === 'filtered';
|
||||
|
||||
let quoteError: React.ReactNode = null;
|
||||
|
||||
@@ -154,10 +170,20 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
quoteState === 'unauthorized'
|
||||
) {
|
||||
quoteError = (
|
||||
<FormattedMessage
|
||||
id='status.quote_error.not_available'
|
||||
defaultMessage='Post unavailable'
|
||||
/>
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='status.quote_error.not_available'
|
||||
defaultMessage='Post unavailable'
|
||||
/>
|
||||
{contextType === 'composer' && (
|
||||
<Button compact plain onClick={onQuoteCancel}>
|
||||
<FormattedMessage
|
||||
id='status.remove_quote'
|
||||
defaultMessage='Remove'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ export const ComposeQuotedStatus: FC = () => {
|
||||
const quotedStatusId = useAppSelector(
|
||||
(state) => state.compose.get('quoted_status_id') as string | null,
|
||||
);
|
||||
|
||||
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
||||
|
||||
const quote = useMemo(
|
||||
() =>
|
||||
quotedStatusId
|
||||
@@ -22,16 +24,20 @@ export const ComposeQuotedStatus: FC = () => {
|
||||
: null,
|
||||
[quotedStatusId],
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleQuoteCancel = useCallback(() => {
|
||||
dispatch(quoteComposeCancel());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!quote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QuotedStatus
|
||||
quote={quote}
|
||||
contextType='composer'
|
||||
onQuoteCancel={!isEditing ? handleQuoteCancel : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -924,6 +924,7 @@
|
||||
"status.redraft": "Delete & re-draft",
|
||||
"status.remove_bookmark": "Remove bookmark",
|
||||
"status.remove_favourite": "Remove from favorites",
|
||||
"status.remove_quote": "Remove",
|
||||
"status.replied_in_thread": "Replied in thread",
|
||||
"status.replied_to": "Replied to {name}",
|
||||
"status.reply": "Reply",
|
||||
|
||||
@@ -8,57 +8,98 @@ import { getFilters } from './filters';
|
||||
export { makeGetAccount } from "./accounts";
|
||||
export { getStatusList } from "./statuses";
|
||||
|
||||
const getStatusInputSelectors = [
|
||||
(state, { id }) => state.getIn(['statuses', id]),
|
||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||
getFilters,
|
||||
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
|
||||
];
|
||||
|
||||
function getStatusResultFunction(
|
||||
statusBase,
|
||||
statusReblog,
|
||||
accountBase,
|
||||
accountReblog,
|
||||
filters,
|
||||
warnInsteadOfHide
|
||||
) {
|
||||
if (!statusBase) {
|
||||
return {
|
||||
status: null,
|
||||
loadingState: 'not-found',
|
||||
};
|
||||
}
|
||||
|
||||
if (statusBase.get('isLoading')) {
|
||||
return {
|
||||
status: null,
|
||||
loadingState: 'loading',
|
||||
}
|
||||
}
|
||||
|
||||
if (statusReblog) {
|
||||
statusReblog = statusReblog.set('account', accountReblog);
|
||||
} else {
|
||||
statusReblog = null;
|
||||
}
|
||||
|
||||
let filtered = false;
|
||||
let mediaFiltered = false;
|
||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||
return {
|
||||
status: null,
|
||||
loadingState: 'filtered',
|
||||
}
|
||||
}
|
||||
|
||||
let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
|
||||
if (!mediaFilters.isEmpty()) {
|
||||
mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||
}
|
||||
|
||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
|
||||
if (!filterResults.isEmpty()) {
|
||||
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: statusBase.withMutations(map => {
|
||||
map.set('reblog', statusReblog);
|
||||
map.set('account', accountBase);
|
||||
map.set('matched_filters', filtered);
|
||||
map.set('matched_media_filters', mediaFiltered);
|
||||
}),
|
||||
loadingState: 'complete'
|
||||
};
|
||||
}
|
||||
|
||||
export const makeGetStatus = () => {
|
||||
return createSelector(
|
||||
[
|
||||
(state, { id }) => state.getIn(['statuses', id]),
|
||||
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||
getFilters,
|
||||
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
|
||||
],
|
||||
|
||||
(statusBase, statusReblog, accountBase, accountReblog, filters, warnInsteadOfHide) => {
|
||||
if (!statusBase || statusBase.get('isLoading')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (statusReblog) {
|
||||
statusReblog = statusReblog.set('account', accountReblog);
|
||||
} else {
|
||||
statusReblog = null;
|
||||
}
|
||||
|
||||
let filtered = false;
|
||||
let mediaFiltered = false;
|
||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
|
||||
if (!mediaFilters.isEmpty()) {
|
||||
mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||
}
|
||||
|
||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
|
||||
if (!filterResults.isEmpty()) {
|
||||
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||
}
|
||||
}
|
||||
|
||||
return statusBase.withMutations(map => {
|
||||
map.set('reblog', statusReblog);
|
||||
map.set('account', accountBase);
|
||||
map.set('matched_filters', filtered);
|
||||
map.set('matched_media_filters', mediaFiltered);
|
||||
});
|
||||
getStatusInputSelectors,
|
||||
(...args) => {
|
||||
const {status} = getStatusResultFunction(...args);
|
||||
return status
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This selector extends the `makeGetStatus` with a more detailed
|
||||
* `loadingState`, which is useful to find out why `null` is returned
|
||||
* for the `status` field
|
||||
*/
|
||||
export const makeGetStatusWithExtraInfo = () => {
|
||||
return createSelector(
|
||||
getStatusInputSelectors,
|
||||
getStatusResultFunction,
|
||||
);
|
||||
};
|
||||
|
||||
export const makeGetPictureInPicture = () => {
|
||||
return createSelector([
|
||||
(state, { id }) => state.picture_in_picture.statusId === id,
|
||||
|
||||
Reference in New Issue
Block a user