Change paste-link-to-quote loading state from generic loading bar to compose placeholder (#36695)
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import type { ApiQuotePolicy } from '../api_types/quotes';
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
import type { Status, StatusVisibility } from '../models/status';
|
import type { Status, StatusVisibility } from '../models/status';
|
||||||
|
import type { RootState } from '../store';
|
||||||
|
|
||||||
import { showAlert } from './alerts';
|
import { showAlert } from './alerts';
|
||||||
import { changeCompose, focusCompose } from './compose';
|
import { changeCompose, focusCompose } from './compose';
|
||||||
@@ -212,6 +213,17 @@ export const quoteComposeById = createAppThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const composeStateForbidsLink = (composeState: RootState['compose']) => {
|
||||||
|
return (
|
||||||
|
composeState.get('quoted_status_id') ||
|
||||||
|
composeState.get('is_submitting') ||
|
||||||
|
composeState.get('poll') ||
|
||||||
|
composeState.get('is_uploading') ||
|
||||||
|
composeState.get('id') ||
|
||||||
|
composeState.get('privacy') === 'direct'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const pasteLinkCompose = createDataLoadingThunk(
|
export const pasteLinkCompose = createDataLoadingThunk(
|
||||||
'compose/pasteLink',
|
'compose/pasteLink',
|
||||||
async ({ url }: { url: string }) => {
|
async ({ url }: { url: string }) => {
|
||||||
@@ -222,16 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
|||||||
limit: 2,
|
limit: 2,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(data, { dispatch, getState }) => {
|
(data, { dispatch, getState, requestId }) => {
|
||||||
const composeState = getState().compose;
|
const composeState = getState().compose;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
composeState.get('quoted_status_id') ||
|
composeStateForbidsLink(composeState) ||
|
||||||
composeState.get('is_submitting') ||
|
composeState.get('fetching_link') !== requestId // Request has been cancelled
|
||||||
composeState.get('poll') ||
|
|
||||||
composeState.get('is_uploading') ||
|
|
||||||
composeState.get('id') ||
|
|
||||||
composeState.get('privacy') === 'direct'
|
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -247,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
|||||||
dispatch(quoteComposeById(data.statuses[0].id));
|
dispatch(quoteComposeById(data.statuses[0].id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().compose.get('fetching_link') &&
|
||||||
|
!composeStateForbidsLink(getState().compose),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ideally this would cancel the action and the HTTP request, but this is good enough
|
||||||
|
export const cancelPasteLinkCompose = createAction(
|
||||||
|
'compose/cancelPasteLinkCompose',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { cancelPasteLinkCompose } from '@/mastodon/actions/compose_typed';
|
||||||
|
import { useAppDispatch } from '@/mastodon/store';
|
||||||
|
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||||
|
import { DisplayName } from 'mastodon/components/display_name';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
quote_cancel: { id: 'status.quote.cancel', defaultMessage: 'Cancel quote' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuotePlaceholder: FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleQuoteCancel = useCallback(() => {
|
||||||
|
dispatch(cancelPasteLinkCompose());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='status__quote'>
|
||||||
|
<div className='status'>
|
||||||
|
<div className='status__info'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
<Skeleton width='32px' height='32px' />
|
||||||
|
</div>
|
||||||
|
<div className='status__display-name'>
|
||||||
|
<DisplayName />
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleQuoteCancel}
|
||||||
|
className='status__quote-cancel'
|
||||||
|
title={intl.formatMessage(messages.quote_cancel)}
|
||||||
|
icon='cancel-fill'
|
||||||
|
iconComponent={CancelFillIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='status__content'>
|
||||||
|
<Skeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,11 +7,17 @@ import { quoteComposeCancel } from '@/mastodon/actions/compose_typed';
|
|||||||
import { QuotedStatus } from '@/mastodon/components/status_quoted';
|
import { QuotedStatus } from '@/mastodon/components/status_quoted';
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
|
import { QuotePlaceholder } from './quote_placeholder';
|
||||||
|
|
||||||
export const ComposeQuotedStatus: FC = () => {
|
export const ComposeQuotedStatus: FC = () => {
|
||||||
const quotedStatusId = useAppSelector(
|
const quotedStatusId = useAppSelector(
|
||||||
(state) => state.compose.get('quoted_status_id') as string | null,
|
(state) => state.compose.get('quoted_status_id') as string | null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFetchingLink = useAppSelector(
|
||||||
|
(state) => !!state.compose.get('fetching_link'),
|
||||||
|
);
|
||||||
|
|
||||||
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
||||||
|
|
||||||
const quote = useMemo(
|
const quote = useMemo(
|
||||||
@@ -30,7 +36,9 @@ export const ComposeQuotedStatus: FC = () => {
|
|||||||
dispatch(quoteComposeCancel());
|
dispatch(quoteComposeCancel());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
if (!quote) {
|
if (isFetchingLink && !quote) {
|
||||||
|
return <QuotePlaceholder />;
|
||||||
|
} else if (!quote) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
quoteCompose,
|
quoteCompose,
|
||||||
quoteComposeCancel,
|
quoteComposeCancel,
|
||||||
setComposeQuotePolicy,
|
setComposeQuotePolicy,
|
||||||
|
pasteLinkCompose,
|
||||||
|
cancelPasteLinkCompose,
|
||||||
} from '@/mastodon/actions/compose_typed';
|
} from '@/mastodon/actions/compose_typed';
|
||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||||
|
|
||||||
@@ -93,6 +95,7 @@ const initialState = ImmutableMap({
|
|||||||
quoted_status_id: null,
|
quoted_status_id: null,
|
||||||
quote_policy: 'public',
|
quote_policy: 'public',
|
||||||
default_quote_policy: 'public', // Set in hydration.
|
default_quote_policy: 'public', // Set in hydration.
|
||||||
|
fetching_link: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialPoll = ImmutableMap({
|
const initialPoll = ImmutableMap({
|
||||||
@@ -350,6 +353,12 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
return state.set('quoted_status_id', null);
|
return state.set('quoted_status_id', null);
|
||||||
} else if (setComposeQuotePolicy.match(action)) {
|
} else if (setComposeQuotePolicy.match(action)) {
|
||||||
return state.set('quote_policy', action.payload);
|
return state.set('quote_policy', action.payload);
|
||||||
|
} else if (pasteLinkCompose.pending.match(action)) {
|
||||||
|
return state.set('fetching_link', action.meta.requestId);
|
||||||
|
} else if (pasteLinkCompose.fulfilled.match(action) || pasteLinkCompose.rejected.match(action)) {
|
||||||
|
return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state;
|
||||||
|
} else if (cancelPasteLinkCompose.match(action)) {
|
||||||
|
return state.set('fetching_link', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ interface AppThunkConfig {
|
|||||||
}
|
}
|
||||||
export type AppThunkApi = Pick<
|
export type AppThunkApi = Pick<
|
||||||
GetThunkAPI<AppThunkConfig>,
|
GetThunkAPI<AppThunkConfig>,
|
||||||
'getState' | 'dispatch'
|
'getState' | 'dispatch' | 'requestId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
interface AppThunkOptions<Arg> {
|
interface AppThunkOptions<Arg> {
|
||||||
@@ -60,7 +60,7 @@ type AppThunk<Arg = void, Returned = void> = (
|
|||||||
|
|
||||||
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
|
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
|
||||||
arg: Arg,
|
arg: Arg,
|
||||||
api: AppThunkApi,
|
api: Pick<AppThunkApi, 'getState' | 'dispatch'>,
|
||||||
extra?: ExtraArg,
|
extra?: ExtraArg,
|
||||||
) => Returned;
|
) => Returned;
|
||||||
|
|
||||||
@@ -143,10 +143,10 @@ export function createAsyncThunk<Arg = void, Returned = void>(
|
|||||||
name,
|
name,
|
||||||
async (
|
async (
|
||||||
arg: Arg,
|
arg: Arg,
|
||||||
{ getState, dispatch, fulfillWithValue, rejectWithValue },
|
{ getState, dispatch, requestId, fulfillWithValue, rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const result = await creator(arg, { dispatch, getState });
|
const result = await creator(arg, { dispatch, getState, requestId });
|
||||||
|
|
||||||
return fulfillWithValue(result, {
|
return fulfillWithValue(result, {
|
||||||
useLoadingBar: options.useLoadingBar,
|
useLoadingBar: options.useLoadingBar,
|
||||||
@@ -280,10 +280,11 @@ export function createDataLoadingThunk<
|
|||||||
|
|
||||||
return createAsyncThunk<Args, Returned>(
|
return createAsyncThunk<Args, Returned>(
|
||||||
name,
|
name,
|
||||||
async (arg, { getState, dispatch }) => {
|
async (arg, { getState, dispatch, requestId }) => {
|
||||||
const data = await loadData(arg, {
|
const data = await loadData(arg, {
|
||||||
dispatch,
|
dispatch,
|
||||||
getState,
|
getState,
|
||||||
|
requestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!onData) return data as Returned;
|
if (!onData) return data as Returned;
|
||||||
@@ -291,6 +292,7 @@ export function createDataLoadingThunk<
|
|||||||
const result = await onData(data, {
|
const result = await onData(data, {
|
||||||
dispatch,
|
dispatch,
|
||||||
getState,
|
getState,
|
||||||
|
requestId,
|
||||||
discardLoadData: discardLoadDataInPayload,
|
discardLoadData: discardLoadDataInPayload,
|
||||||
actionArg: arg,
|
actionArg: arg,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user