From a5ea7633fcf6f4628ab74eb34b4188b8f6840f1b Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:24:43 +0100 Subject: [PATCH] fix: explore tab first-click navigation and misleading empty state Clear stale currentChannelId when clicking Explore/DMs in ServerSidebar, and guard AppLayout route effect from clobbering showExplore. Move member exclusion into SQL for correct pagination/totals, surface allSettled errors, and show context-aware empty state messages. --- packages/server/src/routes/explore.ts | 38 ++++++++++++------- .../web/src/components/chat/ExplorePage.tsx | 7 +++- .../web/src/components/layout/AppLayout.tsx | 4 +- .../src/components/layout/ServerSidebar.tsx | 3 ++ packages/web/src/stores/exploreStore.ts | 22 +++++++++-- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/packages/server/src/routes/explore.ts b/packages/server/src/routes/explore.ts index 5db81084..d2f71e19 100644 --- a/packages/server/src/routes/explore.ts +++ b/packages/server/src/routes/explore.ts @@ -195,6 +195,11 @@ export async function exploreRoutes(app: FastifyInstance): Promise { // which is more efficient to do with raw SQL const rawDb = (db as any).$client as import('better-sqlite3').Database; + // Count ALL discoverable servers (including ones the user has joined) for context + const totalAllRow = rawDb.prepare( + `SELECT COUNT(DISTINCT s.id) as total FROM servers s WHERE s.visibility IN ('public', 'request')` + ).get() as { total: number }; + let countSql = `SELECT COUNT(DISTINCT s.id) as total FROM servers s WHERE s.visibility IN ('public', 'request')`; let querySql = ` SELECT s.id, s.name, s.icon, s.description, s.visibility, s.created_at, @@ -204,7 +209,15 @@ export async function exploreRoutes(app: FastifyInstance): Promise { WHERE s.visibility IN ('public', 'request') `; - const params: string[] = []; + const params: (string | number)[] = []; + + // Exclude servers the user is already in — do this in SQL for correct total/pagination + if (myServerIds.size > 0) { + const placeholders = [...myServerIds].map(() => '?').join(','); + querySql += ` AND s.id NOT IN (${placeholders})`; + countSql += ` AND s.id NOT IN (${placeholders})`; + params.push(...myServerIds); + } if (q) { const likePattern = `%${q}%`; @@ -226,20 +239,17 @@ export async function exploreRoutes(app: FastifyInstance): Promise { member_count: number; }[]; - // Filter out servers the user is already a member of - const servers: ExploreServer[] = rows - .filter(r => !myServerIds.has(r.id)) - .map(r => ({ - id: r.id, - name: r.name, - icon: r.icon, - description: r.description, - visibility: r.visibility as ExploreServer['visibility'], - memberCount: r.member_count, - createdAt: r.created_at, - })); + const servers: ExploreServer[] = rows.map(r => ({ + id: r.id, + name: r.name, + icon: r.icon, + description: r.description, + visibility: r.visibility as ExploreServer['visibility'], + memberCount: r.member_count, + createdAt: r.created_at, + })); - return reply.code(200).send({ servers, total: totalRow.total, discoveryEnabled: true }); + return reply.code(200).send({ servers, total: totalRow.total, totalAll: totalAllRow.total, discoveryEnabled: true }); }); // POST /api/servers/:id/public-join — join a public server without invite diff --git a/packages/web/src/components/chat/ExplorePage.tsx b/packages/web/src/components/chat/ExplorePage.tsx index 9916de43..7b3cca45 100644 --- a/packages/web/src/components/chat/ExplorePage.tsx +++ b/packages/web/src/components/chat/ExplorePage.tsx @@ -15,6 +15,7 @@ export function ExplorePage() { const myRequests = useExploreStore((s) => s.myRequests); const isLoading = useExploreStore((s) => s.isLoading); const discoveryEnabled = useExploreStore((s) => s.discoveryEnabled); + const totalAll = useExploreStore((s) => s.totalAll); const error = useExploreStore((s) => s.error); const searchQuery = useExploreStore((s) => s.searchQuery); const setSearchQuery = useExploreStore((s) => s.setSearchQuery); @@ -106,7 +107,11 @@ export function ExplorePage() {

- {searchQuery ? 'No servers match your search.' : 'No discoverable servers found.'} + {searchQuery + ? 'No servers match your search.' + : totalAll > 0 + ? `You've already joined all ${totalAll} discoverable server${totalAll === 1 ? '' : 's'}.` + : 'No servers have been made discoverable yet.'}

) : ( diff --git a/packages/web/src/components/layout/AppLayout.tsx b/packages/web/src/components/layout/AppLayout.tsx index e4ddf22e..d37afa42 100644 --- a/packages/web/src/components/layout/AppLayout.tsx +++ b/packages/web/src/components/layout/AppLayout.tsx @@ -183,7 +183,9 @@ export function AppLayout() { // Handle route params useEffect(() => { if (serverId === '@me') { - setShowDms(true); + if (!useUIStore.getState().showExplore) { + setShowDms(true); + } setCurrentServer(null); } else if (serverId) { setShowDms(false); diff --git a/packages/web/src/components/layout/ServerSidebar.tsx b/packages/web/src/components/layout/ServerSidebar.tsx index 808bc207..fa35e904 100644 --- a/packages/web/src/components/layout/ServerSidebar.tsx +++ b/packages/web/src/components/layout/ServerSidebar.tsx @@ -124,6 +124,7 @@ export function ServerSidebar() { const setShowExplore = useUIStore((s) => s.setShowExplore); const openModal = useUIStore((s) => s.openModal); const addToast = useUIStore((s) => s.addToast); + const setCurrentChannel = useChatStore((s) => s.setCurrentChannel); const unreadChannels = useChatStore((s) => s.unreadChannels); const instances = useInstanceStore((s) => s.instances); const navigate = useNavigate(); @@ -188,12 +189,14 @@ export function ServerSidebar() { const handleDmClick = () => { setShowDms(true); setCurrentServer(null); + setCurrentChannel(null); navigate('/channels/@me'); }; const handleExploreClick = () => { setShowExplore(true); setCurrentServer(null); + setCurrentChannel(null); navigate('/channels/@me'); }; diff --git a/packages/web/src/stores/exploreStore.ts b/packages/web/src/stores/exploreStore.ts index 8c1e932d..7f0eefa7 100644 --- a/packages/web/src/stores/exploreStore.ts +++ b/packages/web/src/stores/exploreStore.ts @@ -16,6 +16,7 @@ interface ExploreState { searchQuery: string; isLoading: boolean; discoveryEnabled: boolean; + totalAll: number; error: string | null; fetchServers: (query?: string) => Promise; @@ -42,6 +43,7 @@ export const useExploreStore = create((set, get) => ({ searchQuery: '', isLoading: false, discoveryEnabled: true, + totalAll: 0, error: null, fetchServers: async (query?: string) => { @@ -59,20 +61,30 @@ export const useExploreStore = create((set, get) => ({ ), ]); + const fulfilled = results.filter(r => r.status === 'fulfilled') as PromiseFulfilledResult<{ servers: ExploreServer[]; total: number; totalAll?: number; discoveryEnabled: boolean; origin: string }>[]; + const rejected = results.filter(r => r.status === 'rejected'); + + // If ALL instances failed, surface an error + if (fulfilled.length === 0 && rejected.length > 0) { + set({ isLoading: false, error: 'Failed to reach any server for discovery' }); + return; + } + const allServers: TaggedExploreServer[] = []; const seen = new Set(); // dedup by serverId+origin let homeDiscoveryEnabled = true; + let totalAllSum = 0; - for (const result of results) { - if (result.status !== 'fulfilled') continue; - - const { servers, discoveryEnabled, origin } = result.value; + for (const result of fulfilled) { + const { servers, discoveryEnabled, totalAll, origin } = result.value; // Track home instance discovery state if (!origin) { homeDiscoveryEnabled = discoveryEnabled; } + totalAllSum += totalAll ?? 0; + for (const server of servers) { const key = `${server.id}:${origin}`; if (seen.has(key)) continue; @@ -84,6 +96,7 @@ export const useExploreStore = create((set, get) => ({ set({ servers: allServers, discoveryEnabled: homeDiscoveryEnabled, + totalAll: totalAllSum, isLoading: false, }); } catch (err) { @@ -139,6 +152,7 @@ export const useExploreStore = create((set, get) => ({ searchQuery: '', isLoading: false, discoveryEnabled: true, + totalAll: 0, error: null, }), }));