mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(reply-modal): align offline warning with post form
This commit is contained in:
@@ -11,7 +11,6 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
|
||||
const testState = vi.hoisted(() => ({
|
||||
account: { author: { displayName: 'Alice' } } as { author?: { displayName?: string } },
|
||||
closeModalMock: vi.fn(),
|
||||
currentTime: 10_000,
|
||||
directoryByAddress: {
|
||||
'music-posting.eth': {
|
||||
address: 'music-posting.eth',
|
||||
@@ -21,6 +20,9 @@ const testState = vi.hoisted(() => ({
|
||||
handleUploadMock: vi.fn(),
|
||||
isMobile: false,
|
||||
isUploading: false,
|
||||
offlineTitle: '' as string | false,
|
||||
offlineStatusLoading: false,
|
||||
offlineWarningVisible: false,
|
||||
openEmpty: false,
|
||||
publishReplyMock: vi.fn(),
|
||||
quoteInsertNumber: undefined as number | undefined,
|
||||
@@ -32,8 +34,12 @@ const testState = vi.hoisted(() => ({
|
||||
setAccountMock: vi.fn(),
|
||||
setPublishReplyOptionsMock: vi.fn(),
|
||||
springStartMock: vi.fn(),
|
||||
subplebbits: {
|
||||
'music-posting.eth': {
|
||||
address: 'music-posting.eth',
|
||||
},
|
||||
} as Record<string, { address: string }>,
|
||||
showUploadControls: true,
|
||||
updatedAt: undefined as number | undefined,
|
||||
uploadComplete: undefined as ((url: string) => void) | undefined,
|
||||
uploadedFileName: null as string | null,
|
||||
uploadMode: 'always',
|
||||
@@ -63,8 +69,19 @@ vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
|
||||
useAccount: () => testState.account,
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/use-stable-subplebbit', () => ({
|
||||
useSubplebbitField: () => testState.updatedAt,
|
||||
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits', () => ({
|
||||
default: <T,>(selector: (state: { subplebbits: typeof testState.subplebbits }) => T) =>
|
||||
selector({
|
||||
subplebbits: testState.subplebbits,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/use-is-subplebbit-offline', () => ({
|
||||
default: () => ({
|
||||
isOffline: testState.offlineWarningVisible,
|
||||
isOnlineStatusLoading: testState.offlineStatusLoading,
|
||||
offlineTitle: testState.offlineTitle,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/use-selected-text-store', () => ({
|
||||
@@ -114,10 +131,6 @@ vi.mock('../../../hooks/use-is-mobile', () => ({
|
||||
default: () => testState.isMobile,
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/use-current-time', () => ({
|
||||
useCurrentTime: () => testState.currentTime,
|
||||
}));
|
||||
|
||||
vi.mock('../../../hooks/use-file-upload', () => ({
|
||||
useFileUpload: ({ onUploadComplete }: { onUploadComplete: (url: string) => void }) => {
|
||||
testState.uploadComplete = onUploadComplete;
|
||||
@@ -160,10 +173,6 @@ vi.mock('@use-gesture/react', () => ({
|
||||
useDrag: () => () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/utils/time-utils', () => ({
|
||||
getFormattedTimeAgo: (time: number) => `ago:${time}`,
|
||||
}));
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
@@ -224,7 +233,6 @@ describe('ReplyModal', () => {
|
||||
vi.clearAllMocks();
|
||||
testState.account = { author: { displayName: 'Alice' } };
|
||||
testState.closeModalMock.mockReset();
|
||||
testState.currentTime = 10_000;
|
||||
testState.directoryByAddress = {
|
||||
'music-posting.eth': {
|
||||
address: 'music-posting.eth',
|
||||
@@ -234,6 +242,9 @@ describe('ReplyModal', () => {
|
||||
testState.handleUploadMock.mockReset();
|
||||
testState.isMobile = false;
|
||||
testState.isUploading = false;
|
||||
testState.offlineTitle = '';
|
||||
testState.offlineStatusLoading = false;
|
||||
testState.offlineWarningVisible = false;
|
||||
testState.openEmpty = false;
|
||||
testState.publishReplyMock.mockReset();
|
||||
testState.quoteInsertNumber = undefined;
|
||||
@@ -245,8 +256,12 @@ describe('ReplyModal', () => {
|
||||
testState.setAccountMock.mockReset();
|
||||
testState.setPublishReplyOptionsMock.mockReset();
|
||||
testState.springStartMock.mockReset();
|
||||
testState.subplebbits = {
|
||||
'music-posting.eth': {
|
||||
address: 'music-posting.eth',
|
||||
},
|
||||
};
|
||||
testState.showUploadControls = true;
|
||||
testState.updatedAt = undefined;
|
||||
testState.uploadComplete = undefined;
|
||||
testState.uploadedFileName = null;
|
||||
testState.uploadMode = 'always';
|
||||
@@ -260,8 +275,9 @@ describe('ReplyModal', () => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it('initializes quoted content, display name, upload controls, and offline warning on board routes', async () => {
|
||||
testState.updatedAt = 1_000;
|
||||
it('initializes quoted content, display name, upload controls, and shared offline warning on board routes', async () => {
|
||||
testState.offlineTitle = 'posts_last_synced_info:{"time":"ago:1000"}';
|
||||
testState.offlineWarningVisible = true;
|
||||
|
||||
await renderReplyModal('/mu/thread/post-1');
|
||||
|
||||
@@ -274,12 +290,18 @@ describe('ReplyModal', () => {
|
||||
expect(textarea?.value).toBe('>>42\nselected text');
|
||||
expect(container.textContent).toContain('choose_file');
|
||||
expect(container.textContent).toContain('Spoiler?');
|
||||
expect(container.textContent).toContain('warning');
|
||||
expect(container.textContent).toContain('posts_last_synced_info:{"time":"ago:1000"}');
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: '>>42\nselected text' });
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' });
|
||||
});
|
||||
|
||||
it('does not render an offline warning when the shared offline hook reports the board as online', async () => {
|
||||
await renderReplyModal('/mu/thread/post-1');
|
||||
|
||||
expect(container.querySelector('[class*="offlineBoard"]')).toBeNull();
|
||||
expect(container.textContent).not.toContain('subplebbit_offline_info');
|
||||
});
|
||||
|
||||
it('validates empty and invalid replies, then publishes once the payload is valid', async () => {
|
||||
testState.openEmpty = true;
|
||||
testState.selectedText = '';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setAccount, useAccount } from '@bitsocialhq/bitsocial-react-hooks';
|
||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
||||
import useSubplebbitsStore from '@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import useSelectedTextStore from '../../stores/use-selected-text-store';
|
||||
@@ -12,8 +11,8 @@ import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/sho
|
||||
import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
||||
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||
import styles from './reply-modal.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -32,6 +31,17 @@ interface ReplyModalProps {
|
||||
subplebbitAddress: string;
|
||||
}
|
||||
|
||||
const ReplyModalOfflineAlert = ({ hidden, subplebbitAddress }: { hidden: boolean; subplebbitAddress: string }) => {
|
||||
const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]);
|
||||
const { isOffline, isOnlineStatusLoading, offlineTitle } = useIsSubplebbitOffline(subplebbit);
|
||||
|
||||
if (hidden || (!isOffline && !isOnlineStatusLoading)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={styles.offlineBoard}>{offlineTitle}</div>;
|
||||
};
|
||||
|
||||
const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threadNumber, postCid, scrollY, subplebbitAddress }: ReplyModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
@@ -152,18 +162,6 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
}
|
||||
}, [parentCid]);
|
||||
|
||||
const currentTime = useCurrentTime();
|
||||
// Only subscribe to updatedAt to avoid rerenders from updatingState changes
|
||||
const updatedAt = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.updatedAt);
|
||||
const isBoardOffline = updatedAt && updatedAt < currentTime - 60 * 60;
|
||||
const offlineAlert = updatedAt
|
||||
? isBoardOffline && (
|
||||
<div className={styles.offlineBoard}>
|
||||
{t('warning')}: <Trans i18nKey='posts_last_synced_info' values={{ time: getFormattedTimeAgo(updatedAt) }} />
|
||||
</div>
|
||||
)
|
||||
: t('subplebbit_offline_info');
|
||||
|
||||
useEffect(() => {
|
||||
if (showReplyModal && !isMobile) {
|
||||
setTimeout(() => {
|
||||
@@ -378,7 +376,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
</button>
|
||||
</div>
|
||||
{lengthError ? <div className={styles.error}>{lengthError}</div> : error && <div className={styles.error}>{error}</div>}
|
||||
{!(isInAllView || isInSubscriptionsView) && offlineAlert}
|
||||
<ReplyModalOfflineAlert hidden={isInAllView || isInSubscriptionsView} subplebbitAddress={subplebbitAddress} />
|
||||
</div>
|
||||
</animated.div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user