mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
refactor: migrate 5chan to the community hooks API (#1073)
* refactor(community-api): migrate 5chan to community hooks * fix(review): address PR feedback * fix(review): preserve legacy board context fallbacks * fix(review): address latest bot feedback * fix(review): use communityAddress in edit menu privileges
This commit is contained in:
@@ -25,7 +25,7 @@ const testState = vi.hoisted(() => ({
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
||||
useAccount: () => testState.account,
|
||||
useAccountSubplebbits: () => ({ accountSubplebbits: testState.accountSubplebbits }),
|
||||
useAccountCommunities: () => ({ accountCommunities: testState.accountSubplebbits }),
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks/dist/lib/utils', () => ({
|
||||
@@ -37,8 +37,8 @@ vi.mock('../use-directories', () => ({
|
||||
useDirectoryByAddress: (address: string | undefined) => (address ? testState.directoryLookup[address] : undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../use-stable-subplebbit', () => ({
|
||||
useSubplebbitField: (_address: string | undefined, selector: (subplebbit: unknown) => unknown) => selector(testState.subplebbitSnapshot),
|
||||
vi.mock('../use-stable-community', () => ({
|
||||
useCommunityField: (_address: string | undefined, selector: (community: unknown) => unknown) => selector(testState.subplebbitSnapshot),
|
||||
}));
|
||||
|
||||
let latestValue: unknown;
|
||||
|
||||
@@ -26,15 +26,15 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../stores/use-subplebbit-offline-store', () => ({
|
||||
vi.mock('../../stores/use-community-offline-store', () => ({
|
||||
default: () => ({
|
||||
initializesubplebbitOfflineState: testState.initializeMock,
|
||||
setSubplebbitOfflineState: testState.setOfflineStateMock,
|
||||
subplebbitOfflineState: testState.subplebbitOfflineState,
|
||||
initializeCommunityOfflineState: testState.initializeMock,
|
||||
setCommunityOfflineState: testState.setOfflineStateMock,
|
||||
communityOfflineState: testState.subplebbitOfflineState,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../stores/use-subplebbits-loading-start-timestamps-store', () => ({
|
||||
vi.mock('../../stores/use-communities-loading-start-timestamps-store', () => ({
|
||||
default: (addresses?: string[]) => {
|
||||
testState.requestedAddresses = addresses;
|
||||
return testState.loadingTimestamps;
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock('../use-current-time', () => ({
|
||||
useCurrentTime: () => testState.currentTime,
|
||||
}));
|
||||
|
||||
vi.mock('../../stores/use-subplebbits-loading-start-timestamps-store', () => ({
|
||||
vi.mock('../../stores/use-communities-loading-start-timestamps-store', () => ({
|
||||
default: (addresses?: string[]) => {
|
||||
testState.requestedAddresses = addresses;
|
||||
return testState.loadingTimestamps;
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('usePostPageNumber', () => {
|
||||
testState.feedsOptions = {
|
||||
boardFeed: {
|
||||
sortType: 'active',
|
||||
subplebbitAddresses: ['music.eth'],
|
||||
communityAddresses: ['music.eth'],
|
||||
},
|
||||
};
|
||||
testState.loadedFeeds = {
|
||||
@@ -97,7 +97,7 @@ describe('usePostPageNumber', () => {
|
||||
expect(testState.preloadOptions).toEqual({
|
||||
postsPerPage: 20,
|
||||
sortType: 'active',
|
||||
subplebbitAddresses: ['music.eth'],
|
||||
communityAddresses: ['music.eth'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('usePostPageNumber', () => {
|
||||
expect(testState.preloadOptions).toEqual({
|
||||
postsPerPage: 20,
|
||||
sortType: 'active',
|
||||
subplebbitAddresses: ['music.eth'],
|
||||
communityAddresses: ['music.eth'],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import { useStableSubplebbit, useSubplebbitField } from '../use-stable-subplebbi
|
||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
subplebbits: {} as Record<string, unknown>,
|
||||
communities: {} as Record<string, unknown>,
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks/dist/stores/subplebbits', () => ({
|
||||
default: (selector: (state: { subplebbits: typeof testState.subplebbits }) => unknown) =>
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks/dist/stores/communities', () => ({
|
||||
default: (selector: (state: { communities: typeof testState.communities }) => unknown) =>
|
||||
selector({
|
||||
subplebbits: testState.subplebbits,
|
||||
communities: testState.communities,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('use-stable-subplebbit', () => {
|
||||
beforeEach(() => {
|
||||
latestValue = undefined;
|
||||
renderCount = 0;
|
||||
testState.subplebbits = {};
|
||||
testState.communities = {};
|
||||
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
@@ -53,7 +53,7 @@ describe('use-stable-subplebbit', () => {
|
||||
});
|
||||
|
||||
it('resolves alias board addresses when the store key uses a different suffix', () => {
|
||||
testState.subplebbits = {
|
||||
testState.communities = {
|
||||
'international-sfw.bso': {
|
||||
address: 'international-sfw.bso',
|
||||
roles: {
|
||||
@@ -74,7 +74,7 @@ describe('use-stable-subplebbit', () => {
|
||||
});
|
||||
|
||||
it('prefers an exact key match when both exact and alias variants are present', () => {
|
||||
testState.subplebbits = {
|
||||
testState.communities = {
|
||||
'business.eth': {
|
||||
address: 'business.eth',
|
||||
title: '/biz/ - Exact',
|
||||
|
||||
@@ -9,23 +9,23 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
clientsStates: {} as Record<string, string[]>,
|
||||
subplebbit: undefined as
|
||||
community: undefined as
|
||||
| {
|
||||
publishingState?: string;
|
||||
state?: string;
|
||||
updatingState?: string;
|
||||
}
|
||||
| undefined,
|
||||
subplebbitsStates: {} as Record<string, { clientUrls: string[]; subplebbitAddresses: string[] }>,
|
||||
communitiesStates: {} as Record<string, { clientUrls: string[]; communityAddresses: string[] }>,
|
||||
}));
|
||||
|
||||
vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
||||
useClientsStates: () => ({
|
||||
states: testState.clientsStates,
|
||||
}),
|
||||
useSubplebbit: () => testState.subplebbit,
|
||||
useSubplebbitsStates: () => ({
|
||||
states: testState.subplebbitsStates,
|
||||
useCommunity: () => testState.community,
|
||||
useCommunitiesStates: () => ({
|
||||
states: testState.communitiesStates,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -63,8 +63,8 @@ describe('use-state-string', () => {
|
||||
beforeEach(() => {
|
||||
latestValue = undefined;
|
||||
testState.clientsStates = {};
|
||||
testState.subplebbit = undefined;
|
||||
testState.subplebbitsStates = {};
|
||||
testState.community = undefined;
|
||||
testState.communitiesStates = {};
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
@@ -101,7 +101,7 @@ describe('use-state-string', () => {
|
||||
});
|
||||
|
||||
it('sanitizes single-board feed state strings to board wording', () => {
|
||||
testState.subplebbit = {
|
||||
testState.community = {
|
||||
state: 'updating',
|
||||
updatingState: 'fetching-ipfs',
|
||||
};
|
||||
@@ -114,22 +114,22 @@ describe('use-state-string', () => {
|
||||
});
|
||||
|
||||
it('aggregates multi-board feed states across address resolution, threads, and pages', () => {
|
||||
testState.subplebbitsStates = {
|
||||
testState.communitiesStates = {
|
||||
'fetching-ipfs': {
|
||||
clientUrls: ['https://ipfs.io'],
|
||||
subplebbitAddresses: ['music-posting.eth'],
|
||||
communityAddresses: ['music-posting.eth'],
|
||||
},
|
||||
'fetching-ipns': {
|
||||
clientUrls: ['https://gateway.example.com'],
|
||||
subplebbitAddresses: ['music-posting.eth', 'tech-posting.eth'],
|
||||
communityAddresses: ['music-posting.eth', 'tech-posting.eth'],
|
||||
},
|
||||
'page-1': {
|
||||
clientUrls: ['https://gateway.example.com', 'https://ipfs.io'],
|
||||
subplebbitAddresses: ['music-posting.eth'],
|
||||
communityAddresses: ['music-posting.eth'],
|
||||
},
|
||||
'resolving-address': {
|
||||
clientUrls: ['https://ens.example.com'],
|
||||
subplebbitAddresses: ['music-posting.eth', 'tech-posting.eth'],
|
||||
communityAddresses: ['music-posting.eth', 'tech-posting.eth'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccountCommunities } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import type { DirectoryCommunity } from './use-directories';
|
||||
|
||||
export const useAccountCommunitiesWithMetadata = (): DirectoryCommunity[] => {
|
||||
const { accountCommunities } = useAccountCommunities({ onlyIfCached: true });
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.values(accountCommunities).map((community) => ({
|
||||
address: (community as any).address,
|
||||
title: (community as any).title,
|
||||
})),
|
||||
[accountCommunities],
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccountCommunities } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
|
||||
export const useAccountCommunityAddresses = (): string[] => {
|
||||
const { accountCommunities } = useAccountCommunities({ onlyIfCached: true });
|
||||
|
||||
return useMemo(() => Object.keys(accountCommunities), [accountCommunities]);
|
||||
};
|
||||
@@ -1,8 +1 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccountSubplebbits } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
|
||||
export const useAccountSubplebbitAddresses = (): string[] => {
|
||||
const { accountSubplebbits } = useAccountSubplebbits({ onlyIfCached: true });
|
||||
|
||||
return useMemo(() => Object.keys(accountSubplebbits), [accountSubplebbits]);
|
||||
};
|
||||
export { useAccountCommunityAddresses as useAccountSubplebbitAddresses } from './use-account-community-addresses';
|
||||
|
||||
@@ -1,16 +1 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccountSubplebbits } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { DirectoryCommunity } from './use-directories';
|
||||
|
||||
export const useAccountSubplebbitsWithMetadata = (): DirectoryCommunity[] => {
|
||||
const { accountSubplebbits } = useAccountSubplebbits({ onlyIfCached: true });
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.values(accountSubplebbits).map((sub) => ({
|
||||
address: (sub as any).address,
|
||||
title: (sub as any).title,
|
||||
})),
|
||||
[accountSubplebbits],
|
||||
);
|
||||
};
|
||||
export { useAccountCommunitiesWithMetadata as useAccountSubplebbitsWithMetadata } from './use-account-communities-with-metadata';
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccount } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { useSubplebbitField } from './use-stable-subplebbit';
|
||||
import { useCommunityField } from './use-stable-community';
|
||||
|
||||
interface AuthorPrivilegesProps {
|
||||
commentAuthorAddress: string;
|
||||
subplebbitAddress: string;
|
||||
subplebbitAddress?: string;
|
||||
communityAddress?: string;
|
||||
postCid?: string;
|
||||
}
|
||||
|
||||
const useAuthorPrivileges = ({ commentAuthorAddress, subplebbitAddress }: AuthorPrivilegesProps) => {
|
||||
const useAuthorPrivileges = ({ commentAuthorAddress, subplebbitAddress, communityAddress }: AuthorPrivilegesProps) => {
|
||||
const account = useAccount();
|
||||
const targetAddress = communityAddress ?? subplebbitAddress;
|
||||
const accountAuthorAddress = account?.author?.address;
|
||||
// Only subscribe to roles field to avoid rerenders from updatingState changes
|
||||
const roles = useSubplebbitField(subplebbitAddress, (subplebbit) => subplebbit?.roles);
|
||||
const roles = useCommunityField(targetAddress, (community) => community?.roles);
|
||||
const { isCommentAuthorMod, isAccountMod, isAccountCommentAuthor, commentAuthorRole, accountAuthorRole } = useMemo(() => {
|
||||
const commentAuthorRole = roles?.[commentAuthorAddress]?.role;
|
||||
const isCommentAuthorMod = commentAuthorRole === 'admin' || commentAuthorRole === 'owner' || commentAuthorRole === 'moderator';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useDirectoryByAddress } from './use-directories';
|
||||
import { useSubplebbitField } from './use-stable-subplebbit';
|
||||
import { useCommunityField } from './use-stable-community';
|
||||
|
||||
/**
|
||||
* Prefer authoritative live board metadata when available, but fall back to the
|
||||
* bundled directory entry so known boards can render IDs immediately on first load.
|
||||
*/
|
||||
export const useBoardPseudonymityMode = (subplebbitAddress: string | undefined): string | undefined => {
|
||||
const directory = useDirectoryByAddress(subplebbitAddress);
|
||||
const livePseudonymityMode = useSubplebbitField(subplebbitAddress, (sub) => sub?.features?.pseudonymityMode);
|
||||
export const useBoardPseudonymityMode = (communityAddress: string | undefined): string | undefined => {
|
||||
const directory = useDirectoryByAddress(communityAddress);
|
||||
const livePseudonymityMode = useCommunityField(communityAddress, (community) => community?.features?.pseudonymityMode);
|
||||
|
||||
return livePseudonymityMode ?? directory?.features?.pseudonymityMode;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAccountComments, Subplebbit } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, subplebbit: Subplebbit) => {
|
||||
const { address } = subplebbit || {};
|
||||
import { useAccountComments, type Community } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||
|
||||
const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, community: Community) => {
|
||||
const { address } = community || {};
|
||||
|
||||
const { accountComments } = useAccountComments();
|
||||
|
||||
@@ -14,7 +16,8 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
|
||||
// show account comments instantly in the feed once published (cid defined), instead of waiting for the feed to update
|
||||
const filteredComments = accountComments.filter((comment) => {
|
||||
const { cid, deleted, postCid, removed, state, subplebbitAddress, timestamp } = comment || {};
|
||||
const { cid, deleted, postCid, removed, state, timestamp } = comment || {};
|
||||
const communityAddress = getCommentCommunityAddress(comment);
|
||||
|
||||
return (
|
||||
!deleted &&
|
||||
@@ -23,7 +26,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea
|
||||
state === 'succeeded' &&
|
||||
cid &&
|
||||
cid === postCid &&
|
||||
subplebbitAddress === address &&
|
||||
communityAddress === address &&
|
||||
!_feed.some((feedItem) => feedItem.cid === cid)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { useCommunityStats } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
|
||||
type CommunityStatsState = {
|
||||
communityStats: { [communityAddress: string]: any };
|
||||
setCommunityStats: (communityAddress: string, stats: any) => void;
|
||||
};
|
||||
|
||||
export const useCommunitiesStatsStore = create<CommunityStatsState>((set) => ({
|
||||
communityStats: {},
|
||||
setCommunityStats: (communityAddress, stats) =>
|
||||
set((state) => ({
|
||||
communityStats: { ...state.communityStats, [communityAddress]: stats },
|
||||
})),
|
||||
}));
|
||||
|
||||
export const CommunityStatsCollector = ({ communityAddress }: { communityAddress: string }) => {
|
||||
const stats = useCommunityStats({ communityAddress });
|
||||
const setCommunityStats = useCommunitiesStatsStore((state) => state.setCommunityStats);
|
||||
|
||||
useEffect(() => {
|
||||
if (stats && stats.allPostCount !== undefined) {
|
||||
setCommunityStats(communityAddress, stats);
|
||||
}
|
||||
}, [stats, communityAddress, setCommunityStats]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Back-compat exports for old naming.
|
||||
*/
|
||||
export const useSubplebbitsStatsStore = useCommunitiesStatsStore;
|
||||
export const SubplebbitStatsCollector = CommunityStatsCollector;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { Community } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { getFormattedTimeAgo } from '../lib/utils/time-utils';
|
||||
import useCommunityOfflineStore from '../stores/use-community-offline-store';
|
||||
import useCommunitiesLoadingStartTimestamps from '../stores/use-communities-loading-start-timestamps-store';
|
||||
|
||||
const useIsCommunityOffline = (community?: Community | undefined) => {
|
||||
const { t } = useTranslation();
|
||||
const { address, state, updatedAt, updatingState } = community || {};
|
||||
const { communityOfflineState, setCommunityOfflineState, initializeCommunityOfflineState } = useCommunityOfflineStore();
|
||||
const communitiesLoadingStartTimestamps = useCommunitiesLoadingStartTimestamps([address]);
|
||||
|
||||
useEffect(() => {
|
||||
if (address && !communityOfflineState[address]) {
|
||||
initializeCommunityOfflineState(address);
|
||||
}
|
||||
}, [address, communityOfflineState, initializeCommunityOfflineState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
setCommunityOfflineState(address, { state, updatedAt, updatingState });
|
||||
}
|
||||
}, [address, state, updatedAt, updatingState, setCommunityOfflineState]);
|
||||
|
||||
const offlineState = communityOfflineState[address] || { initialLoad: true };
|
||||
const loadingStartTimestamp = communitiesLoadingStartTimestamps[0] || 0;
|
||||
const isLoading = offlineState.initialLoad && (!updatedAt || Date.now() / 1000 - updatedAt >= 120 * 120) && Date.now() / 1000 - loadingStartTimestamp < 30;
|
||||
const isOffline = !isLoading && ((updatedAt && updatedAt < Date.now() / 1000 - 120 * 120) || (!updatedAt && Date.now() / 1000 - loadingStartTimestamp >= 30));
|
||||
|
||||
const isOnline = updatedAt && Date.now() / 1000 - updatedAt < 120 * 120;
|
||||
const offlineIconClass = isLoading ? 'yellowOfflineIcon' : isOffline ? 'redOfflineIcon' : '';
|
||||
|
||||
const offlineTitle = isLoading
|
||||
? 'downloading board...'
|
||||
: updatedAt
|
||||
? isOffline && t('posts_last_synced_info', { time: getFormattedTimeAgo(updatedAt), interpolation: { escapeValue: false } })
|
||||
: t('subplebbit_offline_info');
|
||||
|
||||
return { isOffline: !isOnline && isOffline, isOnlineStatusLoading: !isOnline && isLoading, offlineIconClass, offlineTitle };
|
||||
};
|
||||
|
||||
export const useIsSubplebbitOffline = useIsCommunityOffline;
|
||||
|
||||
export default useIsCommunityOffline;
|
||||
@@ -1,45 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { Subplebbit } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { getFormattedTimeAgo } from '../lib/utils/time-utils';
|
||||
import useSubplebbitOfflineStore from '../stores/use-subplebbit-offline-store';
|
||||
import useSubplebbitsLoadingStartTimestamps from '../stores/use-subplebbits-loading-start-timestamps-store';
|
||||
import useIsCommunityOffline from './use-is-community-offline';
|
||||
|
||||
const useIsSubplebbitOffline = (subplebbit: Subplebbit | undefined) => {
|
||||
const { t } = useTranslation();
|
||||
const { address, state, updatedAt, updatingState } = subplebbit || {};
|
||||
const { subplebbitOfflineState, setSubplebbitOfflineState, initializesubplebbitOfflineState } = useSubplebbitOfflineStore();
|
||||
const subplebbitsLoadingStartTimestamps = useSubplebbitsLoadingStartTimestamps([address]);
|
||||
|
||||
useEffect(() => {
|
||||
if (address && !subplebbitOfflineState[address]) {
|
||||
initializesubplebbitOfflineState(address);
|
||||
}
|
||||
}, [address, subplebbitOfflineState, initializesubplebbitOfflineState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
setSubplebbitOfflineState(address, { state, updatedAt, updatingState });
|
||||
}
|
||||
}, [address, state, updatedAt, updatingState, setSubplebbitOfflineState]);
|
||||
|
||||
const subplebbitOfflineStore = subplebbitOfflineState[address] || { initialLoad: true };
|
||||
const loadingStartTimestamp = subplebbitsLoadingStartTimestamps[0] || 0;
|
||||
|
||||
const isLoading = subplebbitOfflineStore.initialLoad && (!updatedAt || Date.now() / 1000 - updatedAt >= 120 * 120) && Date.now() / 1000 - loadingStartTimestamp < 30;
|
||||
|
||||
const isOffline = !isLoading && ((updatedAt && updatedAt < Date.now() / 1000 - 120 * 120) || (!updatedAt && Date.now() / 1000 - loadingStartTimestamp >= 30));
|
||||
|
||||
const isOnline = updatedAt && Date.now() / 1000 - updatedAt < 120 * 120;
|
||||
const offlineIconClass = isLoading ? 'yellowOfflineIcon' : isOffline ? 'redOfflineIcon' : '';
|
||||
|
||||
const offlineTitle = isLoading
|
||||
? 'downloading board...'
|
||||
: updatedAt
|
||||
? isOffline && t('posts_last_synced_info', { time: getFormattedTimeAgo(updatedAt), interpolation: { escapeValue: false } })
|
||||
: t('subplebbit_offline_info');
|
||||
|
||||
return { isOffline: !isOnline && isOffline, isOnlineStatusLoading: !isOnline && isLoading, offlineIconClass, offlineTitle };
|
||||
};
|
||||
|
||||
export default useIsSubplebbitOffline;
|
||||
export { useIsCommunityOffline as useIsSubplebbitOffline };
|
||||
export default useIsCommunityOffline;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { Comment, Subplebbit } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { Comment, type Community } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
|
||||
import useSubplebbitsLoadingStartTimestamps from '../stores/use-subplebbits-loading-start-timestamps-store';
|
||||
import useCommunitiesLoadingStartTimestamps from '../stores/use-communities-loading-start-timestamps-store';
|
||||
import { useCurrentTime } from './use-current-time';
|
||||
|
||||
const MAX_POSTS = 8;
|
||||
@@ -32,8 +32,8 @@ function popularityScore(post: Comment, nowSeconds: number): number {
|
||||
return Math.max(replies, 0.1) / (1 + ageSeconds / HALF_LIFE_SECONDS);
|
||||
}
|
||||
|
||||
function isBoardStillLoading(subplebbit: Subplebbit | undefined, loadingStartTimestamp: number | undefined, nowSeconds: number): boolean {
|
||||
if (subplebbit?.updatedAt) {
|
||||
function isBoardStillLoading(community: Community | undefined, loadingStartTimestamp: number | undefined, nowSeconds: number): boolean {
|
||||
if (community?.updatedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ function shuffleBoardAddresses(boardAddresses: string[]): string[] {
|
||||
* The first revealed set is frozen until the user refreshes or changes
|
||||
* the board filter, so threads never disappear during background loads.
|
||||
*/
|
||||
const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitAddresses: string[]) => {
|
||||
const inputKey = [...subplebbitAddresses].sort().join(',');
|
||||
const usePopularPosts = (communities: Array<Community | undefined>, communityAddresses: string[]) => {
|
||||
const inputKey = [...communityAddresses].sort().join(',');
|
||||
const committedRef = useRef<CommittedPopularPosts>({
|
||||
posts: [],
|
||||
revealed: false,
|
||||
@@ -74,7 +74,7 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
||||
// Reset committed and reshuffle when the requested board set changes (e.g. NSFW filter toggle).
|
||||
if (prevInputKeyRef.current !== inputKey) {
|
||||
prevInputKeyRef.current = inputKey;
|
||||
randomizedBoardAddressesRef.current = shuffleBoardAddresses(subplebbitAddresses);
|
||||
randomizedBoardAddressesRef.current = shuffleBoardAddresses(communityAddresses);
|
||||
committedRef.current = {
|
||||
posts: [],
|
||||
revealed: false,
|
||||
@@ -83,7 +83,7 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
||||
|
||||
const currentTime = useCurrentTime(committedRef.current.revealed ? 300 : 5);
|
||||
const nowSeconds = Math.floor(currentTime);
|
||||
const loadingStartTimestamps = useSubplebbitsLoadingStartTimestamps(subplebbitAddresses);
|
||||
const loadingStartTimestamps = useCommunitiesLoadingStartTimestamps(communityAddresses);
|
||||
|
||||
const candidates = useMemo<PopularPostCandidate[]>(() => {
|
||||
if (committedRef.current.revealed || committedRef.current.posts.length >= MAX_POSTS) {
|
||||
@@ -93,16 +93,16 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
||||
try {
|
||||
const selectedLinks = new Set<string>();
|
||||
const allPosts: PopularPostCandidate[] = [];
|
||||
const subplebbitsByAddress = new Map(subplebbitAddresses.map((boardAddress, index) => [boardAddress, subplebbits[index]]));
|
||||
const communitiesByAddress = new Map(communityAddresses.map((boardAddress, index) => [boardAddress, communities[index]]));
|
||||
|
||||
randomizedBoardAddressesRef.current.forEach((boardAddress) => {
|
||||
const subplebbit = subplebbitsByAddress.get(boardAddress);
|
||||
if (!boardAddress || !subplebbit?.posts?.pages?.hot?.comments) {
|
||||
const community = communitiesByAddress.get(boardAddress);
|
||||
if (!boardAddress || !community?.posts?.pages?.hot?.comments) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subPosts: Comment[] = [];
|
||||
for (const post of Object.values(subplebbit.posts.pages.hot.comments as Record<string, Comment>)) {
|
||||
for (const post of Object.values(community.posts.pages.hot.comments as Record<string, Comment>)) {
|
||||
const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, thumbnailUrl } = post;
|
||||
|
||||
try {
|
||||
@@ -131,9 +131,9 @@ const usePopularPosts = (subplebbits: Array<Subplebbit | undefined>, subplebbitA
|
||||
console.error('Error in usePopularPosts:', err);
|
||||
return [];
|
||||
}
|
||||
}, [nowSeconds, subplebbits, subplebbitAddresses]);
|
||||
}, [nowSeconds, communities, communityAddresses]);
|
||||
|
||||
const hasPendingBoards = subplebbitAddresses.some((_, index) => isBoardStillLoading(subplebbits[index], loadingStartTimestamps[index], nowSeconds));
|
||||
const hasPendingBoards = communityAddresses.some((_, index) => isBoardStillLoading(communities[index], loadingStartTimestamps[index], nowSeconds));
|
||||
|
||||
if (!committedRef.current.revealed && (candidates.length >= MAX_POSTS || (!hasPendingBoards && candidates.length > 0))) {
|
||||
committedRef.current.posts = candidates.slice(0, MAX_POSTS).map(({ post }) => post);
|
||||
|
||||
@@ -6,7 +6,10 @@ import { useBoardFeedPageSize } from './use-board-feed-page-size';
|
||||
import { findPostPageInFeed, findPostPageInLoadedBoardFeeds, type FeedsOptionsLike, type LoadedFeedsLike } from '../lib/utils/post-page-resolution';
|
||||
|
||||
interface UsePostPageNumberOptions {
|
||||
subplebbitAddress: string | undefined;
|
||||
/** Canonical name. Kept for backward compatibility with older call sites. */
|
||||
communityAddress?: string;
|
||||
/** Legacy name kept for backwards compatibility. */
|
||||
subplebbitAddress?: string;
|
||||
postCid: string | undefined;
|
||||
/** When false, page segment is excluded (e.g. pending-post view). When true, resolve and show page. */
|
||||
enabled?: boolean;
|
||||
@@ -19,16 +22,29 @@ interface UsePostPageNumberOptions {
|
||||
*
|
||||
* @returns 1-based page number, or undefined when unresolved (render as "?")
|
||||
*/
|
||||
export function usePostPageNumber({ subplebbitAddress, postCid, enabled = true }: UsePostPageNumberOptions): number | undefined {
|
||||
const community = useDirectoryByAddress(subplebbitAddress);
|
||||
export function usePostPageNumber({
|
||||
communityAddress: requestedCommunityAddress,
|
||||
subplebbitAddress: legacyCommunityAddress,
|
||||
postCid,
|
||||
enabled = true,
|
||||
}: UsePostPageNumberOptions): number | undefined {
|
||||
const communityAddress = requestedCommunityAddress ?? legacyCommunityAddress;
|
||||
|
||||
const community = useDirectoryByAddress(communityAddress);
|
||||
const { guiPostsPerPage, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
|
||||
|
||||
const canResolve = Boolean(enabled && subplebbitAddress && postCid && guiPostsPerPage > 0);
|
||||
const canResolve = Boolean(enabled && communityAddress && postCid && guiPostsPerPage > 0);
|
||||
|
||||
// Cache-first: selector returns only computed page to minimize rerenders
|
||||
const cachedPage = useFeedsStore((state) => {
|
||||
if (!canResolve) return undefined;
|
||||
return findPostPageInLoadedBoardFeeds(state.feedsOptions as FeedsOptionsLike, state.loadedFeeds as LoadedFeedsLike, subplebbitAddress!, postCid!, guiPostsPerPage);
|
||||
return findPostPageInLoadedBoardFeeds(
|
||||
state.feedsOptions as unknown as FeedsOptionsLike,
|
||||
state.loadedFeeds as unknown as LoadedFeedsLike,
|
||||
communityAddress!,
|
||||
postCid!,
|
||||
guiPostsPerPage,
|
||||
);
|
||||
});
|
||||
|
||||
// Preload when cache miss and enabled (10 GUI pages)
|
||||
@@ -36,12 +52,12 @@ export function usePostPageNumber({ subplebbitAddress, postCid, enabled = true }
|
||||
() =>
|
||||
canResolve
|
||||
? {
|
||||
subplebbitAddresses: [subplebbitAddress!],
|
||||
communityAddresses: [communityAddress!],
|
||||
sortType: 'active' as const,
|
||||
postsPerPage: paginationFeedPostsPerPage,
|
||||
}
|
||||
: undefined,
|
||||
[canResolve, subplebbitAddress, paginationFeedPostsPerPage],
|
||||
[canResolve, communityAddress, paginationFeedPostsPerPage],
|
||||
);
|
||||
|
||||
const { feed: preloadFeed } = useFeed(preloadOptions);
|
||||
|
||||
@@ -3,7 +3,13 @@ import { Comment, usePublishComment } from '@bitsocialnet/bitsocial-react-hooks'
|
||||
import usePublishPostStore from '../stores/use-publish-post-store';
|
||||
import useChallengesStore from '../stores/use-challenges-store';
|
||||
|
||||
const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) => {
|
||||
type UsePublishPostOptions = {
|
||||
communityAddress?: string;
|
||||
/** legacy compatibility */
|
||||
subplebbitAddress?: string;
|
||||
};
|
||||
|
||||
const usePublishPost = ({ communityAddress: requestedCommunityAddress, subplebbitAddress }: UsePublishPostOptions) => {
|
||||
const { author, title, content, link, spoiler, publishCommentOptions } = usePublishPostStore((state) => ({
|
||||
author: state.author,
|
||||
title: state.title || undefined,
|
||||
@@ -21,9 +27,12 @@ const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) =
|
||||
await abandonPublishRef.current?.();
|
||||
}, []);
|
||||
|
||||
const communityAddress = requestedCommunityAddress ?? subplebbitAddress;
|
||||
|
||||
const createBaseOptions = useCallback(() => {
|
||||
const baseOptions: Comment = {
|
||||
subplebbitAddress,
|
||||
communityAddress,
|
||||
subplebbitAddress: communityAddress,
|
||||
title,
|
||||
content,
|
||||
link,
|
||||
@@ -36,7 +45,7 @@ const usePublishPost = ({ subplebbitAddress }: { subplebbitAddress?: string }) =
|
||||
}
|
||||
|
||||
return baseOptions;
|
||||
}, [author, content, link, spoiler, subplebbitAddress, title]);
|
||||
}, [author, content, link, spoiler, communityAddress, title]);
|
||||
|
||||
const setPublishPostOptions = useCallback(
|
||||
(options: Partial<Comment>) => {
|
||||
|
||||
@@ -9,7 +9,17 @@ import { extractUnresolvedExternalQuoteReferences, getExternalQuoteStatusMessage
|
||||
import { resolveExternalQuoteTarget } from '../lib/utils/external-quote-resolver';
|
||||
import useChallengesStore from '../stores/use-challenges-store';
|
||||
|
||||
const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; subplebbitAddress: string; postCid?: string }) => {
|
||||
type UsePublishReplyOptions = {
|
||||
cid: string;
|
||||
communityAddress?: string;
|
||||
/** legacy compatibility */
|
||||
subplebbitAddress?: string;
|
||||
postCid?: string;
|
||||
};
|
||||
|
||||
const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, subplebbitAddress, postCid }: UsePublishReplyOptions) => {
|
||||
const communityAddress = requestedCommunityAddress ?? subplebbitAddress;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const parentCid = cid;
|
||||
const account = useAccount();
|
||||
@@ -39,7 +49,8 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
|
||||
const createBaseOptions = useCallback(() => {
|
||||
const baseOptions: Comment = {
|
||||
subplebbitAddress,
|
||||
communityAddress,
|
||||
subplebbitAddress: communityAddress,
|
||||
parentCid,
|
||||
postCid: postCid ?? parentCid,
|
||||
content,
|
||||
@@ -53,7 +64,7 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
}
|
||||
|
||||
return baseOptions;
|
||||
}, [author, content, link, parentCid, postCid, spoiler, subplebbitAddress]);
|
||||
}, [author, content, link, parentCid, postCid, spoiler, communityAddress]);
|
||||
|
||||
const setPublishReplyOptions = useCallback(
|
||||
(options: Partial<Comment>) => {
|
||||
@@ -74,16 +85,16 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
|
||||
const resetPublishReplyOptions = useCallback(() => resetPublishReplyStore(parentCid), [parentCid, resetPublishReplyStore]);
|
||||
|
||||
const scopedNumberToCid = usePostNumberStore((state) => (subplebbitAddress ? state.numberToCid[subplebbitAddress] : undefined));
|
||||
const scopedNumberToCid = usePostNumberStore((state) => (communityAddress ? state.numberToCid[communityAddress] : undefined));
|
||||
const quotedCids = useMemo(() => getQuotedCidsFromContent(content, scopedNumberToCid), [content, scopedNumberToCid]);
|
||||
const unresolvedExternalQuoteReferences = useMemo(
|
||||
() =>
|
||||
extractUnresolvedExternalQuoteReferences({
|
||||
content,
|
||||
scopedNumberToCid,
|
||||
subplebbitAddress,
|
||||
communityAddress,
|
||||
}),
|
||||
[content, scopedNumberToCid, subplebbitAddress],
|
||||
[content, scopedNumberToCid, communityAddress],
|
||||
);
|
||||
const publishResolvableQuoteReferences = useMemo(
|
||||
() => unresolvedExternalQuoteReferences.filter((reference) => reference.kind === 'same-board'),
|
||||
@@ -123,7 +134,7 @@ const usePublishReply = ({ cid, subplebbitAddress, postCid }: { cid: string; sub
|
||||
setPublishReplyError(null);
|
||||
setPublishReplyStateMessage(null);
|
||||
setIsResolvingExternalQuotes(false);
|
||||
}, [content, subplebbitAddress]);
|
||||
}, [content, communityAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingPublishRequestId === 0 || pendingPublishRequestId === startedPublishRequestIdRef.current) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Comment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import usePostNumberStore from '../stores/use-post-number-store';
|
||||
import { getCommentCommunityAddress } from '../lib/utils/comment-utils';
|
||||
|
||||
/**
|
||||
* Registers post and fresh replies with the post-number store so backlinks
|
||||
@@ -18,7 +19,7 @@ const useRegisterFreshReplies = (post: Comment | undefined, freshRepliesForRende
|
||||
const cidsKey = all
|
||||
.map((comment) => {
|
||||
const commentKey = comment?.cid ?? (typeof comment?.index === 'number' ? `index:${comment.index}` : `timestamp:${comment?.timestamp ?? ''}`);
|
||||
return `${comment?.subplebbitAddress ?? ''}:${commentKey}:${typeof comment?.number === 'number' ? comment.number : ''}`;
|
||||
return `${getCommentCommunityAddress(comment) ?? ''}:${commentKey}:${typeof comment?.number === 'number' ? comment.number : ''}`;
|
||||
})
|
||||
.sort()
|
||||
.join(',');
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { getCommunityAddress, getBoardPath } from '../lib/utils/route-utils';
|
||||
|
||||
/**
|
||||
* Resolve a board identifier from URL params to canonical community address.
|
||||
* Supports both current route params (`boardIdentifier`) and legacy
|
||||
* compatibility params (`subplebbitAddress`).
|
||||
*/
|
||||
export const useResolvedCommunityAddress = (): string | undefined => {
|
||||
const params = useParams<{ boardIdentifier?: string; subplebbitAddress?: string }>();
|
||||
const directories = useDirectories();
|
||||
|
||||
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!boardIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getCommunityAddress(boardIdentifier, directories);
|
||||
}, [boardIdentifier, directories]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Back-compat export kept for callers still importing the legacy hook name.
|
||||
*/
|
||||
export const useResolvedSubplebbitAddress = useResolvedCommunityAddress;
|
||||
|
||||
/**
|
||||
* Resolve a community address to board path (directory code or address) for links.
|
||||
*/
|
||||
export const useBoardPath = (communityAddress: string | undefined): string | undefined => {
|
||||
const directories = useDirectories();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!communityAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getBoardPath(communityAddress, directories);
|
||||
}, [communityAddress, directories]);
|
||||
};
|
||||
@@ -1,48 +1 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { getSubplebbitAddress, getBoardPath } from '../lib/utils/route-utils';
|
||||
|
||||
/**
|
||||
* Hook to resolve boardIdentifier from URL params to subplebbitAddress
|
||||
* Handles both directory codes (e.g., "biz") and full addresses (e.g., "someboard.eth")
|
||||
*
|
||||
* Performance: Uses useMemo to avoid recalculating when params/directories haven't changed.
|
||||
* The directories reference is stable (from cache) after initial load, so memoization works effectively.
|
||||
*/
|
||||
export const useResolvedSubplebbitAddress = (): string | undefined => {
|
||||
const params = useParams();
|
||||
const directories = useDirectories();
|
||||
|
||||
// Try boardIdentifier first (new format), then subplebbitAddress (old format for backward compatibility)
|
||||
const boardIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!boardIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Resolve directory code to address if needed
|
||||
// getSubplebbitAddress uses internal caching, so this is efficient
|
||||
return getSubplebbitAddress(boardIdentifier, directories);
|
||||
}, [boardIdentifier, directories]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get the board path (directory code or address) for use in links
|
||||
*
|
||||
* Performance: Uses useMemo to avoid recalculating when subplebbitAddress/directories haven't changed.
|
||||
* The directories reference is stable (from cache) after initial load, so memoization works effectively.
|
||||
*/
|
||||
export const useBoardPath = (subplebbitAddress: string | undefined): string | undefined => {
|
||||
const directories = useDirectories();
|
||||
|
||||
return useMemo(() => {
|
||||
if (!subplebbitAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// getBoardPath uses internal caching, so this is efficient
|
||||
return getBoardPath(subplebbitAddress, directories);
|
||||
}, [subplebbitAddress, directories]);
|
||||
};
|
||||
export { useBoardPath, useResolvedSubplebbitAddress } from './use-resolved-community-address';
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import useCommunitiesStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/communities';
|
||||
import type { Community } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { normalizeBoardAddress } from './use-directories';
|
||||
|
||||
type CommunityLike = Record<string, unknown> | Community | undefined;
|
||||
|
||||
const getCommunityByAddress = (communities: Record<string, unknown> | undefined, communityAddress: string | undefined) => {
|
||||
if (!communities || !communityAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exactMatch = communities[communityAddress];
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
const normalizedAddress = normalizeBoardAddress(communityAddress);
|
||||
return Object.entries(communities).find(([key, community]) => {
|
||||
const candidateAddress = typeof (community as CommunityLike)?.address === 'string' ? (community as CommunityLike)?.address : key;
|
||||
return normalizeBoardAddress(candidateAddress) === normalizedAddress;
|
||||
})?.[1];
|
||||
};
|
||||
|
||||
const shallowEqual = (obj1: Record<string, any> | undefined, obj2: Record<string, any> | undefined): boolean => {
|
||||
if (obj1 === obj2) return true;
|
||||
if (!obj1 || !obj2) return obj1 === obj2;
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (obj1[key] !== obj2[key]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ignore transient lifecycle props when deciding whether to update hook consumers.
|
||||
*/
|
||||
const isCommunityEqual = (prev: any, next: any): boolean => {
|
||||
if (prev === next) return true;
|
||||
if (!prev || !next) return prev === next;
|
||||
|
||||
return (
|
||||
prev.address === next.address &&
|
||||
prev.title === next.title &&
|
||||
prev.shortAddress === next.shortAddress &&
|
||||
prev.createdAt === next.createdAt &&
|
||||
prev.updatedAt === next.updatedAt &&
|
||||
prev.description === next.description &&
|
||||
shallowEqual(prev.roles, next.roles)
|
||||
);
|
||||
};
|
||||
|
||||
export const useStableCommunity = (communityAddress: string | undefined) => {
|
||||
const community = useCommunitiesStore((state) => {
|
||||
return getCommunityByAddress(state.communities, communityAddress) as Community | undefined;
|
||||
}, isCommunityEqual);
|
||||
|
||||
return community;
|
||||
};
|
||||
|
||||
export const useCommunityField = <T>(communityAddress: string | undefined, selector: (community: any) => T): T | undefined => {
|
||||
const field = useCommunitiesStore(
|
||||
(state) => {
|
||||
const community = getCommunityByAddress(state.communities, communityAddress);
|
||||
return community ? selector(community) : undefined;
|
||||
},
|
||||
(prev, next) => prev === next,
|
||||
);
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
/**
|
||||
* Back-compat exports for old hook names.
|
||||
*/
|
||||
export const useStableSubplebbit = useStableCommunity;
|
||||
export const useSubplebbitField = useCommunityField;
|
||||
@@ -1,89 +1 @@
|
||||
import useSubplebbitsStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/subplebbits';
|
||||
import { normalizeBoardAddress } from './use-directories';
|
||||
|
||||
const getSubplebbitByAddress = (subplebbits: Record<string, any> | undefined, subplebbitAddress: string | undefined) => {
|
||||
if (!subplebbits || !subplebbitAddress) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exactMatch = subplebbits[subplebbitAddress];
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
const normalizedAddress = normalizeBoardAddress(subplebbitAddress);
|
||||
return Object.entries(subplebbits).find(([key, subplebbit]) => {
|
||||
const candidateAddress = typeof subplebbit?.address === 'string' ? subplebbit.address : key;
|
||||
return normalizeBoardAddress(candidateAddress) === normalizedAddress;
|
||||
})?.[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Shallow compare two objects by keys and values.
|
||||
*/
|
||||
const shallowEqual = (obj1: Record<string, any> | undefined, obj2: Record<string, any> | undefined): boolean => {
|
||||
if (obj1 === obj2) return true;
|
||||
if (!obj1 || !obj2) return obj1 === obj2;
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
for (const key of keys1) {
|
||||
if (obj1[key] !== obj2[key]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom equality function that ignores transient state properties
|
||||
* like updatingState, state, errors, etc. Only compares stable content fields.
|
||||
*/
|
||||
const isSubplebbitEqual = (prev: any, next: any): boolean => {
|
||||
if (prev === next) return true;
|
||||
if (!prev || !next) return prev === next;
|
||||
|
||||
// Compare only stable fields, ignore transient state
|
||||
// Use shallow comparison for roles object to handle new object instances with same content
|
||||
return (
|
||||
prev.address === next.address &&
|
||||
prev.title === next.title &&
|
||||
prev.shortAddress === next.shortAddress &&
|
||||
shallowEqual(prev.roles, next.roles) &&
|
||||
prev.updatedAt === next.updatedAt &&
|
||||
prev.createdAt === next.createdAt &&
|
||||
prev.description === next.description
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get a subplebbit with stable reference that ignores updatingState changes.
|
||||
* Use this when you only need content fields and don't care about loading states.
|
||||
*
|
||||
* @param subplebbitAddress - The address of the subplebbit to retrieve
|
||||
* @returns The subplebbit object, or undefined if not found
|
||||
*/
|
||||
export const useStableSubplebbit = (subplebbitAddress: string | undefined) => {
|
||||
// Use selector with custom equality to ignore transient state
|
||||
const subplebbit = useSubplebbitsStore((state) => getSubplebbitByAddress(state.subplebbits, subplebbitAddress), isSubplebbitEqual);
|
||||
|
||||
return subplebbit;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get only specific fields from a subplebbit, ignoring updatingState.
|
||||
* This is more efficient when you only need a few fields.
|
||||
*
|
||||
* @param subplebbitAddress - The address of the subplebbit
|
||||
* @param selector - Function to extract the needed fields
|
||||
* @returns The selected fields
|
||||
*/
|
||||
export const useSubplebbitField = <T>(subplebbitAddress: string | undefined, selector: (subplebbit: any) => T): T | undefined => {
|
||||
const field = useSubplebbitsStore(
|
||||
(state) => {
|
||||
const subplebbit = getSubplebbitByAddress(state.subplebbits, subplebbitAddress);
|
||||
return subplebbit ? selector(subplebbit) : undefined;
|
||||
},
|
||||
(prev, next) => prev === next,
|
||||
);
|
||||
|
||||
return field;
|
||||
};
|
||||
export { useStableSubplebbit, useSubplebbitField } from './use-stable-community';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useClientsStates, useSubplebbit, useSubplebbitsStates } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { useClientsStates, useCommunity, useCommunitiesStates } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import debounce from 'lodash/debounce';
|
||||
import getShortAddress from '../lib/get-short-address';
|
||||
|
||||
interface CommentOrSubplebbit {
|
||||
interface CommentOrCommunity {
|
||||
state?: string;
|
||||
publishingState?: string;
|
||||
updatingState?: string;
|
||||
@@ -13,13 +13,24 @@ interface States {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
type CommunityLoadingState = {
|
||||
communityAddresses: string[];
|
||||
clientUrls: string[];
|
||||
};
|
||||
|
||||
const isCommunityLoadingState = (state: string[] | CommunityLoadingState | undefined): state is CommunityLoadingState =>
|
||||
Boolean(state && !Array.isArray(state) && 'communityAddresses' in state && 'clientUrls' in state);
|
||||
|
||||
const friendlyStateNames: Record<string, string> = {
|
||||
'fetching-ipns': 'downloading board',
|
||||
'fetching-ipfs': 'downloading thread',
|
||||
'fetching-community-ipns': 'downloading board',
|
||||
'fetching-community-ipfs': 'downloading board',
|
||||
'fetching-subplebbit-ipns': 'downloading board',
|
||||
'fetching-subplebbit-ipfs': 'downloading board',
|
||||
'fetching-update-ipfs': 'downloading update',
|
||||
'resolving-address': 'resolving address',
|
||||
'resolving-community-address': 'resolving board address',
|
||||
'resolving-subplebbit-address': 'resolving board address',
|
||||
'resolving-author-address': 'resolving author address',
|
||||
};
|
||||
@@ -38,8 +49,8 @@ const sanitizeSingleFeedLoadingState = (stateString?: string): string | undefine
|
||||
.replace(/\bloading thread\b/g, 'loading board');
|
||||
};
|
||||
|
||||
const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | undefined => {
|
||||
const { states: rawStates } = useClientsStates({ comment: commentOrSubplebbit }) as { states: States };
|
||||
const useStateString = (commentOrCommunity: CommentOrCommunity): string | undefined => {
|
||||
const { states: rawStates } = useClientsStates({ comment: commentOrCommunity }) as { states: States };
|
||||
|
||||
const debouncedStates = useMemo(() => {
|
||||
const debouncedValue = debounce((value: States) => value, 300);
|
||||
@@ -69,21 +80,21 @@ const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | unde
|
||||
stateString += downloadingParts.join(', ') + ' via IPFS';
|
||||
}
|
||||
|
||||
if (!stateString && commentOrSubplebbit?.state !== 'succeeded') {
|
||||
if (commentOrSubplebbit?.publishingState && commentOrSubplebbit?.publishingState !== 'stopped' && commentOrSubplebbit?.publishingState !== 'succeeded') {
|
||||
stateString = commentOrSubplebbit.publishingState;
|
||||
} else if (commentOrSubplebbit?.updatingState !== 'stopped' && commentOrSubplebbit?.updatingState !== 'succeeded') {
|
||||
stateString = commentOrSubplebbit?.updatingState;
|
||||
if (!stateString && commentOrCommunity?.state !== 'succeeded') {
|
||||
if (commentOrCommunity?.publishingState && commentOrCommunity?.publishingState !== 'stopped' && commentOrCommunity?.publishingState !== 'succeeded') {
|
||||
stateString = commentOrCommunity.publishingState;
|
||||
} else if (commentOrCommunity?.updatingState !== 'stopped' && commentOrCommunity?.updatingState !== 'succeeded') {
|
||||
stateString = commentOrCommunity?.updatingState;
|
||||
}
|
||||
if (stateString) {
|
||||
const isIpfsRelated = stateString.includes('ipfs') || stateString.includes('ipns');
|
||||
stateString = stateString
|
||||
.replaceAll('-', ' ')
|
||||
.replace('ipfs', 'thread')
|
||||
.replace('ipns', 'subplebbit')
|
||||
.replace('ipns', 'community')
|
||||
.replace('fetching', 'downloading')
|
||||
.replace('subplebbit subplebbit', 'board')
|
||||
.replace('downloading subplebbit', 'downloading board');
|
||||
.replace('community community', 'board')
|
||||
.replace('downloading community', 'downloading board');
|
||||
if (isIpfsRelated) {
|
||||
stateString += ' via IPFS';
|
||||
}
|
||||
@@ -95,68 +106,83 @@ const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | unde
|
||||
}
|
||||
|
||||
return stateString === '' ? undefined : stateString;
|
||||
}, [debouncedStates, commentOrSubplebbit]);
|
||||
}, [debouncedStates, commentOrCommunity]);
|
||||
};
|
||||
|
||||
export const useFeedStateString = (subplebbitAddresses?: string[]): string | undefined => {
|
||||
// single subplebbit feed state string
|
||||
const subplebbitAddress = subplebbitAddresses?.length === 1 ? subplebbitAddresses[0] : undefined;
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const singleSubplebbitFeedStateString = sanitizeSingleFeedLoadingState(useStateString(subplebbit));
|
||||
export const useFeedStateString = (communityAddresses?: string[]): string | undefined => {
|
||||
// single community feed state string
|
||||
const communityAddress = communityAddresses?.length === 1 ? communityAddresses[0] : undefined;
|
||||
const community = useCommunity(communityAddress ? { communityAddress } : undefined);
|
||||
const singleCommunityFeedStateString = sanitizeSingleFeedLoadingState(useStateString(community));
|
||||
|
||||
// multiple subplebbit feed state string
|
||||
const { states } = useSubplebbitsStates({ subplebbitAddresses });
|
||||
// multiple community feed state string
|
||||
const { states } = useCommunitiesStates({ communityAddresses });
|
||||
|
||||
const multipleSubplebbitsFeedStateString = useMemo(() => {
|
||||
if (subplebbitAddress) {
|
||||
const multipleCommunitiesFeedStateString = useMemo(() => {
|
||||
if (communityAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stateString = '';
|
||||
|
||||
if (states['resolving-address']) {
|
||||
const { subplebbitAddresses, clientUrls } = states['resolving-address'];
|
||||
if (subplebbitAddresses.length && clientUrls.length) {
|
||||
const count = subplebbitAddresses.length;
|
||||
const resolvingState = states['resolving-address'];
|
||||
if (isCommunityLoadingState(resolvingState)) {
|
||||
const { communityAddresses } = resolvingState;
|
||||
const count = communityAddresses.length;
|
||||
stateString += `resolving ${count} board ${count === 1 ? 'address' : 'addresses'}`;
|
||||
}
|
||||
}
|
||||
|
||||
const pagesStatesSubplebbitAddresses = new Set<string>();
|
||||
const pagesStatesCommunityAddresses = new Set<string>();
|
||||
for (const state in states) {
|
||||
if (state.match('page')) {
|
||||
states[state].subplebbitAddresses.forEach((subplebbitAddress: string) => pagesStatesSubplebbitAddresses.add(subplebbitAddress));
|
||||
const communityState = states[state];
|
||||
if (isCommunityLoadingState(communityState)) {
|
||||
communityState.communityAddresses.forEach((address: string) => pagesStatesCommunityAddresses.add(address));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesSubplebbitAddresses.size) {
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesCommunityAddresses.size) {
|
||||
if (stateString) stateString += ', ';
|
||||
stateString += 'downloading ';
|
||||
if (states['fetching-ipns']) {
|
||||
const count = states['fetching-ipns'].subplebbitAddresses.length;
|
||||
stateString += `${count} ${count === 1 ? 'board' : 'boards'}`;
|
||||
if (count <= 5) {
|
||||
stateString += ` (${states['fetching-ipns'].subplebbitAddresses.map((a: string) => getShortAddress(a) || a).join(', ')})`;
|
||||
const fetchingIpnsState = states['fetching-ipns'];
|
||||
if (isCommunityLoadingState(fetchingIpnsState)) {
|
||||
const count = fetchingIpnsState.communityAddresses.length;
|
||||
stateString += `${count} ${count === 1 ? 'board' : 'boards'}`;
|
||||
if (count <= 5) {
|
||||
stateString += ` (${fetchingIpnsState.communityAddresses.map((a: string) => getShortAddress(a) || a).join(', ')})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (states['fetching-ipfs']) {
|
||||
if (states['fetching-ipns']) stateString += ', ';
|
||||
const count = states['fetching-ipfs'].subplebbitAddresses.length;
|
||||
stateString += `${count} ${count === 1 ? 'thread' : 'threads'}`;
|
||||
const fetchingIpfsState = states['fetching-ipfs'];
|
||||
if (isCommunityLoadingState(fetchingIpfsState)) {
|
||||
if (stateString[stateString.length - 1] !== ' ') {
|
||||
stateString += ', ';
|
||||
}
|
||||
const count = fetchingIpfsState.communityAddresses.length;
|
||||
stateString += `${count} ${count === 1 ? 'thread' : 'threads'}`;
|
||||
}
|
||||
}
|
||||
if (pagesStatesSubplebbitAddresses.size) {
|
||||
|
||||
if (pagesStatesCommunityAddresses.size) {
|
||||
if (states['fetching-ipns'] || states['fetching-ipfs']) stateString += ', ';
|
||||
const count = pagesStatesSubplebbitAddresses.size;
|
||||
const count = pagesStatesCommunityAddresses.size;
|
||||
stateString += `${count} ${count === 1 ? 'page' : 'pages'}`;
|
||||
}
|
||||
|
||||
stateString += ' via IPFS';
|
||||
}
|
||||
|
||||
if (!stateString && subplebbitAddresses?.length) {
|
||||
const count = subplebbitAddresses.length;
|
||||
if (!stateString && communityAddresses?.length) {
|
||||
const count = communityAddresses.length;
|
||||
stateString = `downloading ${count} ${count === 1 ? 'board' : 'boards'}`;
|
||||
if (count <= 5) {
|
||||
stateString += ` (${subplebbitAddresses.map((a) => getShortAddress(a) || a).join(', ')})`;
|
||||
stateString += ` (${communityAddresses.map((a) => getShortAddress(a) || a).join(', ')})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +191,12 @@ export const useFeedStateString = (subplebbitAddresses?: string[]): string | und
|
||||
|
||||
// if string is empty, return undefined instead
|
||||
return stateString === '' ? undefined : stateString;
|
||||
}, [states, subplebbitAddress, subplebbitAddresses]);
|
||||
}, [states, communityAddress, communityAddresses]);
|
||||
|
||||
if (singleSubplebbitFeedStateString) {
|
||||
return singleSubplebbitFeedStateString;
|
||||
if (singleCommunityFeedStateString) {
|
||||
return singleCommunityFeedStateString;
|
||||
}
|
||||
return multipleSubplebbitsFeedStateString;
|
||||
return multipleCommunitiesFeedStateString;
|
||||
};
|
||||
|
||||
export default useStateString;
|
||||
|
||||
@@ -1,34 +1 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSubplebbitStats } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import { create } from 'zustand';
|
||||
|
||||
type SubplebbitsStatsState = {
|
||||
subplebbitsStats: { [subplebbitAddress: string]: any };
|
||||
setSubplebbitStats: (subplebbitAddress: string, stats: any) => void;
|
||||
};
|
||||
|
||||
export const useSubplebbitsStatsStore = create<SubplebbitsStatsState>((set) => ({
|
||||
subplebbitsStats: {},
|
||||
setSubplebbitStats: (subplebbitAddress: string, subplebbitStats: any) =>
|
||||
set((state) => ({
|
||||
subplebbitsStats: { ...state.subplebbitsStats, [subplebbitAddress]: subplebbitStats },
|
||||
})),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Component that fetches stats for a single subplebbit and stores them.
|
||||
* Render one of these for each subplebbit you want to track stats for.
|
||||
*/
|
||||
export const SubplebbitStatsCollector = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
|
||||
const stats = useSubplebbitStats({ subplebbitAddress });
|
||||
const setSubplebbitStats = useSubplebbitsStatsStore((state) => state.setSubplebbitStats);
|
||||
|
||||
useEffect(() => {
|
||||
// Only update store when we have actual stats (not just loading state)
|
||||
if (stats && stats.allPostCount !== undefined) {
|
||||
setSubplebbitStats(subplebbitAddress, stats);
|
||||
}
|
||||
}, [stats, subplebbitAddress, setSubplebbitStats]);
|
||||
|
||||
return null; // This is a data-fetching component, renders nothing
|
||||
};
|
||||
export { SubplebbitStatsCollector, useSubplebbitsStatsStore } from './use-communities-stats';
|
||||
|
||||
+27
-30
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
|
||||
import { isAllView, isModView, isSubscriptionsView } from '../lib/utils/view-utils';
|
||||
import useThemeStore from '../stores/use-theme-store';
|
||||
import { useDirectories } from './use-directories';
|
||||
import { useResolvedSubplebbitAddress } from './use-resolved-subplebbit-address';
|
||||
import { useResolvedCommunityAddress } from './use-resolved-community-address';
|
||||
import { useAccountComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||
import useSpecialThemeStore from '../stores/use-special-theme-store';
|
||||
import { isChristmas } from '../lib/utils/time-utils';
|
||||
import { updateFavicon, isSfwBoard } from '../lib/update-favicon';
|
||||
import { isSfwBoard, updateFavicon } from '../lib/update-favicon';
|
||||
|
||||
const themeClasses = ['yotsuba', 'yotsuba-b', 'futaba', 'burichan', 'tomorrow', 'photon'];
|
||||
|
||||
@@ -20,48 +20,47 @@ const updateThemeClass = (newTheme: string) => {
|
||||
|
||||
const useTheme = (): [string, (theme: string) => void] => {
|
||||
const location = useLocation();
|
||||
const params = useParams<{ subplebbitAddress: string }>();
|
||||
const params = useParams<{ boardIdentifier?: string; subplebbitAddress?: string }>();
|
||||
const pendingPostParams = useParams<{ accountCommentIndex?: string }>();
|
||||
const pendingPostCommentIndex = pendingPostParams?.accountCommentIndex ? parseInt(pendingPostParams.accountCommentIndex) : undefined;
|
||||
const pendingPostCommentIndex = pendingPostParams?.accountCommentIndex ? parseInt(pendingPostParams.accountCommentIndex, 10) : undefined;
|
||||
const pendingPost = useAccountComment({ commentIndex: pendingPostCommentIndex });
|
||||
const pendingPostSubplebbitAddress = pendingPost?.subplebbitAddress;
|
||||
const pendingPostCommunityAddress =
|
||||
(pendingPost as { communityAddress?: string }).communityAddress ||
|
||||
// compatibility fallback for legacy inbound/persisted comment payloads
|
||||
(pendingPost as { subplebbitAddress?: string }).subplebbitAddress;
|
||||
|
||||
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
|
||||
|
||||
const setThemeStore = useThemeStore((state) => state.setTheme);
|
||||
// Subscribe to the actual themes data, not just the getter function
|
||||
const themes = useThemeStore((state) => state.themes);
|
||||
const directories = useDirectories();
|
||||
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const isInModView = isModView(location.pathname);
|
||||
const resolvedAddress = useResolvedSubplebbitAddress();
|
||||
const subplebbitAddress = resolvedAddress || pendingPostSubplebbitAddress;
|
||||
const routeIdentifier = params.boardIdentifier || params.subplebbitAddress;
|
||||
const resolvedAddress = useResolvedCommunityAddress();
|
||||
const communityAddress = resolvedAddress || pendingPostCommunityAddress || routeIdentifier;
|
||||
|
||||
// Check for Christmas and initialize special theme if needed
|
||||
useEffect(() => {
|
||||
const isChristmasTime = isChristmas();
|
||||
|
||||
if (isChristmasTime && isEnabled === null && subplebbitAddress && !isInAllView && !isInSubscriptionsView && !isInModView) {
|
||||
if (isChristmasTime && isEnabled === null && communityAddress && !isInAllView && !isInSubscriptionsView && !isInModView) {
|
||||
setIsEnabled(true);
|
||||
} else if (!isChristmasTime && isEnabled) {
|
||||
setIsEnabled(false);
|
||||
}
|
||||
}, [isEnabled, setIsEnabled, subplebbitAddress, isInAllView, isInSubscriptionsView, isInModView]);
|
||||
}, [isEnabled, setIsEnabled, communityAddress, isInAllView, isInSubscriptionsView, isInModView]);
|
||||
|
||||
// Calculate current theme during render - no effects needed
|
||||
const currentTheme = useMemo(() => {
|
||||
// Always use yotsuba for home page
|
||||
if (location.pathname === '/') {
|
||||
return 'yotsuba';
|
||||
}
|
||||
|
||||
// Always use yotsuba for rules page (boardIdentifier in URL is for loading rules, not theming)
|
||||
if (location.pathname.startsWith('/rules')) {
|
||||
return 'yotsuba';
|
||||
}
|
||||
|
||||
// If special theme is enabled, use tomorrow
|
||||
if (isEnabled) {
|
||||
return 'tomorrow';
|
||||
}
|
||||
@@ -69,9 +68,9 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
let storedTheme = null;
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
storedTheme = themes.nsfw;
|
||||
} else if (subplebbitAddress) {
|
||||
const subplebbit = directories.find((s) => s.address === subplebbitAddress);
|
||||
if (subplebbit?.nsfw) {
|
||||
} else if (communityAddress) {
|
||||
const community = directories.find((entry) => entry.address === communityAddress);
|
||||
if (community?.nsfw) {
|
||||
storedTheme = themes.nsfw;
|
||||
} else {
|
||||
storedTheme = themes.sfw;
|
||||
@@ -79,7 +78,7 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
}
|
||||
|
||||
return storedTheme || 'yotsuba';
|
||||
}, [location.pathname, isEnabled, isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directories, themes]);
|
||||
}, [location.pathname, isEnabled, isInAllView, isInSubscriptionsView, isInModView, communityAddress, directories, themes]);
|
||||
|
||||
const sfw = isSfwBoard({
|
||||
pathname: location.pathname,
|
||||
@@ -87,37 +86,35 @@ const useTheme = (): [string, (theme: string) => void] => {
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
subplebbitAddress,
|
||||
subplebbitAddress: communityAddress,
|
||||
directories,
|
||||
});
|
||||
|
||||
// Update DOM class when theme changes
|
||||
useEffect(() => {
|
||||
updateThemeClass(currentTheme);
|
||||
}, [currentTheme]);
|
||||
|
||||
// Update favicon when SFW status changes (separate effect for independent lifecycle)
|
||||
useEffect(() => {
|
||||
updateFavicon(sfw);
|
||||
}, [sfw]);
|
||||
|
||||
const setSubplebbitTheme = useCallback(
|
||||
const setCommunityTheme = useCallback(
|
||||
async (newTheme: string) => {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
await setThemeStore('nsfw', newTheme);
|
||||
} else if (subplebbitAddress) {
|
||||
const subplebbit = directories.find((s) => s.address === subplebbitAddress);
|
||||
if (subplebbit?.nsfw) {
|
||||
} else if (communityAddress) {
|
||||
const community = directories.find((entry) => entry.address === communityAddress);
|
||||
if (community?.nsfw) {
|
||||
await setThemeStore('nsfw', newTheme);
|
||||
} else {
|
||||
await setThemeStore('sfw', newTheme);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isInAllView, isInSubscriptionsView, isInModView, subplebbitAddress, directories, setThemeStore],
|
||||
[isInAllView, isInSubscriptionsView, isInModView, communityAddress, directories, setThemeStore],
|
||||
);
|
||||
|
||||
return [currentTheme, setSubplebbitTheme];
|
||||
return [currentTheme, setCommunityTheme];
|
||||
};
|
||||
|
||||
export default useTheme;
|
||||
|
||||
Reference in New Issue
Block a user