diff --git a/packages/web/src/components/layout/AppLayout.tsx b/packages/web/src/components/layout/AppLayout.tsx index f2b85b9a..c11ad78b 100644 --- a/packages/web/src/components/layout/AppLayout.tsx +++ b/packages/web/src/components/layout/AppLayout.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { ServerSidebar } from './ServerSidebar'; import { ChannelSidebar } from './ChannelSidebar'; import { MainContent } from './MainContent'; @@ -33,6 +33,7 @@ import { AudioManager } from '../../audio/AudioManager'; export function AppLayout() { const { serverId, channelId, inviteCode } = useParams<{ serverId?: string; channelId?: string; inviteCode?: string }>(); + const navigate = useNavigate(); // Global interaction handler to resume AudioContext useEffect(() => { @@ -95,6 +96,8 @@ export function AppLayout() { const userProfilePopout = useUIStore((s) => s.userProfilePopout); const closeUserProfile = useUIStore((s) => s.closeUserProfile); + const channels = useServerStore((s) => s.channels); + const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); const activeDmCall = useVoiceStore((s) => s.activeDmCall); const { @@ -205,10 +208,35 @@ export function AppLayout() { if (channelId) { setCurrentChannel(channelId); loadMessages(channelId); + if (serverId && serverId !== '@me') { + useUIStore.getState().setLastChannel(serverId, channelId); + } } else { setCurrentChannel(null); } - }, [channelId, setCurrentChannel, loadMessages]); + }, [channelId, serverId, setCurrentChannel, loadMessages]); + + // Auto-select last visited (or first) channel when opening a server without a channelId + useEffect(() => { + if (!serverId || serverId === '@me' || channelId) return; + if (channels.length === 0) return; + + const firstChannel = channels[0]; + if (!firstChannel) return; + + // Guard: only redirect when channels belong to the target server. + // `channels` is shared per-view state set by loadServerDetail (async). + // Without this check, stale channels from a previously viewed server + // would cause a wrong redirect during the fetch window. + const { channelToServerMap } = useServerStore.getState(); + if (channelToServerMap.get(firstChannel.id) !== serverId) return; + + const lastId = useUIStore.getState().lastChannelPerServer[serverId]; + const target = (lastId && channels.find((c) => c.id === lastId)) || firstChannel; + if (target) { + navigate(`/channels/${serverId}/${target.id}`, { replace: true }); + } + }, [serverId, channelId, channels, navigate]); if (isLoading || !user) { return ( diff --git a/packages/web/src/stores/uiStore.ts b/packages/web/src/stores/uiStore.ts index 0f4b981a..b4bbc1d7 100644 --- a/packages/web/src/stores/uiStore.ts +++ b/packages/web/src/stores/uiStore.ts @@ -46,6 +46,8 @@ interface UIState { closeUserProfile: () => void; addToast: (message: string, type?: 'info' | 'warning' | 'success', duration?: number) => void; removeToast: (id: string) => void; + lastChannelPerServer: Record; + setLastChannel: (serverId: string, channelId: string) => void; voiceChatOpen: boolean; voiceFullscreen: boolean; pipCollapsed: boolean; @@ -110,6 +112,11 @@ export const useUIStore = create()( }, removeToast: (id) => set((state) => ({ toasts: state.toasts.filter(t => t.id !== id) })), + lastChannelPerServer: {}, + setLastChannel: (serverId, channelId) => set((state) => ({ + lastChannelPerServer: { ...state.lastChannelPerServer, [serverId]: channelId }, + })), + voiceChatOpen: false, voiceFullscreen: false, pipCollapsed: false, @@ -123,6 +130,7 @@ export const useUIStore = create()( storage: createJSONStorage(() => localStorage), partialize: (state) => ({ memberListOpen: state.memberListOpen, + lastChannelPerServer: state.lastChannelPerServer, }), } )