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:
@@ -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,10 +239,7 @@ 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
|
|
||||||
.filter(r => !myServerIds.has(r.id))
|
|
||||||
.map(r => ({
|
|
||||||
id: r.id,
|
id: r.id,
|
||||||
name: r.name,
|
name: r.name,
|
||||||
icon: r.icon,
|
icon: r.icon,
|
||||||
@@ -239,7 +249,7 @@ export async function exploreRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
createdAt: r.created_at,
|
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') {
|
||||||
|
if (!useUIStore.getState().showExplore) {
|
||||||
setShowDms(true);
|
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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user