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.
This commit is contained in:
Jannis Braun
2026-03-04 21:24:43 +01:00
parent 5e4a5c2ee9
commit a5ea7633fc
5 changed files with 54 additions and 20 deletions
+24 -14
View File
@@ -195,6 +195,11 @@ export async function exploreRoutes(app: FastifyInstance): Promise<void> {
// which is more efficient to do with raw SQL // which is more efficient to do with raw SQL
const rawDb = (db as any).$client as import('better-sqlite3').Database; 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 countSql = `SELECT COUNT(DISTINCT s.id) as total FROM servers s WHERE s.visibility IN ('public', 'request')`;
let querySql = ` let querySql = `
SELECT s.id, s.name, s.icon, s.description, s.visibility, s.created_at, 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<void> {
WHERE s.visibility IN ('public', 'request') 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) { if (q) {
const likePattern = `%${q}%`; const likePattern = `%${q}%`;
@@ -226,20 +239,17 @@ export async function exploreRoutes(app: FastifyInstance): Promise<void> {
member_count: number; member_count: number;
}[]; }[];
// Filter out servers the user is already a member of const servers: ExploreServer[] = rows.map(r => ({
const servers: ExploreServer[] = rows id: r.id,
.filter(r => !myServerIds.has(r.id)) name: r.name,
.map(r => ({ icon: r.icon,
id: r.id, description: r.description,
name: r.name, visibility: r.visibility as ExploreServer['visibility'],
icon: r.icon, memberCount: r.member_count,
description: r.description, createdAt: r.created_at,
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 // POST /api/servers/:id/public-join — join a public server without invite
@@ -15,6 +15,7 @@ export function ExplorePage() {
const myRequests = useExploreStore((s) => s.myRequests); const myRequests = useExploreStore((s) => s.myRequests);
const isLoading = useExploreStore((s) => s.isLoading); const isLoading = useExploreStore((s) => s.isLoading);
const discoveryEnabled = useExploreStore((s) => s.discoveryEnabled); const discoveryEnabled = useExploreStore((s) => s.discoveryEnabled);
const totalAll = useExploreStore((s) => s.totalAll);
const error = useExploreStore((s) => s.error); const error = useExploreStore((s) => s.error);
const searchQuery = useExploreStore((s) => s.searchQuery); const searchQuery = useExploreStore((s) => s.searchQuery);
const setSearchQuery = useExploreStore((s) => s.setSearchQuery); const setSearchQuery = useExploreStore((s) => s.setSearchQuery);
@@ -106,7 +107,11 @@ export function ExplorePage() {
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.49L17.5 6.5 9.99 9.99 6.5 17.5zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1-1.1-.49-1.1-1.1.49-1.1 1.1-1.1z" /> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5l7.51-3.49L17.5 6.5 9.99 9.99 6.5 17.5zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1-1.1-.49-1.1-1.1.49-1.1 1.1-1.1z" />
</svg> </svg>
<p className="text-txt-tertiary text-sm"> <p className="text-txt-tertiary text-sm">
{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.'}
</p> </p>
</div> </div>
) : ( ) : (
@@ -183,7 +183,9 @@ export function AppLayout() {
// Handle route params // Handle route params
useEffect(() => { useEffect(() => {
if (serverId === '@me') { if (serverId === '@me') {
setShowDms(true); if (!useUIStore.getState().showExplore) {
setShowDms(true);
}
setCurrentServer(null); setCurrentServer(null);
} else if (serverId) { } else if (serverId) {
setShowDms(false); setShowDms(false);
@@ -124,6 +124,7 @@ export function ServerSidebar() {
const setShowExplore = useUIStore((s) => s.setShowExplore); const setShowExplore = useUIStore((s) => s.setShowExplore);
const openModal = useUIStore((s) => s.openModal); const openModal = useUIStore((s) => s.openModal);
const addToast = useUIStore((s) => s.addToast); const addToast = useUIStore((s) => s.addToast);
const setCurrentChannel = useChatStore((s) => s.setCurrentChannel);
const unreadChannels = useChatStore((s) => s.unreadChannels); const unreadChannels = useChatStore((s) => s.unreadChannels);
const instances = useInstanceStore((s) => s.instances); const instances = useInstanceStore((s) => s.instances);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -188,12 +189,14 @@ export function ServerSidebar() {
const handleDmClick = () => { const handleDmClick = () => {
setShowDms(true); setShowDms(true);
setCurrentServer(null); setCurrentServer(null);
setCurrentChannel(null);
navigate('/channels/@me'); navigate('/channels/@me');
}; };
const handleExploreClick = () => { const handleExploreClick = () => {
setShowExplore(true); setShowExplore(true);
setCurrentServer(null); setCurrentServer(null);
setCurrentChannel(null);
navigate('/channels/@me'); navigate('/channels/@me');
}; };
+18 -4
View File
@@ -16,6 +16,7 @@ interface ExploreState {
searchQuery: string; searchQuery: string;
isLoading: boolean; isLoading: boolean;
discoveryEnabled: boolean; discoveryEnabled: boolean;
totalAll: number;
error: string | null; error: string | null;
fetchServers: (query?: string) => Promise<void>; fetchServers: (query?: string) => Promise<void>;
@@ -42,6 +43,7 @@ export const useExploreStore = create<ExploreState>((set, get) => ({
searchQuery: '', searchQuery: '',
isLoading: false, isLoading: false,
discoveryEnabled: true, discoveryEnabled: true,
totalAll: 0,
error: null, error: null,
fetchServers: async (query?: string) => { fetchServers: async (query?: string) => {
@@ -59,20 +61,30 @@ export const useExploreStore = create<ExploreState>((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 allServers: TaggedExploreServer[] = [];
const seen = new Set<string>(); // dedup by serverId+origin const seen = new Set<string>(); // dedup by serverId+origin
let homeDiscoveryEnabled = true; let homeDiscoveryEnabled = true;
let totalAllSum = 0;
for (const result of results) { for (const result of fulfilled) {
if (result.status !== 'fulfilled') continue; const { servers, discoveryEnabled, totalAll, origin } = result.value;
const { servers, discoveryEnabled, origin } = result.value;
// Track home instance discovery state // Track home instance discovery state
if (!origin) { if (!origin) {
homeDiscoveryEnabled = discoveryEnabled; homeDiscoveryEnabled = discoveryEnabled;
} }
totalAllSum += totalAll ?? 0;
for (const server of servers) { for (const server of servers) {
const key = `${server.id}:${origin}`; const key = `${server.id}:${origin}`;
if (seen.has(key)) continue; if (seen.has(key)) continue;
@@ -84,6 +96,7 @@ export const useExploreStore = create<ExploreState>((set, get) => ({
set({ set({
servers: allServers, servers: allServers,
discoveryEnabled: homeDiscoveryEnabled, discoveryEnabled: homeDiscoveryEnabled,
totalAll: totalAllSum,
isLoading: false, isLoading: false,
}); });
} catch (err) { } catch (err) {
@@ -139,6 +152,7 @@ export const useExploreStore = create<ExploreState>((set, get) => ({
searchQuery: '', searchQuery: '',
isLoading: false, isLoading: false,
discoveryEnabled: true, discoveryEnabled: true,
totalAll: 0,
error: null, error: null,
}), }),
})); }));