fix(api): respect age gating in search and stabilise discovery (#2598)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-08 19:07:59 +02:00
1 parent 20a15ac11d
commit 55846b24ea
10 files changed
+250 -49

No files matched your search

@@ -99,9 +99,10 @@ export const DiscoveryPage = observer(function DiscoveryPage() {
return Math.min(SKELETON_DISCOVERY_MAX_COLUMNS, Math.max(1, columnsThatFit));
}, [containerWidth, zoomLevel]);
const guilds = Discovery.guilds;
const loadedCount = Discovery.loadedCount;
const searchActive = Discovery.query.length > 0;
const rowCount = columns > 0 ? Math.ceil(guilds.length / columns) : 0;
const hasMore = guilds.length < Discovery.total;
const hasMore = loadedCount < Discovery.total;
const virtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => scrollerRef.current?.getViewportElement() ?? null,
@@ -114,10 +115,10 @@ export const DiscoveryPage = observer(function DiscoveryPage() {
return;
}
void Discovery.search({
offset: guilds.length,
offset: loadedCount,
limit: PAGE_SIZE,
});
}, [guilds.length, hasMore]);
}, [loadedCount, hasMore]);
useEffect(() => {
const items = virtualizer.getVirtualItems();
const lastItem = items[items.length - 1];
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {DiscoveryGuild, DiscoverySearchResponse} from '@app/features/discovery/commands/DiscoveryCommands';
import * as DiscoveryCommands from '@app/features/discovery/commands/DiscoveryCommands';
import {beforeEach, describe, expect, test, vi} from 'vitest';
import Discovery from './Discovery';
vi.mock('@app/features/discovery/commands/DiscoveryCommands', () => ({
searchGuilds: vi.fn(),
getCategories: vi.fn(),
}));
function guild(id: string, memberCount: number): DiscoveryGuild {
return {
id,
name: `Guild ${id}`,
icon: null,
banner: null,
description: null,
category_type: 0,
primary_language: null,
custom_tags: [],
member_count: memberCount,
online_count: 0,
features: [],
verification_level: 0,
};
}
function page(guilds: Array<DiscoveryGuild>, total: number): DiscoverySearchResponse {
return {guilds, total, categoryCounts: null};
}
describe('Discovery.search', () => {
beforeEach(() => {
vi.mocked(DiscoveryCommands.searchGuilds).mockReset();
Discovery.reset();
});
test('does not append a guild that a previous page already returned', async () => {
vi.mocked(DiscoveryCommands.searchGuilds)
.mockResolvedValueOnce(page([guild('1', 50), guild('2', 40)], 4))
.mockResolvedValueOnce(page([guild('2', 40), guild('3', 30)], 4));
await Discovery.search({limit: 2, offset: 0});
await Discovery.search({limit: 2, offset: Discovery.loadedCount});
expect(Discovery.guilds.map((entry) => entry.id)).toEqual(['1', '2', '3']);
});
test('advances the pagination offset by what the server returned, not by what survived deduping', async () => {
vi.mocked(DiscoveryCommands.searchGuilds)
.mockResolvedValueOnce(page([guild('1', 50), guild('2', 40)], 4))
.mockResolvedValueOnce(page([guild('2', 40), guild('3', 30)], 4));
await Discovery.search({limit: 2, offset: 0});
await Discovery.search({limit: 2, offset: Discovery.loadedCount});
expect(Discovery.loadedCount).toBe(4);
});
});
@@ -8,6 +8,7 @@ const DEFAULT_DISCOVERY_PAGE_SIZE = 36;
class Discovery {
guilds: Array<DiscoveryGuild> = [];
loadedCount = 0;
total = 0;
loading = false;
error = false;
@@ -53,6 +54,7 @@ class Discovery {
this.error = false;
if (offset === 0 && searchModeChanged) {
this.guilds = [];
this.loadedCount = 0;
this.total = 0;
}
this.query = query;
@@ -78,8 +80,10 @@ class Discovery {
if (offset === 0) {
this.guilds = result.guilds;
} else {
this.guilds = [...this.guilds, ...result.guilds];
const seen = new Set(this.guilds.map((guild) => guild.id));
this.guilds = [...this.guilds, ...result.guilds.filter((guild) => !seen.has(guild.id))];
}
this.loadedCount = offset + result.guilds.length;
this.total = result.total;
this.categoryCounts = result.categoryCounts;
this.loading = false;
@@ -136,6 +140,7 @@ class Discovery {
reset(): void {
this.activeSearchToken += 1;
this.guilds = [];
this.loadedCount = 0;
this.total = 0;
this.categoryCounts = null;
this.loading = false;