mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(feed): support strict community refs and safer domain publishing
This commit is contained in:
@@ -28,6 +28,16 @@ If uncertain, ask the developer before adding an entry.
|
|||||||
|
|
||||||
## Entries
|
## Entries
|
||||||
|
|
||||||
|
### 5chan patches its installed hooks tarball instead of using the local hooks repo
|
||||||
|
|
||||||
|
- **Date:** 2026-04-15
|
||||||
|
- **Observed by:** Codex
|
||||||
|
- **Context:** Debugging strict `{name, publicKey}` community refs and the temporary `scripts/patch-bitsocial-react-hooks-esm.cjs` workaround
|
||||||
|
- **What was surprising:** 5chan does not consume the nearby `/Users/Tommaso/Desktop/bitsocial/bitsocial-react-hooks` checkout; `package.json` installs a pinned GitHub tarball of `@bitsocialnet/bitsocial-react-hooks` and then mutates its `dist/` files in `postinstall`.
|
||||||
|
- **Impact:** Agents can wrongly assume local hooks source changes are already active in 5chan, or treat the postinstall patch as app logic instead of a temporary package-level workaround.
|
||||||
|
- **Mitigation:** Before debugging hooks behavior from 5chan, check `package.json` to see whether the app points at a tarball commit or a local path. If `scripts/patch-bitsocial-react-hooks-esm.cjs` is involved, fix the underlying issue in `bitsocial-react-hooks`, rebuild its `dist/`, update 5chan to the fixed commit/path, and then remove the patch script.
|
||||||
|
- **Status:** confirmed
|
||||||
|
|
||||||
### Portless breaks Windows installs
|
### Portless breaks Windows installs
|
||||||
|
|
||||||
- **Date:** 2026-03-04
|
- **Date:** 2026-03-04
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const path = require('path');
|
|||||||
const packageDistPath = path.join(__dirname, '..', 'node_modules', '@bitsocialnet', 'bitsocial-react-hooks', 'dist');
|
const packageDistPath = path.join(__dirname, '..', 'node_modules', '@bitsocialnet', 'bitsocial-react-hooks', 'dist');
|
||||||
const logPrefix = '[patch-bitsocial-react-hooks-esm]';
|
const logPrefix = '[patch-bitsocial-react-hooks-esm]';
|
||||||
const packageIndexPath = path.join(packageDistPath, 'index.js');
|
const packageIndexPath = path.join(packageDistPath, 'index.js');
|
||||||
|
const communitiesPagesStorePath = path.join(packageDistPath, 'stores', 'communities-pages', 'communities-pages-store.js');
|
||||||
|
|
||||||
if (!fs.existsSync(packageDistPath)) {
|
if (!fs.existsSync(packageDistPath)) {
|
||||||
console.log(`${logPrefix} Skip: @bitsocialnet/bitsocial-react-hooks dist not found.`);
|
console.log(`${logPrefix} Skip: @bitsocialnet/bitsocial-react-hooks dist not found.`);
|
||||||
@@ -16,9 +17,12 @@ const relativeImportPattern = /(from\s+|import\s+)(['"])(\.\.?\/[^'"]+)\2/g;
|
|||||||
let touchedFiles = 0;
|
let touchedFiles = 0;
|
||||||
let rewrittenImports = 0;
|
let rewrittenImports = 0;
|
||||||
let removedNodeDebugPatches = 0;
|
let removedNodeDebugPatches = 0;
|
||||||
|
let patchedCommunityFirstPageGuards = 0;
|
||||||
|
|
||||||
const nodeDebugPatchPattern =
|
const nodeDebugPatchPattern =
|
||||||
/\/\/ fix DEBUG_DEPTH bug https:\/\/github\.com\/debug-js\/debug\/issues\/746\s*try\s*\{\s*if \(process\.env\.DEBUG_DEPTH\) \{\s*require\("util"\)\.inspect\.defaultOptions\.depth = process\.env\.DEBUG_DEPTH;\s*\}\s*if \(process\.env\.DEBUG_ARRAY\) \{\s*require\("util"\)\.inspect\.defaultOptions\.maxArrayLength = process\.env\.DEBUG_ARRAY;\s*\}\s*\}\s*catch \(e\) \{ \}/m;
|
/\/\/ fix DEBUG_DEPTH bug https:\/\/github\.com\/debug-js\/debug\/issues\/746\s*try\s*\{\s*if \(process\.env\.DEBUG_DEPTH\) \{\s*require\("util"\)\.inspect\.defaultOptions\.depth = process\.env\.DEBUG_DEPTH;\s*\}\s*if \(process\.env\.DEBUG_ARRAY\) \{\s*require\("util"\)\.inspect\.defaultOptions\.maxArrayLength = process\.env\.DEBUG_ARRAY;\s*\}\s*\}\s*catch \(e\) \{ \}/m;
|
||||||
|
const communityFirstPageAssertNeedle =
|
||||||
|
" assert(community === null || community === void 0 ? void 0 : community.address, `getCommunityFirstPageCid community '${community}' invalid`);\n";
|
||||||
|
|
||||||
const splitSpecifier = (specifier) => {
|
const splitSpecifier = (specifier) => {
|
||||||
const suffixStart = specifier.search(/[?#]/);
|
const suffixStart = specifier.search(/[?#]/);
|
||||||
@@ -80,6 +84,18 @@ const patchFile = (filePath) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filePath === communitiesPagesStorePath) {
|
||||||
|
const nextUpdated = updated.replace(
|
||||||
|
communityFirstPageAssertNeedle,
|
||||||
|
" if (!(community === null || community === void 0 ? void 0 : community.address)) {\n return;\n }\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextUpdated !== updated) {
|
||||||
|
updated = nextUpdated;
|
||||||
|
patchedCommunityFirstPageGuards += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!fileImportCount && updated === source) {
|
if (!fileImportCount && updated === source) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,5 +128,5 @@ if (!touchedFiles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`${logPrefix} Patched ${rewrittenImports} imports across ${touchedFiles} files${removedNodeDebugPatches ? ` and removed ${removedNodeDebugPatches} browser-incompatible debug util block(s)` : ''}.`,
|
`${logPrefix} Patched ${rewrittenImports} imports across ${touchedFiles} files${removedNodeDebugPatches ? `, removed ${removedNodeDebugPatches} browser-incompatible debug util block(s)` : ''}${patchedCommunityFirstPageGuards ? `, and relaxed ${patchedCommunityFirstPageGuards} community first-page guard(s)` : ''}.`,
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-1
@@ -12,6 +12,7 @@ import useIsMobile from './hooks/use-is-mobile';
|
|||||||
import { useAccountCommunityAddresses } from './hooks/use-account-community-addresses';
|
import { useAccountCommunityAddresses } from './hooks/use-account-community-addresses';
|
||||||
import useTheme from './hooks/use-theme';
|
import useTheme from './hooks/use-theme';
|
||||||
import { useDirectories } from './hooks/use-directories';
|
import { useDirectories } from './hooks/use-directories';
|
||||||
|
import { useCommunityIdentifier } from './hooks/use-community-identifiers';
|
||||||
import { useResolvedCommunityAddress } from './hooks/use-resolved-community-address';
|
import { useResolvedCommunityAddress } from './hooks/use-resolved-community-address';
|
||||||
import useSafeAccountComment from './hooks/use-safe-account-comment';
|
import useSafeAccountComment from './hooks/use-safe-account-comment';
|
||||||
import {
|
import {
|
||||||
@@ -245,7 +246,8 @@ const ModQueueRoute = () => {
|
|||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
const accountAddress = account?.author?.address;
|
const accountAddress = account?.author?.address;
|
||||||
const communityAddress = useResolvedCommunityAddress();
|
const communityAddress = useResolvedCommunityAddress();
|
||||||
const community = useCommunity({ communityAddress });
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const accountCommunityAddresses = useAccountCommunityAddresses();
|
const accountCommunityAddresses = useAccountCommunityAddresses();
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
|||||||
@@ -304,7 +304,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
|
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
|
||||||
const resolvedAddress = useResolvedCommunityAddress();
|
const resolvedAddress = useResolvedCommunityAddress();
|
||||||
const communityAddress = resolvedAddress || accountComment?.communityAddress;
|
const communityAddress = resolvedAddress || accountComment?.communityAddress;
|
||||||
const { setPublishPostOptions, postIndex, publishPost, publishPostOptions, resetPublishPostOptions } = usePublishPost({ subplebbitAddress: communityAddress });
|
const { setPublishPostOptions, postIndex, publishPost, publishPostError, publishPostOptions, resetPublishPostOptions } = usePublishPost({
|
||||||
|
subplebbitAddress: communityAddress,
|
||||||
|
});
|
||||||
const effectiveBoardAddress = communityAddress || publishPostOptions.communityAddress;
|
const effectiveBoardAddress = communityAddress || publishPostOptions.communityAddress;
|
||||||
|
|
||||||
const textRef = useRef<HTMLTextAreaElement>(null);
|
const textRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -499,6 +501,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{publishPostError && <div className={styles.error}>{publishPostError}</div>}
|
||||||
{publishReplyError && <div className={styles.error}>{publishReplyError}</div>}
|
{publishReplyError && <div className={styles.error}>{publishReplyError}</div>}
|
||||||
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
abandonPublishMock: vi.fn(async () => undefined),
|
abandonPublishMock: vi.fn(async () => undefined),
|
||||||
index: 12,
|
index: 12,
|
||||||
lastPublishOptions: undefined as Record<string, any> | undefined,
|
lastPublishOptions: undefined as Record<string, any> | undefined,
|
||||||
|
publishAuthorBlockedReason: undefined as 'resolving' | 'unresolved' | 'mismatch' | undefined,
|
||||||
publishCommentMock: vi.fn(),
|
publishCommentMock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -27,6 +28,14 @@ vi.mock('@bitsocialnet/bitsocial-react-hooks', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../use-publish-author-domain-guard', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => ({
|
||||||
|
blockedReason: testState.publishAuthorBlockedReason,
|
||||||
|
}),
|
||||||
|
getPublishAuthorDomainErrorMessage: (reason: string) => `blocked:${reason}`,
|
||||||
|
}));
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let latestValue: ReturnType<typeof usePublishPost>;
|
let latestValue: ReturnType<typeof usePublishPost>;
|
||||||
let root: Root;
|
let root: Root;
|
||||||
@@ -47,6 +56,7 @@ describe('usePublishPost', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
testState.index = 12;
|
testState.index = 12;
|
||||||
testState.lastPublishOptions = undefined;
|
testState.lastPublishOptions = undefined;
|
||||||
|
testState.publishAuthorBlockedReason = undefined;
|
||||||
useChallengesStore.setState({ challenges: [] });
|
useChallengesStore.setState({ challenges: [] });
|
||||||
usePublishPostStore.getState().resetPublishPostStore();
|
usePublishPostStore.getState().resetPublishPostStore();
|
||||||
|
|
||||||
@@ -73,7 +83,7 @@ describe('usePublishPost', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(latestValue.postIndex).toBe(12);
|
expect(latestValue.postIndex).toBe(12);
|
||||||
expect(latestValue.publishPost).toBe(testState.publishCommentMock);
|
expect(typeof latestValue.publishPost).toBe('function');
|
||||||
expect(latestValue.publishPostOptions).toMatchObject({
|
expect(latestValue.publishPostOptions).toMatchObject({
|
||||||
author: { displayName: 'Alice' },
|
author: { displayName: 'Alice' },
|
||||||
communityAddress: 'music.eth',
|
communityAddress: 'music.eth',
|
||||||
@@ -116,4 +126,16 @@ describe('usePublishPost', () => {
|
|||||||
|
|
||||||
expect(latestValue.publishPostOptions).toEqual({});
|
expect(latestValue.publishPostOptions).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('blocks publish when the active account address is a domain that is not verified yet', async () => {
|
||||||
|
testState.publishAuthorBlockedReason = 'unresolved';
|
||||||
|
renderHook();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
latestValue.publishPost();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(latestValue.publishPostError).toBe('blocked:unresolved');
|
||||||
|
expect(testState.publishCommentMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
directories: [] as Array<Record<string, unknown>>,
|
directories: [] as Array<Record<string, unknown>>,
|
||||||
index: 7,
|
index: 7,
|
||||||
lastPublishOptions: undefined as Record<string, any> | undefined,
|
lastPublishOptions: undefined as Record<string, any> | undefined,
|
||||||
|
publishAuthorBlockedReason: undefined as 'resolving' | 'unresolved' | 'mismatch' | undefined,
|
||||||
publishCommentMock: vi.fn(),
|
publishCommentMock: vi.fn(),
|
||||||
resolveExternalQuoteTargetMock: vi.fn(),
|
resolveExternalQuoteTargetMock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -46,6 +47,14 @@ vi.mock('../../lib/utils/external-quote-resolver', () => ({
|
|||||||
resolveExternalQuoteTarget: (...args: any[]) => testState.resolveExternalQuoteTargetMock(...args),
|
resolveExternalQuoteTarget: (...args: any[]) => testState.resolveExternalQuoteTargetMock(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../use-publish-author-domain-guard', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => ({
|
||||||
|
blockedReason: testState.publishAuthorBlockedReason,
|
||||||
|
}),
|
||||||
|
getPublishAuthorDomainErrorMessage: (reason: string) => `blocked:${reason}`,
|
||||||
|
}));
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let latestValue: ReturnType<typeof usePublishReply>;
|
let latestValue: ReturnType<typeof usePublishReply>;
|
||||||
let root: Root;
|
let root: Root;
|
||||||
@@ -68,6 +77,7 @@ describe('usePublishReply', () => {
|
|||||||
testState.directories = [];
|
testState.directories = [];
|
||||||
testState.index = 7;
|
testState.index = 7;
|
||||||
testState.lastPublishOptions = undefined;
|
testState.lastPublishOptions = undefined;
|
||||||
|
testState.publishAuthorBlockedReason = undefined;
|
||||||
useChallengesStore.setState({ challenges: [] });
|
useChallengesStore.setState({ challenges: [] });
|
||||||
usePostNumberStore.setState({ cidToNumber: {}, numberToCid: { 'music.eth': { 12: 'quoted-cid' } } });
|
usePostNumberStore.setState({ cidToNumber: {}, numberToCid: { 'music.eth': { 12: 'quoted-cid' } } });
|
||||||
usePublishReplyStore.setState({
|
usePublishReplyStore.setState({
|
||||||
@@ -175,6 +185,18 @@ describe('usePublishReply', () => {
|
|||||||
expect(testState.publishCommentMock).not.toHaveBeenCalled();
|
expect(testState.publishCommentMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('blocks publish when the active account address is a domain that is not verified yet', async () => {
|
||||||
|
testState.publishAuthorBlockedReason = 'unresolved';
|
||||||
|
renderHook();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await latestValue.publishReply();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(latestValue.publishReplyError).toBe('blocked:unresolved');
|
||||||
|
expect(testState.publishCommentMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('queues reply challenges and clears the scoped reply store on reset', async () => {
|
it('queues reply challenges and clears the scoped reply store on reset', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
latestValue.setPublishReplyOptions({
|
latestValue.setPublishReplyOptions({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { useCommunityStats } from '@bitsocialnet/bitsocial-react-hooks';
|
import { useCommunityStats } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
|
import { useCommunityIdentifier } from './use-community-identifiers';
|
||||||
|
|
||||||
type CommunityStatsState = {
|
type CommunityStatsState = {
|
||||||
communityStats: { [communityAddress: string]: any };
|
communityStats: { [communityAddress: string]: any };
|
||||||
@@ -16,7 +17,8 @@ export const useCommunitiesStatsStore = create<CommunityStatsState>((set) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const CommunityStatsCollector = ({ communityAddress }: { communityAddress: string }) => {
|
export const CommunityStatsCollector = ({ communityAddress }: { communityAddress: string }) => {
|
||||||
const stats = useCommunityStats({ communityAddress });
|
const community = useCommunityIdentifier(communityAddress);
|
||||||
|
const stats = useCommunityStats(community ? { community } : undefined);
|
||||||
const setCommunityStats = useCommunitiesStatsStore((state) => state.setCommunityStats);
|
const setCommunityStats = useCommunitiesStatsStore((state) => state.setCommunityStats);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { CommunityIdentifier } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
|
import { findDirectoryByAddress, type DirectoryCommunity, useDirectories } from './use-directories';
|
||||||
|
|
||||||
|
const isLikelyCommunityName = (value: string) => value.includes('.');
|
||||||
|
|
||||||
|
export const getCommunityIdentifier = (communityAddress: string | undefined, directories: DirectoryCommunity[]): CommunityIdentifier | undefined => {
|
||||||
|
if (!communityAddress) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directory = findDirectoryByAddress(directories, communityAddress);
|
||||||
|
if (directory?.name && directory.publicKey) {
|
||||||
|
return {
|
||||||
|
name: directory.name,
|
||||||
|
publicKey: directory.publicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (directory?.publicKey) {
|
||||||
|
return {
|
||||||
|
publicKey: directory.publicKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (directory?.name) {
|
||||||
|
return {
|
||||||
|
name: directory.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLikelyCommunityName(communityAddress)
|
||||||
|
? {
|
||||||
|
name: communityAddress,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
publicKey: communityAddress,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCommunityIdentifiers = (communityAddresses: Array<string | undefined>, directories: DirectoryCommunity[]): CommunityIdentifier[] =>
|
||||||
|
communityAddresses.flatMap((communityAddress) => {
|
||||||
|
const community = getCommunityIdentifier(communityAddress, directories);
|
||||||
|
return community ? [community] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useCommunityIdentifier = (communityAddress: string | undefined): CommunityIdentifier | undefined => {
|
||||||
|
const directories = useDirectories();
|
||||||
|
|
||||||
|
return useMemo(() => getCommunityIdentifier(communityAddress, directories), [communityAddress, directories]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCommunityIdentifiers = (communityAddresses?: Array<string | undefined>): CommunityIdentifier[] => {
|
||||||
|
const directories = useDirectories();
|
||||||
|
|
||||||
|
return useMemo(() => getCommunityIdentifiers(communityAddresses ?? [], directories), [communityAddresses, directories]);
|
||||||
|
};
|
||||||
@@ -53,7 +53,6 @@ let cacheCommunities: DirectoryCommunity[] | null = null;
|
|||||||
let cacheMetadata: DirectoriesMetadata | null = null;
|
let cacheMetadata: DirectoriesMetadata | null = null;
|
||||||
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
let inFlightGitHubFetch: Promise<DirectoriesData> | null = null;
|
||||||
const DIRECTORY_ALIAS_SUFFIXES = ['.bso', '.eth'] as const;
|
const DIRECTORY_ALIAS_SUFFIXES = ['.bso', '.eth'] as const;
|
||||||
const MULTIBOARD_PUBLIC_KEY_FALLBACK_NAMES = new Set(['business-and-finance.bso', 'politically-incorrect.bso']);
|
|
||||||
|
|
||||||
// Exposed for deterministic unit tests around module-level cache state.
|
// Exposed for deterministic unit tests around module-level cache state.
|
||||||
export const __resetDirectoriesModuleStateForTests = () => {
|
export const __resetDirectoriesModuleStateForTests = () => {
|
||||||
@@ -97,14 +96,6 @@ const getDirectoryIdentifiers = (community: DirectoryCommunity): string[] => [
|
|||||||
...new Set([community.address, community.name, community.publicKey].filter((value): value is string => typeof value === 'string' && value.length > 0)),
|
...new Set([community.address, community.name, community.publicKey].filter((value): value is string => typeof value === 'string' && value.length > 0)),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getDirectoryFetchAddress = (community: DirectoryCommunity): string => {
|
|
||||||
if (community.name && MULTIBOARD_PUBLIC_KEY_FALLBACK_NAMES.has(community.name) && community.publicKey) {
|
|
||||||
return community.publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return community.address;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toCanonicalCommunity = (value: {
|
const toCanonicalCommunity = (value: {
|
||||||
address?: unknown;
|
address?: unknown;
|
||||||
communityAddress?: unknown;
|
communityAddress?: unknown;
|
||||||
@@ -473,7 +464,7 @@ export const useDirectoriesState = () => {
|
|||||||
|
|
||||||
export const useDirectoryAddresses = () => {
|
export const useDirectoryAddresses = () => {
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
const directoryAddresses = useMemo(() => (Array.isArray(directories) ? directories.map((community) => getDirectoryFetchAddress(community)) : []), [directories]);
|
const directoryAddresses = useMemo(() => (Array.isArray(directories) ? directories.map((community) => community.address) : []), [directories]);
|
||||||
|
|
||||||
return directoryAddresses;
|
return directoryAddresses;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { getDirectoryFetchAddress, useDirectories } from './use-directories';
|
import { useDirectories } from './use-directories';
|
||||||
import useAllFeedFilterStore from '../stores/use-all-feed-filter-store';
|
import useAllFeedFilterStore from '../stores/use-all-feed-filter-store';
|
||||||
|
|
||||||
export const useFilteredDirectoryAddresses = () => {
|
export const useFilteredDirectoryAddresses = () => {
|
||||||
@@ -8,13 +8,13 @@ export const useFilteredDirectoryAddresses = () => {
|
|||||||
|
|
||||||
const filteredAddresses = useMemo(() => {
|
const filteredAddresses = useMemo(() => {
|
||||||
if (filter === 'all') {
|
if (filter === 'all') {
|
||||||
return directories.map((community) => getDirectoryFetchAddress(community));
|
return directories.map((community) => community.address);
|
||||||
}
|
}
|
||||||
if (filter === 'nsfw') {
|
if (filter === 'nsfw') {
|
||||||
return directories.filter((community) => community.nsfw === true).map((community) => getDirectoryFetchAddress(community));
|
return directories.filter((community) => community.nsfw === true).map((community) => community.address);
|
||||||
}
|
}
|
||||||
// filter === 'sfw'
|
// filter === 'sfw'
|
||||||
return directories.filter((community) => community.nsfw !== true).map((community) => getDirectoryFetchAddress(community));
|
return directories.filter((community) => community.nsfw !== true).map((community) => community.address);
|
||||||
}, [directories, filter]);
|
}, [directories, filter]);
|
||||||
|
|
||||||
return filteredAddresses;
|
return filteredAddresses;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useFeed } from '@bitsocialnet/bitsocial-react-hooks';
|
|||||||
import useFeedsStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/feeds';
|
import useFeedsStore from '@bitsocialnet/bitsocial-react-hooks/dist/stores/feeds';
|
||||||
import { useDirectoryByAddress } from './use-directories';
|
import { useDirectoryByAddress } from './use-directories';
|
||||||
import { useBoardFeedPageSize } from './use-board-feed-page-size';
|
import { useBoardFeedPageSize } from './use-board-feed-page-size';
|
||||||
|
import { useCommunityIdentifier } from './use-community-identifiers';
|
||||||
import { findPostPageInFeed, findPostPageInLoadedBoardFeeds, type FeedsOptionsLike, type LoadedFeedsLike } from '../lib/utils/post-page-resolution';
|
import { findPostPageInFeed, findPostPageInLoadedBoardFeeds, type FeedsOptionsLike, type LoadedFeedsLike } from '../lib/utils/post-page-resolution';
|
||||||
|
|
||||||
interface UsePostPageNumberOptions {
|
interface UsePostPageNumberOptions {
|
||||||
@@ -29,6 +30,7 @@ export function usePostPageNumber({
|
|||||||
enabled = true,
|
enabled = true,
|
||||||
}: UsePostPageNumberOptions): number | undefined {
|
}: UsePostPageNumberOptions): number | undefined {
|
||||||
const communityAddress = requestedCommunityAddress ?? legacyCommunityAddress;
|
const communityAddress = requestedCommunityAddress ?? legacyCommunityAddress;
|
||||||
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
|
|
||||||
const community = useDirectoryByAddress(communityAddress);
|
const community = useDirectoryByAddress(communityAddress);
|
||||||
const { guiPostsPerPage, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
|
const { guiPostsPerPage, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
|
||||||
@@ -52,12 +54,12 @@ export function usePostPageNumber({
|
|||||||
() =>
|
() =>
|
||||||
canResolve
|
canResolve
|
||||||
? {
|
? {
|
||||||
communityAddresses: [communityAddress!],
|
communities: communityIdentifier ? [communityIdentifier] : [],
|
||||||
sortType: 'active' as const,
|
sortType: 'active' as const,
|
||||||
postsPerPage: paginationFeedPostsPerPage,
|
postsPerPage: paginationFeedPostsPerPage,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
[canResolve, communityAddress, paginationFeedPostsPerPage],
|
[canResolve, communityIdentifier, paginationFeedPostsPerPage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { feed: preloadFeed } = useFeed(preloadOptions);
|
const { feed: preloadFeed } = useFeed(preloadOptions);
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useAccount, useResolvedAuthorAddress } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
|
|
||||||
|
type PublishAuthorDomainBlockReason = 'resolving' | 'unresolved' | 'mismatch';
|
||||||
|
|
||||||
|
const isDomainAddress = (address: unknown): address is string => typeof address === 'string' && address.includes('.');
|
||||||
|
|
||||||
|
export const getPublishAuthorDomainErrorMessage = (reason: PublishAuthorDomainBlockReason) => {
|
||||||
|
if (reason === 'mismatch') {
|
||||||
|
return 'Your Bitsocial Account address belongs to another account.';
|
||||||
|
}
|
||||||
|
if (reason === 'resolving') {
|
||||||
|
return 'Your Bitsocial Account address is still being verified. Try again in a moment.';
|
||||||
|
}
|
||||||
|
return 'Your Bitsocial Account address is not resolved yet.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePublishAuthorDomainGuard = () => {
|
||||||
|
const account = useAccount();
|
||||||
|
const authorAddress = account?.author?.address;
|
||||||
|
const hasDomainAuthor = isDomainAddress(authorAddress);
|
||||||
|
const { resolvedAddress, state } = useResolvedAuthorAddress({
|
||||||
|
author: hasDomainAuthor ? account?.author : undefined,
|
||||||
|
cache: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockedReason = useMemo<PublishAuthorDomainBlockReason | undefined>(() => {
|
||||||
|
if (!hasDomainAuthor) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'succeeded') {
|
||||||
|
if (!resolvedAddress) {
|
||||||
|
return 'unresolved';
|
||||||
|
}
|
||||||
|
return resolvedAddress === account?.signer?.address ? undefined : 'mismatch';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'failed') {
|
||||||
|
return 'unresolved';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'resolving';
|
||||||
|
}, [account?.signer?.address, hasDomainAuthor, resolvedAddress, state]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
account,
|
||||||
|
blockedReason,
|
||||||
|
hasDomainAuthor,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePublishAuthorDomainGuard;
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Comment, usePublishComment } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, usePublishComment } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import usePublishPostStore from '../stores/use-publish-post-store';
|
import usePublishPostStore from '../stores/use-publish-post-store';
|
||||||
import useChallengesStore from '../stores/use-challenges-store';
|
import useChallengesStore from '../stores/use-challenges-store';
|
||||||
|
import usePublishAuthorDomainGuard, { getPublishAuthorDomainErrorMessage } from './use-publish-author-domain-guard';
|
||||||
|
|
||||||
type UsePublishPostOptions = {
|
type UsePublishPostOptions = {
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
@@ -23,6 +24,8 @@ const usePublishPost = ({ communityAddress: requestedCommunityAddress, subplebbi
|
|||||||
const resetPublishPostStore = usePublishPostStore((state) => state.resetPublishPostStore);
|
const resetPublishPostStore = usePublishPostStore((state) => state.resetPublishPostStore);
|
||||||
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
||||||
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
||||||
|
const [publishPostError, setPublishPostError] = useState<string | null>(null);
|
||||||
|
const { blockedReason } = usePublishAuthorDomainGuard();
|
||||||
const abandonCurrentPublish = useCallback(async () => {
|
const abandonCurrentPublish = useCallback(async () => {
|
||||||
await abandonPublishRef.current?.();
|
await abandonPublishRef.current?.();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -88,11 +91,26 @@ const usePublishPost = ({ communityAddress: requestedCommunityAddress, subplebbi
|
|||||||
const { index, publishComment, abandonPublish } = usePublishComment(publishOptionsWithAbandon);
|
const { index, publishComment, abandonPublish } = usePublishComment(publishOptionsWithAbandon);
|
||||||
abandonPublishRef.current = abandonPublish;
|
abandonPublishRef.current = abandonPublish;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPublishPostError(null);
|
||||||
|
}, [author?.displayName, blockedReason, communityAddress, content, link, spoiler, title]);
|
||||||
|
|
||||||
|
const publishPost = useCallback(() => {
|
||||||
|
if (blockedReason) {
|
||||||
|
setPublishPostError(getPublishAuthorDomainErrorMessage(blockedReason));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPublishPostError(null);
|
||||||
|
return publishComment();
|
||||||
|
}, [blockedReason, publishComment]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setPublishPostOptions,
|
setPublishPostOptions,
|
||||||
resetPublishPostOptions,
|
resetPublishPostOptions,
|
||||||
postIndex: index,
|
postIndex: index,
|
||||||
publishPost: publishComment,
|
publishPost,
|
||||||
|
publishPostError,
|
||||||
publishPostOptions: publishCommentOptions,
|
publishPostOptions: publishCommentOptions,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getQuotedCidsFromContent, mergeQuotedCids } from '../lib/utils/reply-qu
|
|||||||
import { extractUnresolvedExternalQuoteReferences, getExternalQuoteStatusMessage } from '../lib/utils/external-quote-utils';
|
import { extractUnresolvedExternalQuoteReferences, getExternalQuoteStatusMessage } from '../lib/utils/external-quote-utils';
|
||||||
import { resolveExternalQuoteTarget } from '../lib/utils/external-quote-resolver';
|
import { resolveExternalQuoteTarget } from '../lib/utils/external-quote-resolver';
|
||||||
import useChallengesStore from '../stores/use-challenges-store';
|
import useChallengesStore from '../stores/use-challenges-store';
|
||||||
|
import usePublishAuthorDomainGuard, { getPublishAuthorDomainErrorMessage } from './use-publish-author-domain-guard';
|
||||||
|
|
||||||
type UsePublishReplyOptions = {
|
type UsePublishReplyOptions = {
|
||||||
cid: string;
|
cid: string;
|
||||||
@@ -36,6 +37,7 @@ const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, sub
|
|||||||
const setPublishReplyStore = usePublishReplyStore((state) => state.setPublishReplyStore);
|
const setPublishReplyStore = usePublishReplyStore((state) => state.setPublishReplyStore);
|
||||||
const resetPublishReplyStore = usePublishReplyStore((state) => state.resetPublishReplyStore);
|
const resetPublishReplyStore = usePublishReplyStore((state) => state.resetPublishReplyStore);
|
||||||
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
||||||
|
const { blockedReason } = usePublishAuthorDomainGuard();
|
||||||
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
||||||
const startedPublishRequestIdRef = useRef(0);
|
const startedPublishRequestIdRef = useRef(0);
|
||||||
const [resolvedExternalQuotedCids, setResolvedExternalQuotedCids] = useState<string[] | undefined>();
|
const [resolvedExternalQuotedCids, setResolvedExternalQuotedCids] = useState<string[] | undefined>();
|
||||||
@@ -143,7 +145,7 @@ const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, sub
|
|||||||
setPublishReplyError(null);
|
setPublishReplyError(null);
|
||||||
setPublishReplyStateMessage(null);
|
setPublishReplyStateMessage(null);
|
||||||
setIsResolvingExternalQuotes(false);
|
setIsResolvingExternalQuotes(false);
|
||||||
}, [content, communityAddress]);
|
}, [blockedReason, content, communityAddress]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingPublishRequestId === 0 || pendingPublishRequestId === startedPublishRequestIdRef.current) {
|
if (pendingPublishRequestId === 0 || pendingPublishRequestId === startedPublishRequestIdRef.current) {
|
||||||
@@ -157,6 +159,12 @@ const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, sub
|
|||||||
const publishReply = useCallback(async () => {
|
const publishReply = useCallback(async () => {
|
||||||
setPublishReplyError(null);
|
setPublishReplyError(null);
|
||||||
|
|
||||||
|
if (blockedReason) {
|
||||||
|
setPublishReplyStateMessage(null);
|
||||||
|
setPublishReplyError(getPublishAuthorDomainErrorMessage(blockedReason));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (publishResolvableQuoteReferences.length === 0) {
|
if (publishResolvableQuoteReferences.length === 0) {
|
||||||
setResolvedExternalQuotedCids(undefined);
|
setResolvedExternalQuotedCids(undefined);
|
||||||
setPublishReplyStateMessage(null);
|
setPublishReplyStateMessage(null);
|
||||||
@@ -205,7 +213,7 @@ const usePublishReply = ({ cid, communityAddress: requestedCommunityAddress, sub
|
|||||||
} finally {
|
} finally {
|
||||||
setIsResolvingExternalQuotes(false);
|
setIsResolvingExternalQuotes(false);
|
||||||
}
|
}
|
||||||
}, [account, directories, publishResolvableQuoteReferences, t]);
|
}, [account, blockedReason, directories, publishResolvableQuoteReferences, t]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isResolvingExternalQuotes,
|
isResolvingExternalQuotes,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|||||||
import { useClientsStates, useCommunity, useCommunitiesStates } from '@bitsocialnet/bitsocial-react-hooks';
|
import { useClientsStates, useCommunity, useCommunitiesStates } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import getShortAddress from '../lib/get-short-address';
|
import getShortAddress from '../lib/get-short-address';
|
||||||
|
import { useCommunityIdentifiers } from './use-community-identifiers';
|
||||||
|
|
||||||
interface CommentOrCommunity {
|
interface CommentOrCommunity {
|
||||||
state?: string;
|
state?: string;
|
||||||
@@ -110,13 +111,16 @@ const useStateString = (commentOrCommunity: CommentOrCommunity): string | undefi
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useFeedStateString = (communityAddresses?: string[]): string | undefined => {
|
export const useFeedStateString = (communityAddresses?: string[]): string | undefined => {
|
||||||
|
const communities = useCommunityIdentifiers(communityAddresses);
|
||||||
|
|
||||||
// single community feed state string
|
// single community feed state string
|
||||||
const communityAddress = communityAddresses?.length === 1 ? communityAddresses[0] : undefined;
|
const communityAddress = communityAddresses?.length === 1 ? communityAddresses[0] : undefined;
|
||||||
const community = useCommunity(communityAddress ? { communityAddress } : undefined);
|
const communityIdentifier = communityAddress ? communities[0] : undefined;
|
||||||
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const singleCommunityFeedStateString = sanitizeSingleFeedLoadingState(useStateString(community));
|
const singleCommunityFeedStateString = sanitizeSingleFeedLoadingState(useStateString(community));
|
||||||
|
|
||||||
// multiple community feed state string
|
// multiple community feed state string
|
||||||
const { states } = useCommunitiesStates({ communityAddresses });
|
const { states } = useCommunitiesStates({ communities });
|
||||||
|
|
||||||
const multipleCommunitiesFeedStateString = useMemo(() => {
|
const multipleCommunitiesFeedStateString = useMemo(() => {
|
||||||
if (communityAddress) {
|
if (communityAddress) {
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export interface CommentWithCid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Minimal FeedOptions shape for board-feed filtering */
|
/** Minimal FeedOptions shape for board-feed filtering */
|
||||||
|
type CommunityIdentifierLike = {
|
||||||
|
name?: string;
|
||||||
|
publicKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type LegacyFeedOptionsLike = {
|
type LegacyFeedOptionsLike = {
|
||||||
subplebbitAddresses?: string[];
|
subplebbitAddresses?: string[];
|
||||||
sortType: string;
|
sortType: string;
|
||||||
@@ -21,6 +26,7 @@ type LegacyFeedOptionsLike = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface FeedOptionsLike {
|
export interface FeedOptionsLike {
|
||||||
|
communities?: CommunityIdentifierLike[];
|
||||||
communityAddresses?: string[];
|
communityAddresses?: string[];
|
||||||
sortType: string;
|
sortType: string;
|
||||||
postsPerPage?: number;
|
postsPerPage?: number;
|
||||||
@@ -59,26 +65,29 @@ export function findPostPageInFeed(feed: CommentWithCid[], postCid: string, guiP
|
|||||||
* - single-board feed (one community)
|
* - single-board feed (one community)
|
||||||
* - no filter, no newerThan, no modQueue, no accountComments
|
* - no filter, no newerThan, no modQueue, no accountComments
|
||||||
*/
|
*/
|
||||||
const getCommunityAddresses = (opts: FeedOptionsLike | LegacyFeedOptionsLike): string[] => {
|
const getCommunityIdentifiers = (opts: FeedOptionsLike | LegacyFeedOptionsLike): CommunityIdentifierLike[] => {
|
||||||
|
if ('communities' in opts && Array.isArray(opts.communities)) {
|
||||||
|
return opts.communities;
|
||||||
|
}
|
||||||
if ('communityAddresses' in opts && Array.isArray(opts.communityAddresses)) {
|
if ('communityAddresses' in opts && Array.isArray(opts.communityAddresses)) {
|
||||||
return opts.communityAddresses;
|
return opts.communityAddresses.map((communityAddress) => ({ name: communityAddress }));
|
||||||
}
|
}
|
||||||
if ('subplebbitAddresses' in opts && Array.isArray(opts.subplebbitAddresses)) {
|
if ('subplebbitAddresses' in opts && Array.isArray(opts.subplebbitAddresses)) {
|
||||||
return opts.subplebbitAddresses;
|
return opts.subplebbitAddresses.map((communityAddress) => ({ name: communityAddress }));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supports both canonical `communityAddresses` and legacy `subplebbitAddresses`.
|
* Supports canonical `communities` and legacy string-array feed options.
|
||||||
*/
|
*/
|
||||||
export function isBoardFeedOptions(opts: FeedOptionsLike | LegacyFeedOptionsLike, communityAddress: string): boolean {
|
export function isBoardFeedOptions(opts: FeedOptionsLike | LegacyFeedOptionsLike, communityAddress: string): boolean {
|
||||||
const communityAddresses = getCommunityAddresses(opts);
|
const communities = getCommunityIdentifiers(opts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
opts.sortType === 'active' &&
|
opts.sortType === 'active' &&
|
||||||
communityAddresses.length === 1 &&
|
communities.length === 1 &&
|
||||||
communityAddresses[0] === communityAddress &&
|
(communities[0]?.name === communityAddress || communities[0]?.publicKey === communityAddress) &&
|
||||||
!opts.filter &&
|
!opts.filter &&
|
||||||
opts.newerThan == null &&
|
opts.newerThan == null &&
|
||||||
!opts.modQueue &&
|
!opts.modQueue &&
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils'
|
|||||||
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
||||||
import { removeMarkdown } from '../../lib/utils/post-utils';
|
import { removeMarkdown } from '../../lib/utils/post-utils';
|
||||||
import { useDirectories } from '../../hooks/use-directories';
|
import { useDirectories } from '../../hooks/use-directories';
|
||||||
|
import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
|
||||||
import styles from './archive.module.css';
|
import styles from './archive.module.css';
|
||||||
|
|
||||||
type BoardFeedComment = {
|
type BoardFeedComment = {
|
||||||
@@ -176,19 +177,21 @@ const Archive = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const communityAddresses = useMemo(() => (subplebbitAddress ? [subplebbitAddress] : []), [subplebbitAddress]);
|
const communityAddresses = useMemo(() => (subplebbitAddress ? [subplebbitAddress] : []), [subplebbitAddress]);
|
||||||
|
const communities = useCommunityIdentifiers(communityAddresses);
|
||||||
|
const communityIdentifier = useCommunityIdentifier(subplebbitAddress);
|
||||||
|
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
communityAddresses,
|
communities,
|
||||||
sortType: BOARD_SORT_TYPE,
|
sortType: BOARD_SORT_TYPE,
|
||||||
filter: archiveFilter,
|
filter: archiveFilter,
|
||||||
}),
|
}),
|
||||||
[communityAddresses, archiveFilter],
|
[communities, archiveFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { feed, hasMore, loadMore } = useFeed(feedOptions);
|
const { feed, hasMore, loadMore } = useFeed(feedOptions);
|
||||||
const loadingState = useFeedStateString(communityAddresses) || (hasMore ? t('loading_feed') : t('no_threads'));
|
const loadingState = useFeedStateString(communityAddresses) || (hasMore ? t('loading_feed') : t('no_threads'));
|
||||||
const community = useCommunity({ communityAddress: subplebbitAddress });
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { error: communityError } = community || {};
|
const { error: communityError } = community || {};
|
||||||
const archiveWindowInDays = useMemo(() => getArchiveWindowInDays(feed), [feed]);
|
const archiveWindowInDays = useMemo(() => getArchiveWindowInDays(feed), [feed]);
|
||||||
const isLoading = feed.length === 0 && hasMore;
|
const isLoading = feed.length === 0 && hasMore;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import styles from './board.module.css';
|
|||||||
import mobileFooterStyles from '../../components/footer/footer.module.css';
|
import mobileFooterStyles from '../../components/footer/footer.module.css';
|
||||||
import { shouldShowSnow } from '../../lib/snow';
|
import { shouldShowSnow } from '../../lib/snow';
|
||||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||||
import { useDirectoryAddresses, useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||||
|
import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
|
||||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||||
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
||||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||||
@@ -118,7 +119,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
|
|||||||
return resolvedAddressFromUrl;
|
return resolvedAddressFromUrl;
|
||||||
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
|
}, [boardIdentifierProp, directories, resolvedAddressFromUrl]);
|
||||||
|
|
||||||
const directoryAddresses = useDirectoryAddresses();
|
|
||||||
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
|
const filteredDirectoryAddresses = useFilteredDirectoryAddresses();
|
||||||
|
|
||||||
const account = useAccount();
|
const account = useAccount();
|
||||||
@@ -137,7 +137,9 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
|
|||||||
return accountCommunityAddresses;
|
return accountCommunityAddresses;
|
||||||
}
|
}
|
||||||
return [communityAddress];
|
return [communityAddress];
|
||||||
}, [isInAllView, isInSubscriptionsView, isInModView, communityAddress, directoryAddresses, filteredDirectoryAddresses, subscriptions, accountCommunityAddresses]);
|
}, [isInAllView, isInSubscriptionsView, isInModView, communityAddress, filteredDirectoryAddresses, subscriptions, accountCommunityAddresses]);
|
||||||
|
const communities = useCommunityIdentifiers(communityAddresses);
|
||||||
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
|
|
||||||
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
|
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
|
||||||
const setEnableInfiniteScroll = useFeedViewSettingsStore((state) => state.setEnableInfiniteScroll);
|
const setEnableInfiniteScroll = useFeedViewSettingsStore((state) => state.setEnableInfiniteScroll);
|
||||||
@@ -157,12 +159,12 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
|
|||||||
|
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
communityAddresses,
|
communities,
|
||||||
sortType: BOARD_SORT_TYPE,
|
sortType: BOARD_SORT_TYPE,
|
||||||
postsPerPage: effectiveInfiniteScroll ? infiniteFeedPostsPerPage : paginationFeedPostsPerPage,
|
postsPerPage: effectiveInfiniteScroll ? infiniteFeedPostsPerPage : paginationFeedPostsPerPage,
|
||||||
filter: excludeArchivedFilter,
|
filter: excludeArchivedFilter,
|
||||||
}),
|
}),
|
||||||
[communityAddresses, effectiveInfiniteScroll, infiniteFeedPostsPerPage, paginationFeedPostsPerPage, excludeArchivedFilter],
|
[communities, effectiveInfiniteScroll, infiniteFeedPostsPerPage, paginationFeedPostsPerPage, excludeArchivedFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
||||||
@@ -288,7 +290,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
|
|||||||
const communityTitle = useCommunityField(communityAddress, (community) => community?.title);
|
const communityTitle = useCommunityField(communityAddress, (community) => community?.title);
|
||||||
const shortAddress = useCommunityField(communityAddress, (community) => community?.shortAddress);
|
const shortAddress = useCommunityField(communityAddress, (community) => community?.shortAddress);
|
||||||
// useCommunityField only reads from store, doesn't trigger fetching
|
// useCommunityField only reads from store, doesn't trigger fetching
|
||||||
const communityData = useCommunity({ communityAddress });
|
const communityData = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { error: communityError, state: communityState } = communityData || {};
|
const { error: communityError, state: communityState } = communityData || {};
|
||||||
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : communityTitle;
|
const title = isInAllView ? t('all') : isInSubscriptionsView ? t('subscriptions') : isInModView ? t('mod') : communityTitle;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Comment, useAccount, useCommunity, useFeed, useAccountComments } from '@bitsocialnet/bitsocial-react-hooks';
|
import { Comment, useAccount, useCommunity, useFeed, useAccountComments } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso';
|
||||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||||
|
import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
|
||||||
import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
|
import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size';
|
||||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||||
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
||||||
@@ -232,6 +233,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
// Only include communityAddress if it's defined
|
// Only include communityAddress if it's defined
|
||||||
return communityAddress ? [communityAddress] : [];
|
return communityAddress ? [communityAddress] : [];
|
||||||
}, [isInAllView, isInSubscriptionsView, communityAddress, filteredDirectoryAddresses, subscriptions]);
|
}, [isInAllView, isInSubscriptionsView, communityAddress, filteredDirectoryAddresses, subscriptions]);
|
||||||
|
const communities = useCommunityIdentifiers(communityAddresses);
|
||||||
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
|
|
||||||
const { imageSize, showOPComment } = useCatalogStyleStore();
|
const { imageSize, showOPComment } = useCatalogStyleStore();
|
||||||
const columnWidth = imageSize === 'Large' ? 270 : 180;
|
const columnWidth = imageSize === 'Large' ? 270 : 180;
|
||||||
@@ -272,22 +275,12 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
|
|
||||||
const feedOptions = useMemo(() => {
|
const feedOptions = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
communityAddresses,
|
communities,
|
||||||
sortType: feedSortType,
|
sortType: feedSortType,
|
||||||
postsPerPage: isMultiboard ? multiboardCatalogPostsPerPage : paginationFeedPostsPerPage,
|
postsPerPage: isMultiboard ? multiboardCatalogPostsPerPage : paginationFeedPostsPerPage,
|
||||||
filter: createCombinedFilter(filterItems, searchText, communityAddress || 'all', handleFilterMatch),
|
filter: createCombinedFilter(filterItems, searchText, communityAddress || 'all', handleFilterMatch),
|
||||||
};
|
};
|
||||||
}, [
|
}, [communities, feedSortType, isMultiboard, paginationFeedPostsPerPage, multiboardCatalogPostsPerPage, filterItems, searchText, communityAddress, handleFilterMatch]);
|
||||||
communityAddresses,
|
|
||||||
feedSortType,
|
|
||||||
isMultiboard,
|
|
||||||
paginationFeedPostsPerPage,
|
|
||||||
multiboardCatalogPostsPerPage,
|
|
||||||
filterItems,
|
|
||||||
searchText,
|
|
||||||
communityAddress,
|
|
||||||
handleFilterMatch,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
||||||
const accountCommentLookupOptions = useMemo(
|
const accountCommentLookupOptions = useMemo(
|
||||||
@@ -370,7 +363,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
|||||||
}
|
}
|
||||||
}, [reset, setResetFunction, isVisible]);
|
}, [reset, setResetFunction, isVisible]);
|
||||||
|
|
||||||
const community = useCommunity({ communityAddress });
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { error, shortAddress, state, title } = community || {};
|
const { error, shortAddress, state, title } = community || {};
|
||||||
|
|
||||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||||||
import { useCommunities } from '@bitsocialnet/bitsocial-react-hooks';
|
import { useCommunities } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import styles from './home.module.css';
|
import styles from './home.module.css';
|
||||||
import { useDirectories, useDirectoryAddresses } from '../../hooks/use-directories';
|
import { useDirectories, useDirectoryAddresses } from '../../hooks/use-directories';
|
||||||
|
import { useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
|
||||||
import { CommunityStatsCollector, useCommunitiesStatsStore } from '../../hooks/use-communities-stats';
|
import { CommunityStatsCollector, useCommunitiesStatsStore } from '../../hooks/use-communities-stats';
|
||||||
import PopularThreadsBox from './popular-threads-box';
|
import PopularThreadsBox from './popular-threads-box';
|
||||||
import BoardsList from './boards-list';
|
import BoardsList from './boards-list';
|
||||||
@@ -189,7 +190,8 @@ export const HomeLogo = () => {
|
|||||||
const Home = () => {
|
const Home = () => {
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
const directoryAddresses = useDirectoryAddresses();
|
const directoryAddresses = useDirectoryAddresses();
|
||||||
const { communities } = useCommunities({ communityAddresses: directoryAddresses });
|
const directoryCommunities = useCommunityIdentifiers(directoryAddresses);
|
||||||
|
const { communities } = useCommunities({ communities: directoryCommunities });
|
||||||
const { closeDirectoryModal } = useDirectoryModalStore();
|
const { closeDirectoryModal } = useDirectoryModalStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import useChallengesStore from '../../stores/use-challenges-store';
|
|||||||
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
|
||||||
import Tooltip from '../../components/tooltip';
|
import Tooltip from '../../components/tooltip';
|
||||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||||
|
import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import { useCurrentTime } from '../../hooks/use-current-time';
|
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||||
import { Post } from '../post/post';
|
import { Post } from '../post/post';
|
||||||
@@ -728,7 +729,8 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [boardIdentifier, directories]);
|
}, [boardIdentifier, directories]);
|
||||||
const community = useCommunity({ communityAddress: resolvedAddress });
|
const resolvedCommunity = useCommunityIdentifier(resolvedAddress);
|
||||||
|
const community = useCommunity(resolvedCommunity ? { community: resolvedCommunity } : undefined);
|
||||||
|
|
||||||
const communityAddresses = useMemo(() => {
|
const communityAddresses = useMemo(() => {
|
||||||
if (resolvedAddress) {
|
if (resolvedAddress) {
|
||||||
@@ -758,14 +760,15 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
|||||||
const shouldFetch = !isBoardAccessLoading && communityAddresses.length > 0 && hasBoardAccess;
|
const shouldFetch = !isBoardAccessLoading && communityAddresses.length > 0 && hasBoardAccess;
|
||||||
|
|
||||||
const feedAddresses = shouldFetch ? communityAddresses : [];
|
const feedAddresses = shouldFetch ? communityAddresses : [];
|
||||||
|
const feedCommunities = useCommunityIdentifiers(feedAddresses);
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
communityAddresses: feedAddresses,
|
communities: feedCommunities,
|
||||||
modQueue: ['pendingApproval'],
|
modQueue: ['pendingApproval'],
|
||||||
sortType: 'new' as const,
|
sortType: 'new' as const,
|
||||||
postsPerPage: 200,
|
postsPerPage: 200,
|
||||||
}),
|
}),
|
||||||
[feedAddresses],
|
[feedCommunities],
|
||||||
);
|
);
|
||||||
const { feed } = useFeed(feedOptions);
|
const { feed } = useFeed(feedOptions);
|
||||||
|
|
||||||
@@ -802,18 +805,20 @@ const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProp
|
|||||||
if (resolvedAddress) return [resolvedAddress];
|
if (resolvedAddress) return [resolvedAddress];
|
||||||
return accountCommunityAddresses;
|
return accountCommunityAddresses;
|
||||||
}, [resolvedAddress, accountCommunityAddresses]);
|
}, [resolvedAddress, accountCommunityAddresses]);
|
||||||
|
const communities = useCommunityIdentifiers(communityAddresses);
|
||||||
|
|
||||||
const communityAddress = communityAddresses[0];
|
const communityAddress = communityAddresses[0];
|
||||||
const community = useCommunity({ communityAddress });
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { error: communityError } = community || {};
|
const { error: communityError } = community || {};
|
||||||
|
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
communityAddresses,
|
communities,
|
||||||
modQueue: ['pendingApproval'],
|
modQueue: ['pendingApproval'],
|
||||||
postsPerPage: 50,
|
postsPerPage: 50,
|
||||||
}),
|
}),
|
||||||
[communityAddresses],
|
[communities],
|
||||||
);
|
);
|
||||||
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { isAllView } from '../../lib/utils/view-utils';
|
import { isAllView } from '../../lib/utils/view-utils';
|
||||||
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
||||||
import { useDirectories } from '../../hooks/use-directories';
|
import { useDirectories } from '../../hooks/use-directories';
|
||||||
|
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
|
||||||
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
||||||
import { areSameBoardAddress, isDirectoryBoard } from '../../lib/utils/route-utils';
|
import { areSameBoardAddress, isDirectoryBoard } from '../../lib/utils/route-utils';
|
||||||
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
||||||
@@ -193,6 +194,7 @@ const PostPage = () => {
|
|||||||
const comment = useCommentWithFeedCache({ commentCid, autoUpdate: autoUpdateEnabled });
|
const comment = useCommentWithFeedCache({ commentCid, autoUpdate: autoUpdateEnabled });
|
||||||
const commentCommunityAddress = getCommentCommunityAddress(comment);
|
const commentCommunityAddress = getCommentCommunityAddress(comment);
|
||||||
const communityAddress = resolvedCommunityAddress ?? commentCommunityAddress;
|
const communityAddress = resolvedCommunityAddress ?? commentCommunityAddress;
|
||||||
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
const consumedThreadTopScrollRef = useRef<string | null>(null);
|
const consumedThreadTopScrollRef = useRef<string | null>(null);
|
||||||
const previousThreadCidRef = useRef<string>();
|
const previousThreadCidRef = useRef<string>();
|
||||||
const lastProcessedUpdateRequestIdRef = useRef(0);
|
const lastProcessedUpdateRequestIdRef = useRef(0);
|
||||||
@@ -204,7 +206,7 @@ const PostPage = () => {
|
|||||||
}
|
}
|
||||||
}, [commentCommunityAddress, resolvedCommunityAddress, navigate]);
|
}, [commentCommunityAddress, resolvedCommunityAddress, navigate]);
|
||||||
|
|
||||||
const community = useCommunity({ communityAddress });
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { error: communityError, shortAddress, title } = community || {};
|
const { error: communityError, shortAddress, title } = community || {};
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { useCommunity } from '@bitsocialnet/bitsocial-react-hooks';
|
import { useCommunity } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import { Footer, HomeLogo } from '../home';
|
import { Footer, HomeLogo } from '../home';
|
||||||
import { useDirectories, DirectoryCommunity, findDirectoryByAddress } from '../../hooks/use-directories';
|
import { useDirectories, DirectoryCommunity, findDirectoryByAddress } from '../../hooks/use-directories';
|
||||||
|
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
|
||||||
import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils';
|
import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils';
|
||||||
import Markdown from '../../components/markdown';
|
import Markdown from '../../components/markdown';
|
||||||
import styles from './rules.module.css';
|
import styles from './rules.module.css';
|
||||||
@@ -22,7 +23,8 @@ const getBoardName = (title?: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress: string; directories: DirectoryCommunity[] }) => {
|
const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress: string; directories: DirectoryCommunity[] }) => {
|
||||||
const community = useCommunity({ communityAddress });
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
||||||
|
const community = useCommunity(communityIdentifier ? { community: communityIdentifier } : undefined);
|
||||||
const { rules, state, title, shortAddress } = community || {};
|
const { rules, state, title, shortAddress } = community || {};
|
||||||
|
|
||||||
let loadingText: string | null = null;
|
let loadingText: string | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user