diff --git a/src/components/reply-modal/__tests__/reply-modal.test.tsx b/src/components/reply-modal/__tests__/reply-modal.test.tsx index 18659913..5a0df9e9 100644 --- a/src/components/reply-modal/__tests__/reply-modal.test.tsx +++ b/src/components/reply-modal/__tests__/reply-modal.test.tsx @@ -11,7 +11,6 @@ const act = (React as { act?: (cb: () => void | Promise) => 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, 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: (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 = ''; diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index 3c462698..2ad976a6 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -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
{offlineTitle}
; +}; + 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 && ( -
- {t('warning')}: -
- ) - : t('subplebbit_offline_info'); - useEffect(() => { if (showReplyModal && !isMobile) { setTimeout(() => { @@ -378,7 +376,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa {lengthError ?
{lengthError}
: error &&
{error}
} - {!(isInAllView || isInSubscriptionsView) && offlineAlert} +