fix(app): pick default channel when guild channels arrive late (#2475)

This commit is contained in:
Hampus
2026-09-06 00:04:43 +02:00
committed by GitHub
parent 622bd124b9
commit 7cb8f9f4ae
3 changed files with 67 additions and 56 deletions
@@ -3,7 +3,7 @@
import {Routes} from '@app/app/Routes';
import {GuildLayout} from '@app/features/app/components/layout/GuildLayout';
import Channels from '@app/features/channel/state/Channels';
import {compareChannelPosition, filterViewableChannels} from '@app/features/messaging/utils/ChannelShared';
import {pickDefaultGuildChannelId} from '@app/features/messaging/utils/ChannelShared';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
import {useLocation} from '@app/features/platform/components/router/RouterReact';
@@ -13,47 +13,33 @@ import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useEffect} from 'react';
function isGuildRootPath(pathname: string, guildId: string): boolean {
if (guildId === ME || pathname === Routes.ME || pathname.startsWith(Routes.ME)) {
return false;
}
if (!pathname.startsWith('/channels/')) {
return false;
}
const segments = pathname.split('/');
return segments.length === 3 && segments[2] === guildId;
}
export const GuildChannelRouter = observer<{guildId: string; children: React.ReactNode}>(({guildId, children}) => {
const location = useLocation();
const needsDefaultChannel = !MobileLayout.enabled && isGuildRootPath(location.pathname, guildId);
const defaultChannelId = needsDefaultChannel
? pickDefaultGuildChannelId({
guildId,
channels: Channels.getGuildChannels(guildId),
selectedChannelId: SelectedChannel.selectedChannelIds.get(guildId),
})
: null;
useEffect(() => {
if (guildId === ME || location.pathname === Routes.ME) {
if (!defaultChannelId) {
return;
}
if (MobileLayout.enabled) {
return;
}
if (location.pathname.startsWith('/channels/') && !location.pathname.startsWith(Routes.ME)) {
if (location.pathname.split('/').length === 3) {
const pathSegments = location.pathname.split('/');
const currentGuildId = pathSegments[2];
if (currentGuildId !== guildId) {
return;
}
const selectedChannelId = SelectedChannel.selectedChannelIds.get(guildId);
if (selectedChannelId) {
const channel = Channels.getChannel(selectedChannelId);
const isViewableChannel = channel ? filterViewableChannels([channel]).length > 0 : false;
if (channel && channel.guildId === guildId && isViewableChannel) {
NavigationCommands.selectChannel(guildId, selectedChannelId, undefined, 'replace');
} else {
const channels = Channels.getGuildChannels(guildId);
const viewableChannels = filterViewableChannels(channels).sort(compareChannelPosition);
if (viewableChannels.length > 0) {
const firstChannel = viewableChannels[0];
NavigationCommands.selectChannel(guildId, firstChannel.id, undefined, 'replace');
}
}
} else {
const channels = Channels.getGuildChannels(guildId);
const viewableChannels = filterViewableChannels(channels).sort(compareChannelPosition);
if (viewableChannels.length > 0) {
const firstChannel = viewableChannels[0];
NavigationCommands.selectChannel(guildId, firstChannel.id, undefined, 'replace');
}
}
}
}
}, [guildId, location.pathname, MobileLayout.enabled]);
NavigationCommands.selectChannel(guildId, defaultChannelId, undefined, 'replace');
}, [guildId, defaultChannelId]);
if (guildId === ME || location.pathname === Routes.ME) {
return null;
}
@@ -2,7 +2,7 @@
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {describe, expect, it} from 'vitest';
import {filterViewableChannels} from './ChannelShared';
import {filterViewableChannels, pickDefaultGuildChannelId} from './ChannelShared';
describe('filterViewableChannels', () => {
it('excludes link channels from default navigation fallbacks', () => {
@@ -15,3 +15,41 @@ describe('filterViewableChannels', () => {
expect(filterViewableChannels(channels).map((channel) => channel.id)).toEqual(['text', 'voice']);
});
});
describe('pickDefaultGuildChannelId', () => {
const channels = [
{id: 'category', type: ChannelTypes.GUILD_CATEGORY, position: 1, guildId: 'guild'},
{id: 'link', type: ChannelTypes.GUILD_LINK, position: 2, guildId: 'guild'},
{id: 'rules', type: ChannelTypes.GUILD_TEXT, position: 3, guildId: 'guild'},
{id: 'general', type: ChannelTypes.GUILD_TEXT, position: 4, guildId: 'guild'},
{id: 'voice', type: ChannelTypes.GUILD_VOICE, position: 5, guildId: 'guild'},
];
it('returns null while the guild has no channels yet', () => {
expect(pickDefaultGuildChannelId({guildId: 'guild', channels: []})).toBeNull();
});
it('picks the first channel by position, skipping categories and links', () => {
expect(pickDefaultGuildChannelId({guildId: 'guild', channels})).toBe('rules');
});
it('keeps the remembered channel for the guild', () => {
expect(pickDefaultGuildChannelId({guildId: 'guild', channels, selectedChannelId: 'general'})).toBe('general');
});
it('falls back when the remembered channel is gone', () => {
expect(pickDefaultGuildChannelId({guildId: 'guild', channels, selectedChannelId: 'deleted'})).toBe('rules');
});
it('falls back when the remembered channel is not navigable', () => {
expect(pickDefaultGuildChannelId({guildId: 'guild', channels, selectedChannelId: 'link'})).toBe('rules');
expect(pickDefaultGuildChannelId({guildId: 'guild', channels, selectedChannelId: 'category'})).toBe('rules');
});
it('falls back when the remembered channel belongs to another guild', () => {
const withForeign = [...channels, {id: 'other', type: ChannelTypes.GUILD_TEXT, position: 0, guildId: 'other'}];
expect(pickDefaultGuildChannelId({guildId: 'guild', channels: withForeign, selectedChannelId: 'other'})).toBe(
'rules',
);
});
});
@@ -20,28 +20,15 @@ export function pickDefaultGuildChannelId({
guildId,
channels,
selectedChannelId,
systemChannelId,
rulesChannelId,
}: {
guildId: string;
channels: ReadonlyArray<MinimalChannel>;
selectedChannelId?: string | null;
systemChannelId?: string | null;
rulesChannelId?: string | null;
}): string | null {
if (!channels.length) return null;
const channelById = new Map(channels.map((channel) => [channel.id, channel]));
const isChannelInGuild = (channelId?: string | null) =>
channelId ? channelById.get(channelId)?.guildId === guildId : false;
if (isChannelInGuild(selectedChannelId)) {
return selectedChannelId!;
const viewable = filterViewableChannels(channels.filter((channel) => channel.guildId === guildId));
if (!viewable.length) return null;
if (selectedChannelId && viewable.some((channel) => channel.id === selectedChannelId)) {
return selectedChannelId;
}
if (isChannelInGuild(systemChannelId)) {
return systemChannelId!;
}
if (isChannelInGuild(rulesChannelId)) {
return rulesChannelId!;
}
const viewable = [...filterViewableChannels(channels)].sort(compareChannelPosition);
return viewable[0]?.id ?? null;
return viewable.sort(compareChannelPosition)[0].id;
}