fix: guard auto-channel redirect against stale cross-server channels
The redirect effect used the shared `channels` array which could contain stale data from a previously viewed server during async loadServerDetail. Added channelToServerMap ownership check to ensure channels belong to the target server before redirecting. Also persists last-visited channel per server in localStorage for better return navigation.
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -46,6 +46,8 @@ interface UIState {
|
||||
closeUserProfile: () => void;
|
||||
addToast: (message: string, type?: 'info' | 'warning' | 'success', duration?: number) => void;
|
||||
removeToast: (id: string) => void;
|
||||
lastChannelPerServer: Record<string, string>;
|
||||
setLastChannel: (serverId: string, channelId: string) => void;
|
||||
voiceChatOpen: boolean;
|
||||
voiceFullscreen: boolean;
|
||||
pipCollapsed: boolean;
|
||||
@@ -110,6 +112,11 @@ export const useUIStore = create<UIState>()(
|
||||
},
|
||||
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<UIState>()(
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
memberListOpen: state.memberListOpen,
|
||||
lastChannelPerServer: state.lastChannelPerServer,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user