[Glitch] Implement new design for "Refetch all"

Port 3a81ee8f5b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2025-09-24 11:54:07 +02:00
committed by Claire
parent 7430d399b5
commit ea3f6ce2e5
6 changed files with 247 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { useIntl, defineMessages } from 'react-intl';
import {
fetchContext,
@@ -8,31 +8,80 @@ import {
} from 'flavours/glitch/actions/statuses';
import type { AsyncRefreshHeader } from 'flavours/glitch/api';
import { apiGetAsyncRefresh } from 'flavours/glitch/api/async_refreshes';
import { Alert } from 'flavours/glitch/components/alert';
import { ExitAnimationWrapper } from 'flavours/glitch/components/exit_animation_wrapper';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
const AnimatedAlert: React.FC<
React.ComponentPropsWithoutRef<typeof Alert> & { withEntryDelay?: boolean }
> = ({ isActive = false, withEntryDelay, ...props }) => (
<ExitAnimationWrapper withEntryDelay isActive={isActive}>
{(delayedIsActive) => <Alert isActive={delayedIsActive} {...props} />}
</ExitAnimationWrapper>
);
const messages = defineMessages({
loading: {
moreFound: {
id: 'status.context.more_replies_found',
defaultMessage: 'More replies found',
},
show: {
id: 'status.context.show',
defaultMessage: 'Show',
},
loadingInitial: {
id: 'status.context.loading',
defaultMessage: 'Checking for more replies',
defaultMessage: 'Loading',
},
loadingMore: {
id: 'status.context.loading_more',
defaultMessage: 'Loading more replies',
},
success: {
id: 'status.context.loading_success',
defaultMessage: 'All replies loaded',
},
error: {
id: 'status.context.loading_error',
defaultMessage: "Couldn't load new replies",
},
retry: {
id: 'status.context.retry',
defaultMessage: 'Retry',
},
});
type LoadingState =
| 'idle'
| 'more-available'
| 'loading-initial'
| 'loading-more'
| 'success'
| 'error';
export const RefreshController: React.FC<{
statusId: string;
}> = ({ statusId }) => {
const refresh = useAppSelector(
(state) => state.contexts.refreshing[statusId],
);
const autoRefresh = useAppSelector(
(state) =>
!state.contexts.replies[statusId] ||
state.contexts.replies[statusId].length === 0,
const currentReplyCount = useAppSelector(
(state) => state.contexts.replies[statusId]?.length ?? 0,
);
const autoRefresh = !currentReplyCount;
const dispatch = useAppDispatch();
const intl = useIntl();
const [ready, setReady] = useState(false);
const [loading, setLoading] = useState(false);
const [loadingState, setLoadingState] = useState<LoadingState>(
refresh && autoRefresh ? 'loading-initial' : 'idle',
);
const [wasDismissed, setWasDismissed] = useState(false);
const dismissPrompt = useCallback(() => {
setWasDismissed(true);
setLoadingState('idle');
}, []);
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>;
@@ -45,67 +94,104 @@ export const RefreshController: React.FC<{
if (result.async_refresh.result_count > 0) {
if (autoRefresh) {
void dispatch(fetchContext({ statusId }));
return '';
void dispatch(fetchContext({ statusId })).then(() => {
setLoadingState('idle');
});
} else {
setLoadingState('more-available');
}
setReady(true);
} else {
setLoadingState('idle');
}
} else {
scheduleRefresh(refresh);
}
return '';
});
}, refresh.retry * 1000);
};
if (refresh) {
if (refresh && !wasDismissed) {
scheduleRefresh(refresh);
setLoadingState('loading-initial');
}
return () => {
clearTimeout(timeoutId);
};
}, [dispatch, setReady, statusId, refresh, autoRefresh]);
}, [dispatch, statusId, refresh, autoRefresh, wasDismissed]);
useEffect(() => {
// Hide success message after a short delay
if (loadingState === 'success') {
const timeoutId = setTimeout(() => {
setLoadingState('idle');
}, 3000);
return () => {
clearTimeout(timeoutId);
};
}
return () => '';
}, [loadingState]);
const handleClick = useCallback(() => {
setLoading(true);
setReady(false);
setLoadingState('loading-more');
dispatch(fetchContext({ statusId }))
.then(() => {
setLoading(false);
setLoadingState('success');
return '';
})
.catch(() => {
setLoading(false);
setLoadingState('error');
});
}, [dispatch, setReady, statusId]);
}, [dispatch, statusId]);
if (ready && !loading) {
if (loadingState === 'loading-initial') {
return (
<button className='load-more load-gap' onClick={handleClick}>
<FormattedMessage
id='status.context.load_new_replies'
defaultMessage='New replies available'
/>
</button>
<div
className='load-more load-gap'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loadingInitial)}
>
<LoadingIndicator />
</div>
);
}
if (!refresh && !loading) {
return null;
}
return (
<div
className='load-more load-gap'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loading)}
>
<LoadingIndicator />
<div className='column__alert' role='status' aria-live='polite'>
<AnimatedAlert
isActive={loadingState === 'more-available'}
message={intl.formatMessage(messages.moreFound)}
action={intl.formatMessage(messages.show)}
onActionClick={handleClick}
onDismiss={dismissPrompt}
animateFrom='below'
/>
<AnimatedAlert
isLoading
withEntryDelay
isActive={loadingState === 'loading-more'}
message={intl.formatMessage(messages.loadingMore)}
animateFrom='below'
/>
<AnimatedAlert
withEntryDelay
isActive={loadingState === 'error'}
message={intl.formatMessage(messages.error)}
action={intl.formatMessage(messages.retry)}
onActionClick={handleClick}
onDismiss={dismissPrompt}
animateFrom='below'
/>
<AnimatedAlert
withEntryDelay
isActive={loadingState === 'success'}
message={intl.formatMessage(messages.success)}
animateFrom='below'
/>
</div>
);
};