2026-04-20 22:11:36 +07:00
|
|
|
import type { Comment } from '@bitsocial/bitsocial-react-hooks';
|
2026-05-23 15:36:39 +07:00
|
|
|
import type { DirectoryCommunity } from '../../hooks/use-directories';
|
2026-04-19 13:45:24 +07:00
|
|
|
import { getCommentCommunityAddress } from './comment-utils';
|
2026-05-23 15:36:39 +07:00
|
|
|
import { hasModQueueAccessRole } from './mod-access';
|
2026-05-15 18:35:41 +07:00
|
|
|
import { isPendingApprovalRejected } from './pending-approval-moderation';
|
2026-05-23 15:36:39 +07:00
|
|
|
import { areSameBoardAddress, getBoardPath } from './route-utils';
|
2026-04-19 13:45:24 +07:00
|
|
|
import { getThreadTopNavigationState } from './thread-scroll-utils';
|
|
|
|
|
|
|
|
|
|
type ModQueueCommentLike = {
|
|
|
|
|
approved?: boolean;
|
|
|
|
|
author?: Comment['author'];
|
|
|
|
|
cid?: string;
|
|
|
|
|
commentModeration?: Comment['commentModeration'];
|
|
|
|
|
communityAddress?: string;
|
|
|
|
|
content?: Comment['content'];
|
|
|
|
|
deleted?: Comment['deleted'];
|
|
|
|
|
link?: Comment['link'];
|
|
|
|
|
linkHeight?: Comment['linkHeight'];
|
|
|
|
|
linkWidth?: Comment['linkWidth'];
|
|
|
|
|
number?: Comment['number'];
|
|
|
|
|
parentCid?: Comment['parentCid'];
|
|
|
|
|
pendingApproval?: boolean;
|
|
|
|
|
postCid?: Comment['postCid'];
|
|
|
|
|
reason?: Comment['reason'];
|
|
|
|
|
removed?: boolean;
|
|
|
|
|
replyCount?: Comment['replyCount'];
|
|
|
|
|
threadCid?: Comment['threadCid'];
|
|
|
|
|
thumbnailUrl?: Comment['thumbnailUrl'];
|
|
|
|
|
timestamp?: Comment['timestamp'];
|
|
|
|
|
title?: Comment['title'];
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-19 15:02:36 +07:00
|
|
|
const emptyDismissedCommentCids = new Set<string>();
|
|
|
|
|
|
2026-04-19 13:45:24 +07:00
|
|
|
export type QueuedCommentRouteState = {
|
|
|
|
|
scrollThreadContainerCid?: string;
|
2026-04-19 15:02:36 +07:00
|
|
|
queuedComment?: QueuedCommentSnapshot;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type QueuedCommentSnapshot = {
|
|
|
|
|
approved?: Comment['approved'];
|
|
|
|
|
author?: Comment['author'];
|
|
|
|
|
cid?: Comment['cid'];
|
|
|
|
|
commentModeration?: Comment['commentModeration'];
|
|
|
|
|
communityAddress?: string;
|
|
|
|
|
content?: Comment['content'];
|
|
|
|
|
deleted?: Comment['deleted'];
|
|
|
|
|
link?: Comment['link'];
|
|
|
|
|
linkHeight?: Comment['linkHeight'];
|
|
|
|
|
linkWidth?: Comment['linkWidth'];
|
|
|
|
|
number?: Comment['number'];
|
|
|
|
|
parentCid?: Comment['parentCid'];
|
|
|
|
|
pendingApproval?: Comment['pendingApproval'];
|
|
|
|
|
postCid?: Comment['postCid'];
|
|
|
|
|
reason?: Comment['reason'];
|
|
|
|
|
removed?: Comment['removed'];
|
|
|
|
|
replyCount?: Comment['replyCount'];
|
|
|
|
|
threadCid?: Comment['threadCid'];
|
|
|
|
|
thumbnailUrl?: Comment['thumbnailUrl'];
|
|
|
|
|
timestamp?: Comment['timestamp'];
|
|
|
|
|
title?: Comment['title'];
|
2026-04-19 13:45:24 +07:00
|
|
|
};
|
|
|
|
|
|
2026-05-23 15:36:39 +07:00
|
|
|
export interface ModQueueBoardFilterGroup {
|
|
|
|
|
addresses: string[];
|
|
|
|
|
boardPath: string;
|
|
|
|
|
filterKey: string;
|
|
|
|
|
isDirectory: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ModQueueCommunityRoleSource {
|
|
|
|
|
roles?: Record<string, { role?: string } | undefined>;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 13:45:24 +07:00
|
|
|
export const getModQueueCommentRoute = (boardPath: string | undefined, commentCid: string | undefined): string | undefined =>
|
|
|
|
|
boardPath && commentCid ? `/${boardPath}/thread/${commentCid}` : undefined;
|
|
|
|
|
|
2026-05-23 15:36:39 +07:00
|
|
|
export const getModQueueBoardFilterKey = (communityAddress: string, directories: DirectoryCommunity[]): string => {
|
|
|
|
|
const boardPath = getBoardPath(communityAddress, directories);
|
|
|
|
|
return boardPath !== communityAddress ? boardPath : communityAddress;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addUniqueBoardAddress = (addresses: string[], address: string) => {
|
|
|
|
|
if (!addresses.some((existingAddress) => areSameBoardAddress(existingAddress, address))) {
|
|
|
|
|
addresses.push(address);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getModQueueBoardFilterGroups = (
|
|
|
|
|
accountCommunityAddresses: readonly string[],
|
|
|
|
|
directories: DirectoryCommunity[],
|
|
|
|
|
boardCodeGroups: readonly (readonly string[])[],
|
|
|
|
|
): ModQueueBoardFilterGroup[] => {
|
|
|
|
|
const groupsByFilterKey = new Map<string, ModQueueBoardFilterGroup>();
|
|
|
|
|
const filterKeyByAddress = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
for (const address of accountCommunityAddresses) {
|
|
|
|
|
const boardPath = getBoardPath(address, directories);
|
|
|
|
|
const filterKey = boardPath !== address ? boardPath : address;
|
|
|
|
|
const existingGroup = groupsByFilterKey.get(filterKey);
|
|
|
|
|
if (existingGroup) {
|
|
|
|
|
addUniqueBoardAddress(existingGroup.addresses, address);
|
|
|
|
|
} else {
|
|
|
|
|
groupsByFilterKey.set(filterKey, {
|
|
|
|
|
addresses: [address],
|
|
|
|
|
boardPath,
|
|
|
|
|
filterKey,
|
|
|
|
|
isDirectory: boardPath !== address,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
filterKeyByAddress.set(address, filterKey);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const orderedGroups: ModQueueBoardFilterGroup[] = [];
|
|
|
|
|
const seenFilterKeys = new Set<string>();
|
|
|
|
|
const addGroup = (filterKey: string | undefined) => {
|
|
|
|
|
if (!filterKey || seenFilterKeys.has(filterKey)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const group = groupsByFilterKey.get(filterKey);
|
|
|
|
|
if (!group) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
orderedGroups.push(group);
|
|
|
|
|
seenFilterKeys.add(filterKey);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const group of boardCodeGroups) {
|
|
|
|
|
for (const code of group) {
|
|
|
|
|
addGroup(code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const address of accountCommunityAddresses) {
|
|
|
|
|
addGroup(filterKeyByAddress.get(address));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return orderedGroups;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getModQueueSelectedBoardAddresses = (
|
|
|
|
|
communityAddresses: readonly string[],
|
|
|
|
|
selectedBoardFilter: string | null,
|
|
|
|
|
directories: DirectoryCommunity[],
|
|
|
|
|
): string[] | null => {
|
|
|
|
|
if (!selectedBoardFilter) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedFilterKey = getModQueueBoardFilterKey(selectedBoardFilter, directories);
|
|
|
|
|
const selectedAddresses = communityAddresses.filter((address) => {
|
|
|
|
|
if (areSameBoardAddress(address, selectedBoardFilter)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return getModQueueBoardFilterKey(address, directories) === selectedFilterKey;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return selectedAddresses.length > 0 ? selectedAddresses : [selectedBoardFilter];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getModeratedCommunityAddresses = ({
|
|
|
|
|
accountAddress,
|
|
|
|
|
accountCommunityAddresses,
|
|
|
|
|
candidateCommunityAddresses,
|
|
|
|
|
communities,
|
|
|
|
|
}: {
|
|
|
|
|
accountAddress: string | undefined;
|
|
|
|
|
accountCommunityAddresses: readonly string[];
|
|
|
|
|
candidateCommunityAddresses: readonly string[];
|
|
|
|
|
communities: readonly (ModQueueCommunityRoleSource | undefined)[];
|
|
|
|
|
}): string[] => {
|
|
|
|
|
const moderatedAddresses = [...accountCommunityAddresses];
|
|
|
|
|
|
|
|
|
|
if (!accountAddress) {
|
|
|
|
|
return moderatedAddresses;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
candidateCommunityAddresses.forEach((candidateAddress, index) => {
|
|
|
|
|
const role = communities[index]?.roles?.[accountAddress]?.role;
|
|
|
|
|
if (hasModQueueAccessRole(role)) {
|
|
|
|
|
addUniqueBoardAddress(moderatedAddresses, candidateAddress);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return moderatedAddresses;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-19 15:02:36 +07:00
|
|
|
export const getQueuedCommentSnapshot = (comment: ModQueueCommentLike | undefined): QueuedCommentSnapshot | undefined => {
|
2026-04-19 13:45:24 +07:00
|
|
|
if (!comment?.cid) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-19 15:02:36 +07:00
|
|
|
approved: comment.approved,
|
|
|
|
|
author: comment.author,
|
|
|
|
|
cid: comment.cid,
|
|
|
|
|
commentModeration: comment.commentModeration,
|
|
|
|
|
communityAddress: getCommentCommunityAddress(comment),
|
|
|
|
|
content: comment.content,
|
|
|
|
|
deleted: comment.deleted,
|
|
|
|
|
link: comment.link,
|
|
|
|
|
linkHeight: comment.linkHeight,
|
|
|
|
|
linkWidth: comment.linkWidth,
|
|
|
|
|
number: comment.number,
|
|
|
|
|
parentCid: comment.parentCid,
|
|
|
|
|
pendingApproval: comment.pendingApproval,
|
|
|
|
|
postCid: comment.postCid,
|
|
|
|
|
reason: comment.reason,
|
|
|
|
|
removed: comment.removed,
|
|
|
|
|
replyCount: comment.replyCount,
|
|
|
|
|
threadCid: comment.threadCid,
|
|
|
|
|
thumbnailUrl: comment.thumbnailUrl,
|
|
|
|
|
timestamp: comment.timestamp,
|
|
|
|
|
title: comment.title,
|
2026-04-19 13:45:24 +07:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-19 15:02:36 +07:00
|
|
|
export const getQueuedCommentRouteState = (comment: ModQueueCommentLike | undefined): QueuedCommentRouteState | undefined => {
|
|
|
|
|
const queuedComment = getQueuedCommentSnapshot(comment);
|
|
|
|
|
if (!queuedComment) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...(queuedComment.parentCid ? {} : getThreadTopNavigationState(queuedComment.cid)),
|
|
|
|
|
queuedComment,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 18:35:41 +07:00
|
|
|
export const shouldKeepQueuedCommentHistory = (comment: ModQueueCommentLike | undefined): boolean => comment?.approved === true || isPendingApprovalRejected(comment);
|
|
|
|
|
|
|
|
|
|
export const getVisibleQueuedCommentHistory = <T extends ModQueueCommentLike>(
|
|
|
|
|
feed: readonly ModQueueCommentLike[],
|
|
|
|
|
queuedCommentHistory: readonly T[],
|
|
|
|
|
communityAddresses: readonly string[],
|
|
|
|
|
): T[] => {
|
|
|
|
|
const liveCids = feed.reduce<Set<string>>((cids, comment) => {
|
|
|
|
|
if (comment.cid) cids.add(comment.cid);
|
|
|
|
|
return cids;
|
|
|
|
|
}, new Set());
|
|
|
|
|
|
|
|
|
|
return queuedCommentHistory.filter((comment) => {
|
|
|
|
|
const commentCommunityAddress = getCommentCommunityAddress(comment);
|
|
|
|
|
if (!comment.cid || !shouldKeepQueuedCommentHistory(comment) || liveCids.has(comment.cid) || !commentCommunityAddress) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return communityAddresses.some((communityAddress) => areSameBoardAddress(communityAddress, commentCommunityAddress));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-19 15:02:36 +07:00
|
|
|
export const filterVisibleModQueueFeed = <T extends ModQueueCommentLike>(
|
|
|
|
|
feed: T[],
|
|
|
|
|
selectedBoardFilter: string | null,
|
|
|
|
|
dismissedCommentCids: ReadonlySet<string> = emptyDismissedCommentCids,
|
2026-05-23 15:36:39 +07:00
|
|
|
selectedBoardFilterAddresses: readonly string[] | null = null,
|
2026-04-19 15:02:36 +07:00
|
|
|
): T[] =>
|
2026-04-19 13:45:24 +07:00
|
|
|
feed.filter((comment) => {
|
2026-04-19 15:02:36 +07:00
|
|
|
if (comment.cid && dismissedCommentCids.has(comment.cid)) {
|
2026-04-19 13:45:24 +07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!selectedBoardFilter) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 15:36:39 +07:00
|
|
|
const commentCommunityAddress = getCommentCommunityAddress(comment);
|
|
|
|
|
if (!commentCommunityAddress) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (selectedBoardFilterAddresses?.length) {
|
|
|
|
|
return selectedBoardFilterAddresses.some((address) => areSameBoardAddress(address, commentCommunityAddress));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return areSameBoardAddress(commentCommunityAddress, selectedBoardFilter);
|
2026-04-19 13:45:24 +07:00
|
|
|
});
|