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(() => ({
|
const testState = vi.hoisted(() => ({
|
||||||
account: { author: { displayName: 'Alice' } } as { author?: { displayName?: string } },
|
account: { author: { displayName: 'Alice' } } as { author?: { displayName?: string } },
|
||||||
closeModalMock: vi.fn(),
|
closeModalMock: vi.fn(),
|
||||||
currentTime: 10_000,
|
|
||||||
directoryByAddress: {
|
directoryByAddress: {
|
||||||
'music-posting.eth': {
|
'music-posting.eth': {
|
||||||
address: 'music-posting.eth',
|
address: 'music-posting.eth',
|
||||||
@@ -21,6 +20,9 @@ const testState = vi.hoisted(() => ({
|
|||||||
handleUploadMock: vi.fn(),
|
handleUploadMock: vi.fn(),
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
|
offlineTitle: '' as string | false,
|
||||||
|
offlineStatusLoading: false,
|
||||||
|
offlineWarningVisible: false,
|
||||||
openEmpty: false,
|
openEmpty: false,
|
||||||
publishReplyMock: vi.fn(),
|
publishReplyMock: vi.fn(),
|
||||||
quoteInsertNumber: undefined as number | undefined,
|
quoteInsertNumber: undefined as number | undefined,
|
||||||
@@ -32,8 +34,12 @@ const testState = vi.hoisted(() => ({
|
|||||||
setAccountMock: vi.fn(),
|
setAccountMock: vi.fn(),
|
||||||
setPublishReplyOptionsMock: vi.fn(),
|
setPublishReplyOptionsMock: vi.fn(),
|
||||||
springStartMock: vi.fn(),
|
springStartMock: vi.fn(),
|
||||||
|
subplebbits: {
|
||||||
|
'music-posting.eth': {
|
||||||
|
address: 'music-posting.eth',
|
||||||
|
},
|
||||||
|
} as Record<string, { address: string }>,
|
||||||
showUploadControls: true,
|
showUploadControls: true,
|
||||||
updatedAt: undefined as number | undefined,
|
|
||||||
uploadComplete: undefined as ((url: string) => void) | undefined,
|
uploadComplete: undefined as ((url: string) => void) | undefined,
|
||||||
uploadedFileName: null as string | null,
|
uploadedFileName: null as string | null,
|
||||||
uploadMode: 'always',
|
uploadMode: 'always',
|
||||||
@@ -63,8 +69,19 @@ vi.mock('@bitsocialhq/bitsocial-react-hooks', () => ({
|
|||||||
useAccount: () => testState.account,
|
useAccount: () => testState.account,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../hooks/use-stable-subplebbit', () => ({
|
vi.mock('@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits', () => ({
|
||||||
useSubplebbitField: () => testState.updatedAt,
|
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', () => ({
|
vi.mock('../../../stores/use-selected-text-store', () => ({
|
||||||
@@ -114,10 +131,6 @@ vi.mock('../../../hooks/use-is-mobile', () => ({
|
|||||||
default: () => testState.isMobile,
|
default: () => testState.isMobile,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../hooks/use-current-time', () => ({
|
|
||||||
useCurrentTime: () => testState.currentTime,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../../../hooks/use-file-upload', () => ({
|
vi.mock('../../../hooks/use-file-upload', () => ({
|
||||||
useFileUpload: ({ onUploadComplete }: { onUploadComplete: (url: string) => void }) => {
|
useFileUpload: ({ onUploadComplete }: { onUploadComplete: (url: string) => void }) => {
|
||||||
testState.uploadComplete = onUploadComplete;
|
testState.uploadComplete = onUploadComplete;
|
||||||
@@ -160,10 +173,6 @@ vi.mock('@use-gesture/react', () => ({
|
|||||||
useDrag: () => () => ({}),
|
useDrag: () => () => ({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../lib/utils/time-utils', () => ({
|
|
||||||
getFormattedTimeAgo: (time: number) => `ago:${time}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let root: Root;
|
let root: Root;
|
||||||
|
|
||||||
@@ -224,7 +233,6 @@ describe('ReplyModal', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
testState.account = { author: { displayName: 'Alice' } };
|
testState.account = { author: { displayName: 'Alice' } };
|
||||||
testState.closeModalMock.mockReset();
|
testState.closeModalMock.mockReset();
|
||||||
testState.currentTime = 10_000;
|
|
||||||
testState.directoryByAddress = {
|
testState.directoryByAddress = {
|
||||||
'music-posting.eth': {
|
'music-posting.eth': {
|
||||||
address: 'music-posting.eth',
|
address: 'music-posting.eth',
|
||||||
@@ -234,6 +242,9 @@ describe('ReplyModal', () => {
|
|||||||
testState.handleUploadMock.mockReset();
|
testState.handleUploadMock.mockReset();
|
||||||
testState.isMobile = false;
|
testState.isMobile = false;
|
||||||
testState.isUploading = false;
|
testState.isUploading = false;
|
||||||
|
testState.offlineTitle = '';
|
||||||
|
testState.offlineStatusLoading = false;
|
||||||
|
testState.offlineWarningVisible = false;
|
||||||
testState.openEmpty = false;
|
testState.openEmpty = false;
|
||||||
testState.publishReplyMock.mockReset();
|
testState.publishReplyMock.mockReset();
|
||||||
testState.quoteInsertNumber = undefined;
|
testState.quoteInsertNumber = undefined;
|
||||||
@@ -245,8 +256,12 @@ describe('ReplyModal', () => {
|
|||||||
testState.setAccountMock.mockReset();
|
testState.setAccountMock.mockReset();
|
||||||
testState.setPublishReplyOptionsMock.mockReset();
|
testState.setPublishReplyOptionsMock.mockReset();
|
||||||
testState.springStartMock.mockReset();
|
testState.springStartMock.mockReset();
|
||||||
|
testState.subplebbits = {
|
||||||
|
'music-posting.eth': {
|
||||||
|
address: 'music-posting.eth',
|
||||||
|
},
|
||||||
|
};
|
||||||
testState.showUploadControls = true;
|
testState.showUploadControls = true;
|
||||||
testState.updatedAt = undefined;
|
|
||||||
testState.uploadComplete = undefined;
|
testState.uploadComplete = undefined;
|
||||||
testState.uploadedFileName = null;
|
testState.uploadedFileName = null;
|
||||||
testState.uploadMode = 'always';
|
testState.uploadMode = 'always';
|
||||||
@@ -260,8 +275,9 @@ describe('ReplyModal', () => {
|
|||||||
container.remove();
|
container.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('initializes quoted content, display name, upload controls, and offline warning on board routes', async () => {
|
it('initializes quoted content, display name, upload controls, and shared offline warning on board routes', async () => {
|
||||||
testState.updatedAt = 1_000;
|
testState.offlineTitle = 'posts_last_synced_info:{"time":"ago:1000"}';
|
||||||
|
testState.offlineWarningVisible = true;
|
||||||
|
|
||||||
await renderReplyModal('/mu/thread/post-1');
|
await renderReplyModal('/mu/thread/post-1');
|
||||||
|
|
||||||
@@ -274,12 +290,18 @@ describe('ReplyModal', () => {
|
|||||||
expect(textarea?.value).toBe('>>42\nselected text');
|
expect(textarea?.value).toBe('>>42\nselected text');
|
||||||
expect(container.textContent).toContain('choose_file');
|
expect(container.textContent).toContain('choose_file');
|
||||||
expect(container.textContent).toContain('Spoiler?');
|
expect(container.textContent).toContain('Spoiler?');
|
||||||
expect(container.textContent).toContain('warning');
|
|
||||||
expect(container.textContent).toContain('posts_last_synced_info:{"time":"ago:1000"}');
|
expect(container.textContent).toContain('posts_last_synced_info:{"time":"ago:1000"}');
|
||||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: '>>42\nselected text' });
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ content: '>>42\nselected text' });
|
||||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({ displayName: 'Alice' });
|
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 () => {
|
it('validates empty and invalid replies, then publishes once the payload is valid', async () => {
|
||||||
testState.openEmpty = true;
|
testState.openEmpty = true;
|
||||||
testState.selectedText = '';
|
testState.selectedText = '';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
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 { setAccount, useAccount } from '@bitsocialhq/bitsocial-react-hooks';
|
||||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
import useSubplebbitsStore from '@bitsocialhq/bitsocial-react-hooks/dist/stores/subplebbits';
|
||||||
import { getFormattedTimeAgo } from '../../lib/utils/time-utils';
|
|
||||||
import { isValidURL } from '../../lib/utils/url-utils';
|
import { isValidURL } from '../../lib/utils/url-utils';
|
||||||
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||||
import useSelectedTextStore from '../../stores/use-selected-text-store';
|
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 useMediaHostingStore from '../../stores/use-media-hosting-store';
|
||||||
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
||||||
import usePublishReply from '../../hooks/use-publish-reply';
|
import usePublishReply from '../../hooks/use-publish-reply';
|
||||||
|
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import { useCurrentTime } from '../../hooks/use-current-time';
|
|
||||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||||
import styles from './reply-modal.module.css';
|
import styles from './reply-modal.module.css';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
@@ -32,6 +31,17 @@ interface ReplyModalProps {
|
|||||||
subplebbitAddress: string;
|
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 ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threadNumber, postCid, scrollY, subplebbitAddress }: ReplyModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -152,18 +162,6 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
}
|
}
|
||||||
}, [parentCid]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (showReplyModal && !isMobile) {
|
if (showReplyModal && !isMobile) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -378,7 +376,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{lengthError ? <div className={styles.error}>{lengthError}</div> : error && <div className={styles.error}>{error}</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>
|
</div>
|
||||||
</animated.div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user