mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(replies): order approved queue replies
This commit is contained in:
@@ -12,6 +12,7 @@ type TestComment = {
|
|||||||
content?: string;
|
content?: string;
|
||||||
index?: number;
|
index?: number;
|
||||||
number?: number;
|
number?: number;
|
||||||
|
pendingApproval?: boolean;
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,4 +145,65 @@ describe('useFreshReplies', () => {
|
|||||||
expect(latestValue[0]).toBe(testState.accountComments[0] as never);
|
expect(latestValue[0]).toBe(testState.accountComments[0] as never);
|
||||||
expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [0] });
|
expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [0] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('orders replies by final post number after a pending reply is approved', () => {
|
||||||
|
testState.replies = [
|
||||||
|
{
|
||||||
|
cid: 'reply-8',
|
||||||
|
index: 8,
|
||||||
|
number: 8,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cid: 'reply-12',
|
||||||
|
index: 12,
|
||||||
|
number: 12,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cid: 'pending-reply',
|
||||||
|
index: 11,
|
||||||
|
number: undefined,
|
||||||
|
pendingApproval: true,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testState.accountComments = [
|
||||||
|
{
|
||||||
|
cid: 'reply-11',
|
||||||
|
index: 11,
|
||||||
|
number: 11,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
renderHook();
|
||||||
|
|
||||||
|
expect(latestValue.map((reply) => reply.cid)).toEqual(['reply-8', 'reply-11', 'reply-12']);
|
||||||
|
expect(testState.accountCommentsCalls).toContainEqual({ commentIndices: [8, 12, 11] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('orders already-numbered replies when an approved queued reply was appended', () => {
|
||||||
|
testState.replies = [
|
||||||
|
{
|
||||||
|
cid: 'reply-8',
|
||||||
|
number: 8,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cid: 'reply-12',
|
||||||
|
number: 12,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cid: 'reply-11',
|
||||||
|
number: 11,
|
||||||
|
communityAddress: 'music.eth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
renderHook();
|
||||||
|
|
||||||
|
expect(latestValue.map((reply) => reply.cid)).toEqual(['reply-8', 'reply-11', 'reply-12']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Comment, useAccountComments } from '@bitsocial/bitsocial-react-hooks';
|
import { Comment, useAccountComments } from '@bitsocial/bitsocial-react-hooks';
|
||||||
|
import { sortRepliesForDisplay } from '../lib/utils/replies-preview-utils';
|
||||||
|
|
||||||
// Keep the hook on its indexed fast path when there are no reply indices to resolve.
|
// Keep the hook on its indexed fast path when there are no reply indices to resolve.
|
||||||
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
||||||
@@ -13,10 +14,14 @@ const useFreshReplies = (replies: Comment[] = []) => {
|
|||||||
const { accountComments } = useAccountComments(accountCommentLookupOptions);
|
const { accountComments } = useAccountComments(accountCommentLookupOptions);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!replies.length || !accountComments?.length) {
|
if (!replies.length) {
|
||||||
return replies;
|
return replies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!accountComments?.length) {
|
||||||
|
return sortRepliesForDisplay(replies);
|
||||||
|
}
|
||||||
|
|
||||||
const accountCommentsByIndex = new Map<number, Comment>();
|
const accountCommentsByIndex = new Map<number, Comment>();
|
||||||
for (const accountComment of accountComments) {
|
for (const accountComment of accountComments) {
|
||||||
if (typeof accountComment?.index === 'number') {
|
if (typeof accountComment?.index === 'number') {
|
||||||
@@ -40,7 +45,7 @@ const useFreshReplies = (replies: Comment[] = []) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!hasFreshReplies) {
|
if (!hasFreshReplies) {
|
||||||
return replies;
|
return sortRepliesForDisplay(replies);
|
||||||
}
|
}
|
||||||
|
|
||||||
const seenReplyIndices = new Set<number>();
|
const seenReplyIndices = new Set<number>();
|
||||||
@@ -59,7 +64,7 @@ const useFreshReplies = (replies: Comment[] = []) => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return hasDuplicateReplyIndices ? dedupedReplies : nextReplies;
|
return sortRepliesForDisplay(hasDuplicateReplyIndices ? dedupedReplies : nextReplies);
|
||||||
}, [accountComments, replies]);
|
}, [accountComments, replies]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { copyToClipboard } from '../clipboard-utils';
|
import { copyToClipboard } from '../clipboard-utils';
|
||||||
import { hashStringToColor, getTextColorForBackground, removeMarkdown } from '../post-utils';
|
import { hashStringToColor, getTextColorForBackground, removeMarkdown } from '../post-utils';
|
||||||
import { preloadReplyModal, preloadThemeAssets, resolveAssetUrl } from '../preload-utils';
|
import { preloadReplyModal, preloadThemeAssets, resolveAssetUrl } from '../preload-utils';
|
||||||
import { computeOmittedCount, filterRepliesForDisplay, getPreviewDisplayReplies, getTotalReplyCount, hasEnoughPreviewReplies } from '../replies-preview-utils';
|
import {
|
||||||
|
computeOmittedCount,
|
||||||
|
filterRepliesForDisplay,
|
||||||
|
getPreviewDisplayReplies,
|
||||||
|
getTotalReplyCount,
|
||||||
|
hasEnoughPreviewReplies,
|
||||||
|
sortRepliesForDisplay,
|
||||||
|
} from '../replies-preview-utils';
|
||||||
import { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils';
|
import { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils';
|
||||||
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils';
|
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils';
|
||||||
import { getFormattedDate, getFormattedTimeAgo, isChristmas } from '../time-utils';
|
import { getFormattedDate, getFormattedTimeAgo, isChristmas } from '../time-utils';
|
||||||
@@ -184,6 +191,33 @@ describe('misc utils', () => {
|
|||||||
{ cid: 'pending', pendingApproval: true },
|
{ cid: 'pending', pendingApproval: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortRepliesForDisplay([
|
||||||
|
{ cid: 'reply-8', number: 8 },
|
||||||
|
{ cid: 'reply-12', number: 12 },
|
||||||
|
{ cid: 'reply-11', number: 11 },
|
||||||
|
]),
|
||||||
|
).toEqual([
|
||||||
|
{ cid: 'reply-8', number: 8 },
|
||||||
|
{ cid: 'reply-11', number: 11 },
|
||||||
|
{ cid: 'reply-12', number: 12 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getPreviewDisplayReplies(
|
||||||
|
[
|
||||||
|
{ cid: 'reply-8', number: 8, timestamp: 8 },
|
||||||
|
{ cid: 'reply-12', number: 12, timestamp: 12 },
|
||||||
|
{ cid: 'reply-11', number: 11, timestamp: 99 },
|
||||||
|
],
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{ cid: 'reply-8', number: 8, timestamp: 8 },
|
||||||
|
{ cid: 'reply-11', number: 11, timestamp: 99 },
|
||||||
|
{ cid: 'reply-12', number: 12, timestamp: 12 },
|
||||||
|
]);
|
||||||
|
|
||||||
expect(computeOmittedCount({ totalReplyCount: 2, visibleCount: 5 })).toBe(0);
|
expect(computeOmittedCount({ totalReplyCount: 2, visibleCount: 5 })).toBe(0);
|
||||||
expect(computeOmittedCount({ totalReplyCount: 9, visibleCount: 5 })).toBe(4);
|
expect(computeOmittedCount({ totalReplyCount: 9, visibleCount: 5 })).toBe(4);
|
||||||
expect(hasEnoughPreviewReplies({ replyCount: 2, loadedCount: 2, visibleCount: 5 })).toBe(true);
|
expect(hasEnoughPreviewReplies({ replyCount: 2, loadedCount: 2, visibleCount: 5 })).toBe(true);
|
||||||
|
|||||||
@@ -4,11 +4,45 @@ interface CommentLike {
|
|||||||
cid?: string | null;
|
cid?: string | null;
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
index?: number;
|
index?: number;
|
||||||
|
number?: number;
|
||||||
pendingApproval?: boolean;
|
pendingApproval?: boolean;
|
||||||
state?: string;
|
state?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getReplyNumber = (reply: CommentLike): number | undefined => (typeof reply?.number === 'number' && Number.isFinite(reply.number) ? reply.number : undefined);
|
||||||
|
|
||||||
|
export function sortRepliesForDisplay<T extends CommentLike>(replies: T[]): T[] {
|
||||||
|
if (replies.length < 2) {
|
||||||
|
return replies;
|
||||||
|
}
|
||||||
|
|
||||||
|
const taggedReplies = replies.map((reply, index) => ({
|
||||||
|
index,
|
||||||
|
number: getReplyNumber(reply),
|
||||||
|
reply,
|
||||||
|
}));
|
||||||
|
|
||||||
|
taggedReplies.sort((a, b) => {
|
||||||
|
if (a.number !== undefined && b.number !== undefined) {
|
||||||
|
return a.number === b.number ? a.index - b.index : a.number - b.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.number !== undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.number !== undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.index - b.index;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedReplies = taggedReplies.map((taggedReply) => taggedReply.reply);
|
||||||
|
return sortedReplies.every((reply, index) => reply === replies[index]) ? replies : sortedReplies;
|
||||||
|
}
|
||||||
|
|
||||||
export function filterRepliesForDisplay<T extends CommentLike>(replies: T[]): T[] {
|
export function filterRepliesForDisplay<T extends CommentLike>(replies: T[]): T[] {
|
||||||
return replies.filter((reply) => !reply.deleted);
|
return replies.filter((reply) => !reply.deleted);
|
||||||
}
|
}
|
||||||
@@ -21,23 +55,42 @@ export function filterRepliesForDisplay<T extends CommentLike>(replies: T[]): T[
|
|||||||
* board previews.
|
* board previews.
|
||||||
*/
|
*/
|
||||||
export function getPreviewDisplayReplies<T extends CommentLike>(replies: T[], visibleCount: number = BOARD_REPLIES_PREVIEW_VISIBLE_COUNT): T[] {
|
export function getPreviewDisplayReplies<T extends CommentLike>(replies: T[], visibleCount: number = BOARD_REPLIES_PREVIEW_VISIBLE_COUNT): T[] {
|
||||||
const getRecency = (reply: T): number => {
|
const getRecency = (reply: T): { group: number; value: number } => {
|
||||||
if (typeof reply?.timestamp === 'number') {
|
const number = getReplyNumber(reply);
|
||||||
return reply.timestamp;
|
if (number !== undefined) {
|
||||||
|
return { group: 2, value: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pending/local account replies can be missing timestamp early on.
|
// Pending/local account replies can be missing timestamp early on.
|
||||||
if (typeof reply?.index === 'number' || reply?.pendingApproval || (reply?.state && reply.state !== 'succeeded')) {
|
if (typeof reply?.index === 'number' || reply?.pendingApproval || (reply?.state && reply.state !== 'succeeded')) {
|
||||||
return Number.POSITIVE_INFINITY;
|
return { group: 3, value: 0 };
|
||||||
}
|
}
|
||||||
return Number.NEGATIVE_INFINITY;
|
|
||||||
|
if (typeof reply?.timestamp === 'number') {
|
||||||
|
return { group: 1, value: reply.timestamp };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { group: 0, value: 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagged = replies.map((reply, i) => ({ reply, recency: getRecency(reply), i }));
|
const tagged = replies.map((reply, i) => ({ reply, recency: getRecency(reply), i }));
|
||||||
tagged.sort((a, b) => (a.recency !== b.recency ? b.recency - a.recency : a.i - b.i));
|
tagged.sort((a, b) => {
|
||||||
return tagged
|
if (a.recency.group !== b.recency.group) {
|
||||||
.slice(0, visibleCount)
|
return b.recency.group - a.recency.group;
|
||||||
.reverse()
|
}
|
||||||
.map((t) => t.reply);
|
|
||||||
|
if (a.recency.value !== b.recency.value) {
|
||||||
|
return b.recency.value - a.recency.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.i - b.i;
|
||||||
|
});
|
||||||
|
return sortRepliesForDisplay(
|
||||||
|
tagged
|
||||||
|
.slice(0, visibleCount)
|
||||||
|
.reverse()
|
||||||
|
.map((t) => t.reply),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComputeOmittedParams {
|
interface ComputeOmittedParams {
|
||||||
|
|||||||
Reference in New Issue
Block a user