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 React, { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { ServerSidebar } from './ServerSidebar';
|
import { ServerSidebar } from './ServerSidebar';
|
||||||
import { ChannelSidebar } from './ChannelSidebar';
|
import { ChannelSidebar } from './ChannelSidebar';
|
||||||
import { MainContent } from './MainContent';
|
import { MainContent } from './MainContent';
|
||||||
@@ -33,6 +33,7 @@ import { AudioManager } from '../../audio/AudioManager';
|
|||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
const { serverId, channelId, inviteCode } = useParams<{ serverId?: string; channelId?: string; inviteCode?: string }>();
|
const { serverId, channelId, inviteCode } = useParams<{ serverId?: string; channelId?: string; inviteCode?: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Global interaction handler to resume AudioContext
|
// Global interaction handler to resume AudioContext
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -95,6 +96,8 @@ export function AppLayout() {
|
|||||||
const userProfilePopout = useUIStore((s) => s.userProfilePopout);
|
const userProfilePopout = useUIStore((s) => s.userProfilePopout);
|
||||||
const closeUserProfile = useUIStore((s) => s.closeUserProfile);
|
const closeUserProfile = useUIStore((s) => s.closeUserProfile);
|
||||||
|
|
||||||
|
const channels = useServerStore((s) => s.channels);
|
||||||
|
|
||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
|
||||||
const {
|
const {
|
||||||
@@ -205,10 +208,35 @@ export function AppLayout() {
|
|||||||
if (channelId) {
|
if (channelId) {
|
||||||
setCurrentChannel(channelId);
|
setCurrentChannel(channelId);
|
||||||
loadMessages(channelId);
|
loadMessages(channelId);
|
||||||
|
if (serverId && serverId !== '@me') {
|
||||||
|
useUIStore.getState().setLastChannel(serverId, channelId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setCurrentChannel(null);
|
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) {
|
if (isLoading || !user) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface UIState {
|
|||||||
closeUserProfile: () => void;
|
closeUserProfile: () => void;
|
||||||
addToast: (message: string, type?: 'info' | 'warning' | 'success', duration?: number) => void;
|
addToast: (message: string, type?: 'info' | 'warning' | 'success', duration?: number) => void;
|
||||||
removeToast: (id: string) => void;
|
removeToast: (id: string) => void;
|
||||||
|
lastChannelPerServer: Record<string, string>;
|
||||||
|
setLastChannel: (serverId: string, channelId: string) => void;
|
||||||
voiceChatOpen: boolean;
|
voiceChatOpen: boolean;
|
||||||
voiceFullscreen: boolean;
|
voiceFullscreen: boolean;
|
||||||
pipCollapsed: boolean;
|
pipCollapsed: boolean;
|
||||||
@@ -110,6 +112,11 @@ export const useUIStore = create<UIState>()(
|
|||||||
},
|
},
|
||||||
removeToast: (id) => set((state) => ({ toasts: state.toasts.filter(t => t.id !== id) })),
|
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,
|
voiceChatOpen: false,
|
||||||
voiceFullscreen: false,
|
voiceFullscreen: false,
|
||||||
pipCollapsed: false,
|
pipCollapsed: false,
|
||||||
@@ -123,6 +130,7 @@ export const useUIStore = create<UIState>()(
|
|||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
memberListOpen: state.memberListOpen,
|
memberListOpen: state.memberListOpen,
|
||||||
|
lastChannelPerServer: state.lastChannelPerServer,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user