Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions packages/database/convex/public/discord_accounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { Database } from "../../src/database";
import { DatabaseTestLayer } from "../../src/database-test";
import {
createAuthor,
createChannel,
createForumThreadWithReplies,
createMessage,
createServer,
enableChannelIndexing,
makeMessagesPublic,
} from "../../src/test";

Expand Down Expand Up @@ -187,5 +190,123 @@ describe("public/discord_accounts", () => {
expect(result.page).toEqual([]);
}).pipe(Effect.provide(DatabaseTestLayer)),
);

it.scoped("should not link anonymized posts from a real user profile", () =>
Effect.gen(function* () {
const database = yield* Database;
const publicFixture = yield* createForumThreadWithReplies();
yield* publicFixture.addRootMessage();

const anonymousServer = yield* createServer();
const anonymousForum = yield* createChannel(anonymousServer.discordId, {
type: 15,
});
const anonymousThread = yield* createChannel(
anonymousServer.discordId,
{ type: 11, parentId: anonymousForum.id },
);
yield* enableChannelIndexing(anonymousForum.id);
yield* database.private.server_preferences.upsertServerPreferences({
serverId: anonymousServer.discordId,
plan: "FREE",
considerAllMessagesPublicEnabled: true,
anonymizeMessagesEnabled: true,
});
yield* createMessage(
{
authorId: publicFixture.author.id,
serverId: anonymousServer.discordId,
channelId: anonymousThread.id,
},
{
id: anonymousThread.id,
parentChannelId: anonymousForum.id,
},
);

const result = yield* database.public.discord_accounts.getUserPosts({
userId: publicFixture.author.id,
paginationOpts: { numItems: 10, cursor: null },
});

expect(result.page).toHaveLength(1);
expect(result.page[0]?.server.discordId).toBe(
publicFixture.server.discordId,
);
}).pipe(Effect.provide(DatabaseTestLayer)),
);

it.scoped("should keep a cursor when an anonymized page is empty", () =>
Effect.gen(function* () {
const database = yield* Database;
const identifiedFixture = yield* createForumThreadWithReplies();
yield* createMessage(
{
authorId: identifiedFixture.author.id,
serverId: identifiedFixture.server.discordId,
channelId: identifiedFixture.thread.id,
},
{
id: identifiedFixture.thread.id,
parentChannelId: identifiedFixture.forum.id,
childThreadId: 1n,
},
);

const anonymousServer = yield* createServer();
const anonymousForum = yield* createChannel(anonymousServer.discordId, {
type: 15,
});
yield* enableChannelIndexing(anonymousForum.id);
yield* database.private.server_preferences.upsertServerPreferences({
serverId: anonymousServer.discordId,
plan: "FREE",
considerAllMessagesPublicEnabled: true,
anonymizeMessagesEnabled: true,
});

for (let count = 0; count < 2; count += 1) {
const thread = yield* createChannel(anonymousServer.discordId, {
type: 11,
parentId: anonymousForum.id,
});
yield* createMessage(
{
authorId: identifiedFixture.author.id,
serverId: anonymousServer.discordId,
channelId: thread.id,
},
{
id: thread.id,
parentChannelId: anonymousForum.id,
childThreadId: 3n - BigInt(count),
},
);
}

const firstPage = yield* database.public.discord_accounts.getUserPosts({
userId: identifiedFixture.author.id,
paginationOpts: { numItems: 2, cursor: null },
});

expect(firstPage.page).toEqual([]);
expect(firstPage.isDone).toBe(false);

const secondPage = yield* database.public.discord_accounts.getUserPosts(
{
userId: identifiedFixture.author.id,
paginationOpts: {
numItems: 2,
cursor: firstPage.continueCursor,
},
},
);

expect(secondPage.page).toHaveLength(1);
expect(secondPage.page[0]?.server.discordId).toBe(
identifiedFixture.server.discordId,
);
}).pipe(Effect.provide(DatabaseTestLayer)),
);
});
});
5 changes: 4 additions & 1 deletion packages/database/convex/public/discord_accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ export const getUserPosts = publicQuery({
ctx,
filteredMessages,
);
const identifiedPosts = enrichedPosts.filter(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: Filtering only after paginate can return an empty page with isDone: false when a user’s first 20 posts are anonymized. SnapshotInfiniteList renders loading skeletons for that state and never mounts Virtuoso, so it never follows continueCursor to reach later identified posts. Filter/skip anonymized rows while filling the requested page (or have the consumer advance empty, non-terminal pages) so profiles with a leading anonymized run do not get stuck indefinitely.

(post) => !post.message.author?.isAnonymous,
);

return {
page: enrichedPosts,
page: identifiedPosts,
isDone: paginatedResult.isDone,
continueCursor: paginatedResult.continueCursor,
};
Expand Down
22 changes: 22 additions & 0 deletions packages/ui/src/components/snapshot-infinite-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function SnapshotInfiniteList<Item>({
}: SnapshotInfiniteListProps<Item>) {
const generationRef = useRef(0);
const lastLoadedLength = useRef(0);
const lastAutoAdvancedCursor = useRef<string | null>(null);
const [pages, setPages] = useState<Array<SnapshotPage<Item>>>(() =>
initialData ? [initialData] : [],
);
Expand Down Expand Up @@ -129,6 +130,7 @@ export function SnapshotInfiniteList<Item>({
generationRef.current += 1;
const generation = generationRef.current;
lastLoadedLength.current = 0;
lastAutoAdvancedCursor.current = null;
setError(null);

if (initialData) {
Expand Down Expand Up @@ -159,6 +161,26 @@ export function SnapshotInfiniteList<Item>({

const canLoadMore = !isLoadingFirstPage && !isLoadingMore && !isDone;

useEffect(() => {
if (
!canLoadMore ||
continueCursor === null ||
results.length > 0 ||
lastAutoAdvancedCursor.current === continueCursor
) {
return;
}

lastAutoAdvancedCursor.current = continueCursor;
setIsLoadingMore(true);
void fetchPage({
cursor: continueCursor,
numItems: pageSize,
append: true,
generation: generationRef.current,
});
}, [canLoadMore, continueCursor, fetchPage, pageSize, results.length]);

const handleRangeChanged = useCallback(
(range: { startIndex: number; endIndex: number }) => {
if (!canLoadMore || continueCursor === null) {
Expand Down
Loading