mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-16 01:09:55 +00:00
Merge commit '45a996f12b04f88e0774bc6d6b52452ce39b612f' into glitch-soc/merge-upstream
This commit is contained in:
@@ -351,6 +351,31 @@ const setInputDisabled = (
|
||||
}
|
||||
};
|
||||
|
||||
const setInputHint = (
|
||||
input: HTMLInputElement | HTMLSelectElement,
|
||||
hintPrefix: string,
|
||||
) => {
|
||||
const fieldWrapper = input.closest<HTMLElement>('.fields-group > .input');
|
||||
if (!fieldWrapper) return;
|
||||
|
||||
const hint = fieldWrapper.dataset[`${hintPrefix}Hint`];
|
||||
const hintElement =
|
||||
fieldWrapper.querySelector<HTMLSpanElement>(':scope > .hint');
|
||||
|
||||
if (hint) {
|
||||
if (hintElement) {
|
||||
hintElement.textContent = hint;
|
||||
} else {
|
||||
const newHintElement = document.createElement('span');
|
||||
newHintElement.className = 'hint';
|
||||
newHintElement.textContent = hint;
|
||||
fieldWrapper.appendChild(newHintElement);
|
||||
}
|
||||
} else {
|
||||
hintElement?.remove();
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#account_statuses_cleanup_policy_enabled',
|
||||
@@ -379,6 +404,8 @@ const updateDefaultQuotePrivacyFromPrivacy = (
|
||||
);
|
||||
if (!select) return;
|
||||
|
||||
setInputHint(select, privacySelect.value);
|
||||
|
||||
if (privacySelect.value === 'private') {
|
||||
select.value = 'nobody';
|
||||
setInputDisabled(select, true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
@@ -13,14 +13,18 @@ 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;
|
||||
|
||||
const QuoteWrapper: React.FC<{
|
||||
isError?: boolean;
|
||||
contextType?: string;
|
||||
onQuoteCancel?: () => void;
|
||||
children: React.ReactElement;
|
||||
}> = ({ isError, children }) => {
|
||||
}> = ({ isError, contextType, onQuoteCancel, children }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('status__quote', {
|
||||
@@ -28,6 +32,11 @@ const QuoteWrapper: React.FC<{
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{contextType === 'composer' && (
|
||||
<Button compact plain onClick={onQuoteCancel}>
|
||||
<FormattedMessage id='status.remove_quote' defaultMessage='Remove' />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -55,11 +64,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 +99,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;
|
||||
|
||||
@@ -130,27 +153,27 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
/>
|
||||
|
||||
<LearnMoreLink>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id='status.quote_error.pending_approval_popout.title'
|
||||
defaultMessage='Pending quote? Remain calm'
|
||||
/>
|
||||
</h6>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='status.quote_error.pending_approval_popout.body'
|
||||
defaultMessage='Quotes shared across the Fediverse may take time to display, as different servers have different protocols.'
|
||||
defaultMessage="On Mastodon, you can control whether someone can quote you. This post is pending while we're getting the original author's approval."
|
||||
/>
|
||||
</p>
|
||||
</LearnMoreLink>
|
||||
</>
|
||||
);
|
||||
} else if (quoteState === 'revoked') {
|
||||
quoteError = (
|
||||
<FormattedMessage
|
||||
id='status.quote_error.revoked'
|
||||
defaultMessage='Post removed by author'
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
!status ||
|
||||
!quotedStatusId ||
|
||||
quoteState === 'deleted' ||
|
||||
quoteState === 'rejected' ||
|
||||
quoteState === 'revoked' ||
|
||||
quoteState === 'unauthorized'
|
||||
) {
|
||||
quoteError = (
|
||||
@@ -162,7 +185,15 @@ export const QuotedStatus: React.FC<QuotedStatusProps> = ({
|
||||
}
|
||||
|
||||
if (quoteError) {
|
||||
return <QuoteWrapper isError>{quoteError}</QuoteWrapper>;
|
||||
return (
|
||||
<QuoteWrapper
|
||||
isError
|
||||
contextType={contextType}
|
||||
onQuoteCancel={onQuoteCancel}
|
||||
>
|
||||
{quoteError}
|
||||
</QuoteWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'link' && status) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -905,8 +905,8 @@
|
||||
"status.quote_error.filtered": "Hidden due to one of your filters",
|
||||
"status.quote_error.not_available": "Post unavailable",
|
||||
"status.quote_error.pending_approval": "Post pending",
|
||||
"status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
|
||||
"status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
|
||||
"status.quote_error.pending_approval_popout.body": "On Mastodon, you can control whether someone can quote you. This post is pending while we're getting the original author's approval.",
|
||||
"status.quote_error.revoked": "Post removed by author",
|
||||
"status.quote_followers_only": "Only followers can quote this post",
|
||||
"status.quote_manual_review": "Author will manually review",
|
||||
"status.quote_policy_change": "Change who can quote",
|
||||
@@ -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