initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

878
fluxer_app/src/router.tsx Normal file
View File

@@ -0,0 +1,878 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {i18n} from '@lingui/core';
import {observer} from 'mobx-react-lite';
import React from 'react';
import * as AuthenticationActionCreators from '~/actions/AuthenticationActionCreators';
import * as GiftActionCreators from '~/actions/GiftActionCreators';
import * as InviteActionCreators from '~/actions/InviteActionCreators';
import * as ThemeActionCreators from '~/actions/ThemeActionCreators';
import {ChannelTypes, ME} from '~/Constants';
import {AppBadge} from '~/components/AppBadge';
import {ChannelIndexPage} from '~/components/channel/ChannelIndexPage';
import {ChannelLayout} from '~/components/channel/ChannelLayout';
import {DMLayout} from '~/components/channel/dm/DMLayout';
import {AppLayout} from '~/components/layout/AppLayout';
import {AuthLayout} from '~/components/layout/AuthLayout';
import {FavoritesLayout} from '~/components/layout/FavoritesLayout';
import {GuildLayout} from '~/components/layout/GuildLayout';
import {GuildsLayout} from '~/components/layout/GuildsLayout';
import {KeyboardModeListener} from '~/components/layout/KeyboardModeListener';
import {MobileBottomNav} from '~/components/layout/MobileBottomNav';
import {SplashScreen} from '~/components/layout/SplashScreen';
import {BookmarksBottomSheet} from '~/components/modals/BookmarksBottomSheet';
import {StatusChangeBottomSheet} from '~/components/modals/StatusChangeBottomSheet';
import AuthorizeIPPage from '~/components/pages/AuthorizeIPPage';
import EmailRevertPage from '~/components/pages/EmailRevertPage';
import ForgotPasswordPage from '~/components/pages/ForgotPasswordPage';
import GiftLoginPage from '~/components/pages/GiftLoginPage';
import GiftRegisterPage from '~/components/pages/GiftRegisterPage';
import InviteLoginPage from '~/components/pages/InviteLoginPage';
import InviteRegisterPage from '~/components/pages/InviteRegisterPage';
import LoginPage from '~/components/pages/LoginPage';
import {NotFoundPage} from '~/components/pages/NotFoundPage';
import {NotificationsPage} from '~/components/pages/NotificationsPage';
import OAuthAuthorizePage from '~/components/pages/OAuthAuthorizePage';
import PendingVerificationPage from '~/components/pages/PendingVerificationPage';
import PremiumCallbackPage from '~/components/pages/PremiumCallbackPage';
import RegisterPage from '~/components/pages/RegisterPage';
import {ReportPage} from '~/components/pages/ReportPage';
import ResetPasswordPage from '~/components/pages/ResetPasswordPage';
import ThemeLoginPage from '~/components/pages/ThemeLoginPage';
import ThemeRegisterPage from '~/components/pages/ThemeRegisterPage';
import VerifyEmailPage from '~/components/pages/VerifyEmailPage';
import {YouPage} from '~/components/pages/YouPage';
import {
createRootRoute,
createRoute,
createRouter,
Redirect,
type RouteConfig,
useLocation,
useParams,
} from '~/lib/router';
import SessionManager from '~/lib/SessionManager';
import {Routes} from '~/Routes';
import * as PushSubscriptionService from '~/services/push/PushSubscriptionService';
import AccountManager from '~/stores/AccountManager';
import AuthenticationStore from '~/stores/AuthenticationStore';
import ChannelStore from '~/stores/ChannelStore';
import ConnectionStore from '~/stores/ConnectionStore';
import InitializationStore from '~/stores/InitializationStore';
import LocationStore from '~/stores/LocationStore';
import MobileLayoutStore from '~/stores/MobileLayoutStore';
import NavigationStore from '~/stores/NavigationStore';
import SelectedChannelStore from '~/stores/SelectedChannelStore';
import UserStore from '~/stores/UserStore';
import {compareChannelPosition, filterViewableChannels} from '~/utils/channelShared';
import {navigateToWithMobileHistory} from '~/utils/MobileNavigation';
import {isInstalledPwa} from '~/utils/PwaUtils';
import * as RouterUtils from '~/utils/RouterUtils';
import RuntimeConfigStore from './stores/RuntimeConfigStore';
const AUTO_REDIRECT_EXEMPT_PATHS = new Set<string>([
Routes.RESET_PASSWORD,
Routes.AUTHORIZE_IP,
Routes.EMAIL_REVERT,
Routes.VERIFY_EMAIL,
Routes.OAUTH_AUTHORIZE,
Routes.REPORT,
]);
const GuildChannelRouter: React.FC<{guildId: string; children?: React.ReactNode}> = observer(({guildId, children}) => {
const location = useLocation();
React.useEffect(() => {
if (guildId === ME || location.pathname === Routes.ME) {
return;
}
if (MobileLayoutStore.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 = SelectedChannelStore.selectedChannelIds.get(guildId);
if (selectedChannelId) {
const channel = ChannelStore.getChannel(selectedChannelId);
if (channel && channel.guildId === guildId) {
RouterUtils.replaceWith(Routes.guildChannel(guildId, selectedChannelId));
} else {
const channels = ChannelStore.getGuildChannels(guildId);
const viewableChannels = filterViewableChannels(channels).sort(compareChannelPosition);
if (viewableChannels.length > 0) {
const firstChannel = viewableChannels[0];
RouterUtils.replaceWith(Routes.guildChannel(guildId, firstChannel.id));
}
}
} else {
const channels = ChannelStore.getGuildChannels(guildId);
const viewableChannels = filterViewableChannels(channels).sort(compareChannelPosition);
if (viewableChannels.length > 0) {
const firstChannel = viewableChannels[0];
RouterUtils.replaceWith(Routes.guildChannel(guildId, firstChannel.id));
}
}
}
}
}, [guildId, location.pathname, MobileLayoutStore.enabled]);
if (guildId === ME || location.pathname === Routes.ME) {
return null;
}
return <GuildLayout>{children}</GuildLayout>;
});
const RootComponent: React.FC<{children?: React.ReactNode}> = observer(({children}) => {
const location = useLocation();
const isAuthenticated = AuthenticationStore.isAuthenticated;
const mobileLayoutState = MobileLayoutStore;
const [hasRestoredLocation, setHasRestoredLocation] = React.useState(false);
const currentUser = UserStore.currentUser;
const [hasHandledNotificationNav, setHasHandledNotificationNav] = React.useState(false);
const [previousMobileLayoutState, setPreviousMobileLayoutState] = React.useState(mobileLayoutState.enabled);
const lastMobileHistoryBuildRef = React.useRef<{ts: number; path: string} | null>(null);
const isLocationStoreHydrated = LocationStore.isHydrated;
const canNavigateToProtectedRoutes = InitializationStore.canNavigateToProtectedRoutes;
const hasStartedRestoreRef = React.useRef(false);
const pathname = location.pathname;
const isDesktopHandoff = location.searchParams.get('desktop_handoff') === '1';
const isAutoRedirectExemptRoute = AUTO_REDIRECT_EXEMPT_PATHS.has(pathname);
const shouldSkipAutoRedirect = isAutoRedirectExemptRoute || (pathname === Routes.LOGIN && isDesktopHandoff);
const isAuthRoute = React.useMemo(() => {
return (
pathname.startsWith(Routes.LOGIN) ||
pathname.startsWith(Routes.REGISTER) ||
pathname.startsWith(Routes.FORGOT_PASSWORD) ||
pathname.startsWith(Routes.RESET_PASSWORD) ||
pathname.startsWith(Routes.VERIFY_EMAIL) ||
pathname.startsWith(Routes.AUTHORIZE_IP) ||
pathname.startsWith(Routes.EMAIL_REVERT) ||
pathname.startsWith(Routes.OAUTH_AUTHORIZE) ||
pathname.startsWith(Routes.REPORT) ||
pathname.startsWith('/invite/') ||
pathname.startsWith('/gift/') ||
pathname.startsWith('/theme/')
);
}, [pathname]);
const shouldBypassGateway = isAuthRoute && pathname !== Routes.PENDING_VERIFICATION;
const authToken = AuthenticationStore.authToken;
React.useEffect(() => {
if (!SessionManager.isInitialized) {
return;
}
if (AccountManager.isSwitching) {
return;
}
const isAuth = AuthenticationStore.isAuthenticated;
if (shouldBypassGateway) {
if (isAuth) {
if (!shouldSkipAutoRedirect) {
RouterUtils.replaceWith(Routes.ME);
}
return;
}
if (ConnectionStore.isConnected || ConnectionStore.isConnecting || ConnectionStore.socket) {
ConnectionStore.logout();
}
return;
}
if (!isAuth) {
const current = pathname + window.location.search;
RouterUtils.replaceWith(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(current)}`);
return;
}
if (isAuth && InitializationStore.isLoading) {
void AuthenticationActionCreators.ensureSessionStarted();
}
}, [
SessionManager.isInitialized,
authToken,
AccountManager.isSwitching,
AuthenticationStore.isAuthenticated,
ConnectionStore.isConnected,
ConnectionStore.isConnecting,
InitializationStore.isLoading,
shouldBypassGateway,
shouldSkipAutoRedirect,
]);
React.useEffect(() => {
if (
!isAuthenticated ||
hasRestoredLocation ||
hasStartedRestoreRef.current ||
!canNavigateToProtectedRoutes ||
!isLocationStoreHydrated
) {
return;
}
if (location.pathname === Routes.HOME) {
return;
}
hasStartedRestoreRef.current = true;
setHasRestoredLocation(true);
const lastLocation = LocationStore.getLastLocation();
if (lastLocation && lastLocation !== location.pathname && location.pathname === Routes.ME) {
navigateToWithMobileHistory(lastLocation, mobileLayoutState.enabled);
} else if (mobileLayoutState.enabled) {
const p = location.pathname;
if ((Routes.isDMRoute(p) && p !== Routes.ME) || (Routes.isGuildChannelRoute(p) && p.split('/').length === 4)) {
navigateToWithMobileHistory(p, true);
setHasHandledNotificationNav(true);
}
}
}, [
isAuthenticated,
hasRestoredLocation,
mobileLayoutState.enabled,
isLocationStoreHydrated,
canNavigateToProtectedRoutes,
location.pathname,
]);
React.useEffect(() => {
if (!isAuthenticated || !hasRestoredLocation) return;
if (previousMobileLayoutState !== mobileLayoutState.enabled) {
setPreviousMobileLayoutState(mobileLayoutState.enabled);
if (mobileLayoutState.enabled) {
const currentPath = location.pathname;
if (
(Routes.isDMRoute(currentPath) && currentPath !== Routes.ME) ||
(Routes.isGuildChannelRoute(currentPath) && currentPath.split('/').length === 4)
) {
navigateToWithMobileHistory(currentPath, true);
}
}
}
}, [isAuthenticated, hasRestoredLocation, mobileLayoutState.enabled, previousMobileLayoutState, location.pathname]);
React.useEffect(() => {
const shouldSaveLocation = Routes.isChannelRoute(location.pathname) || Routes.isSpecialPage(location.pathname);
if (isAuthenticated && shouldSaveLocation) {
LocationStore.saveLocation(location.pathname);
}
}, [isAuthenticated, location.pathname]);
React.useEffect(() => {
if (!isAuthenticated || !hasRestoredLocation) return;
if (previousMobileLayoutState !== mobileLayoutState.enabled) {
setPreviousMobileLayoutState(mobileLayoutState.enabled);
if (mobileLayoutState.enabled) {
const currentPath = location.pathname;
const now = Date.now();
const last = lastMobileHistoryBuildRef.current;
if (last && last.path === currentPath && now - last.ts < 1500) {
return;
}
lastMobileHistoryBuildRef.current = {ts: now, path: currentPath};
if (
(Routes.isDMRoute(currentPath) && currentPath !== Routes.ME) ||
(Routes.isGuildChannelRoute(currentPath) && currentPath.split('/').length === 4)
) {
if (Routes.isDMRoute(currentPath) && currentPath !== Routes.ME) {
RouterUtils.replaceWith(Routes.ME);
setTimeout(() => RouterUtils.transitionTo(currentPath), 0);
} else if (Routes.isGuildChannelRoute(currentPath) && currentPath.split('/').length === 4) {
const parts = currentPath.split('/');
const guildId = parts[2];
const guildPath = Routes.guildChannel(guildId);
RouterUtils.replaceWith(guildPath);
setTimeout(() => RouterUtils.transitionTo(currentPath), 0);
}
}
}
}
}, [isAuthenticated, hasRestoredLocation, mobileLayoutState.enabled, previousMobileLayoutState, location.pathname]);
const navigateWithHistoryStack = React.useCallback(
(url: string) => {
navigateToWithMobileHistory(url, mobileLayoutState.enabled);
},
[mobileLayoutState.enabled],
);
React.useEffect(() => {
if (!isAuthenticated || !mobileLayoutState.enabled) return;
const handleNotificationNavigate = (event: MessageEvent) => {
if (event.data?.type === 'NOTIFICATION_CLICK_NAVIGATE') {
if (hasHandledNotificationNav) {
return;
}
const url = event.data.url;
const targetUserId = event.data.targetUserId as string | undefined;
void (async () => {
if (targetUserId && targetUserId !== AccountManager.currentUserId && AccountManager.canSwitchAccounts) {
try {
await AccountManager.switchToAccount(targetUserId);
} catch (error) {
console.error('Failed to switch account for notification', error);
}
}
navigateWithHistoryStack(url);
setHasHandledNotificationNav(true);
})();
return;
}
if (event.data?.type === 'PUSH_SUBSCRIPTION_CHANGE') {
if (isInstalledPwa()) {
void PushSubscriptionService.registerPushSubscription();
}
}
};
if (!hasHandledNotificationNav) {
const urlParams = location.searchParams;
if (urlParams.get('fromNotification') === '1') {
const newParams = new URLSearchParams(urlParams);
newParams.delete('fromNotification');
const cleanPath = location.pathname + (newParams.toString() ? `?${newParams.toString()}` : '');
navigateWithHistoryStack(cleanPath);
setHasHandledNotificationNav(true);
}
}
navigator.serviceWorker?.addEventListener('message', handleNotificationNavigate);
return () => {
navigator.serviceWorker?.removeEventListener('message', handleNotificationNavigate);
};
}, [isAuthenticated, mobileLayoutState.enabled, hasHandledNotificationNav, location, navigateWithHistoryStack]);
React.useEffect(() => {
if (currentUser?.pendingManualVerification) {
if (
pathname !== Routes.PENDING_VERIFICATION &&
!pathname.startsWith('/login') &&
!pathname.startsWith('/register')
) {
RouterUtils.replaceWith(Routes.PENDING_VERIFICATION);
}
}
}, [currentUser, pathname]);
const showBottomNav =
mobileLayoutState.enabled &&
(location.pathname === Routes.ME ||
Routes.isFavoritesRoute(location.pathname) ||
location.pathname === Routes.NOTIFICATIONS ||
location.pathname === Routes.YOU ||
(Routes.isGuildChannelRoute(location.pathname) && location.pathname.split('/').length === 3));
if (isAuthenticated && !canNavigateToProtectedRoutes && !shouldBypassGateway) {
return <SplashScreen />;
}
return (
<>
<KeyboardModeListener />
{children}
{showBottomNav && currentUser && <MobileBottomNav currentUser={currentUser} />}
</>
);
});
const rootRoute = createRootRoute({
layout: ({children}) => <>{children}</>,
});
const notFoundRoute = createRoute({
id: '__notFound',
path: '/__notfound',
component: () => <NotFoundPage />,
});
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'home',
path: '/',
onEnter: () => new Redirect(Routes.ME),
});
const authLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'authLayout',
layout: ({children}) => <AuthLayout>{children}</AuthLayout>,
});
const loginRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'login',
path: '/login',
onEnter: () => {
if (AuthenticationStore.isAuthenticated) {
const search = window.location.search;
const qp = new URLSearchParams(search);
const isDesktopHandoff = qp.get('desktop_handoff') === '1';
if (isDesktopHandoff) {
return undefined;
}
const redirectTo = qp.get('redirect_to');
return new Redirect(redirectTo || Routes.ME);
}
return undefined;
},
component: () => <LoginPage />,
});
const registerRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'register',
path: '/register',
onEnter: () => {
if (AuthenticationStore.isAuthenticated) {
const search = window.location.search;
const qp = new URLSearchParams(search);
const redirectTo = qp.get('redirect_to');
return new Redirect(redirectTo || Routes.ME);
}
return undefined;
},
component: () => <RegisterPage />,
});
const oauthAuthorizeRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'oauthAuthorize',
path: Routes.OAUTH_AUTHORIZE,
onEnter: () => {
if (!AuthenticationStore.isAuthenticated) {
const current = window.location.pathname + window.location.search;
return new Redirect(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(current)}`);
}
return undefined;
},
component: () => <OAuthAuthorizePage />,
});
const inviteRegisterRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'inviteRegister',
path: '/invite/:code',
onEnter: (ctx) => {
if (AuthenticationStore.isAuthenticated) {
const code = ctx.params.code;
if (code) {
InviteActionCreators.openAcceptModal(code);
}
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <InviteRegisterPage />,
});
const inviteLoginRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'inviteLogin',
path: '/invite/:code/login',
onEnter: (ctx) => {
if (AuthenticationStore.isAuthenticated) {
const code = ctx.params.code;
if (code) {
InviteActionCreators.openAcceptModal(code);
}
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <InviteLoginPage />,
});
const giftRegisterRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'giftRegister',
path: '/gift/:code',
onEnter: (ctx) => {
if (AuthenticationStore.isAuthenticated) {
const code = ctx.params.code;
if (code) {
GiftActionCreators.openAcceptModal(code);
}
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <GiftRegisterPage />,
});
const giftLoginRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'giftLogin',
path: '/gift/:code/login',
onEnter: (ctx) => {
if (AuthenticationStore.isAuthenticated) {
const code = ctx.params.code;
if (code) {
GiftActionCreators.openAcceptModal(code);
}
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <GiftLoginPage />,
});
const forgotPasswordRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'forgotPassword',
path: Routes.FORGOT_PASSWORD,
onEnter: () => {
if (AuthenticationStore.isAuthenticated) {
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <ForgotPasswordPage />,
});
const resetPasswordRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'resetPassword',
path: Routes.RESET_PASSWORD,
component: () => <ResetPasswordPage />,
});
const emailRevertRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'emailRevert',
path: Routes.EMAIL_REVERT,
component: () => <EmailRevertPage />,
});
const verifyEmailRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'verifyEmail',
path: Routes.VERIFY_EMAIL,
component: () => <VerifyEmailPage />,
});
const authorizeIPRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'authorizeIP',
path: Routes.AUTHORIZE_IP,
component: () => <AuthorizeIPPage />,
});
const pendingVerificationRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'pendingVerification',
path: Routes.PENDING_VERIFICATION,
component: () => <PendingVerificationPage />,
});
const reportRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'report',
path: Routes.REPORT,
component: () => <ReportPage />,
});
const appLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'appLayout',
onEnter: () => {
if (!SessionManager.isInitialized) {
return undefined;
}
if (!AuthenticationStore.isAuthenticated) {
const current = window.location.pathname + window.location.search;
return new Redirect(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(current)}`);
}
return undefined;
},
layout: ({children}) => (
<>
<AppBadge />
<AppLayout>{children}</AppLayout>
</>
),
});
const guildsLayoutRoute = createRoute({
getParentRoute: () => appLayoutRoute,
id: 'guildsLayout',
layout: ({children}) => <GuildsLayout>{children}</GuildsLayout>,
});
const notificationsRoute = createRoute({
getParentRoute: () => appLayoutRoute,
id: 'notifications',
path: Routes.NOTIFICATIONS,
component: () => {
const [bookmarksSheetOpen, setBookmarksSheetOpen] = React.useState(false);
return (
<>
<NotificationsPage onBookmarksClick={() => setBookmarksSheetOpen(true)} />
<BookmarksBottomSheet isOpen={bookmarksSheetOpen} onClose={() => setBookmarksSheetOpen(false)} />
</>
);
},
});
const youRoute = createRoute({
getParentRoute: () => appLayoutRoute,
id: 'you',
path: Routes.YOU,
component: () => {
const [statusSheetOpen, setStatusSheetOpen] = React.useState(false);
return (
<>
<YouPage onAvatarClick={() => setStatusSheetOpen(true)} />
<StatusChangeBottomSheet isOpen={statusSheetOpen} onClose={() => setStatusSheetOpen(false)} />
</>
);
},
});
const premiumCallbackRoute = createRoute({
getParentRoute: () => appLayoutRoute,
id: 'premiumCallback',
path: Routes.PREMIUM_CALLBACK,
component: () => <PremiumCallbackPage />,
});
const themeRegisterRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'themeRegister',
path: Routes.THEME_REGISTER,
onEnter: (ctx) => {
if (AuthenticationStore.isAuthenticated) {
const themeId = ctx.params.themeId;
if (themeId) {
ThemeActionCreators.openAcceptModal(themeId, i18n);
}
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <ThemeRegisterPage />,
});
const themeLoginRoute = createRoute({
getParentRoute: () => authLayoutRoute,
id: 'themeLogin',
path: Routes.THEME_LOGIN,
onEnter: (ctx) => {
if (AuthenticationStore.isAuthenticated) {
const themeId = ctx.params.themeId;
if (themeId) {
ThemeActionCreators.openAcceptModal(themeId, i18n);
}
return new Redirect(Routes.ME);
}
return undefined;
},
component: () => <ThemeLoginPage />,
});
const bookmarksRoute = createRoute({
getParentRoute: () => guildsLayoutRoute,
id: 'bookmarks',
path: Routes.BOOKMARKS,
component: () => <DMLayout />,
});
const mentionsRoute = createRoute({
getParentRoute: () => guildsLayoutRoute,
id: 'mentions',
path: Routes.MENTIONS,
component: () => <DMLayout />,
});
const meRoute = createRoute({
getParentRoute: () => guildsLayoutRoute,
id: 'me',
path: '/channels/@me',
component: observer(() => {
const isMobileLayout = MobileLayoutStore.enabled;
React.useEffect(() => {
if (!isMobileLayout && SelectedChannelStore.selectedChannelIds.has(ME)) {
SelectedChannelStore.clearGuildSelection(ME);
}
}, [isMobileLayout]);
return <DMLayout />;
}),
});
const favoritesRoute = createRoute({
getParentRoute: () => guildsLayoutRoute,
id: 'favorites',
path: '/channels/@favorites',
layout: ({children}) => <FavoritesLayout>{children}</FavoritesLayout>,
});
const favoritesChannelRoute = createRoute({
getParentRoute: () => favoritesRoute,
id: 'favoritesChannel',
path: '/channels/@favorites/:channelId',
component: () => (
<ChannelLayout>
<ChannelIndexPage />
</ChannelLayout>
),
});
const channelsRoute = createRoute({
getParentRoute: () => guildsLayoutRoute,
id: 'channels',
path: '/channels/:guildId',
layout: ({children}) => {
const params = useParams() as {guildId: string};
const {guildId} = params;
if (guildId === ME) {
return <DMLayout>{children}</DMLayout>;
}
return guildId ? <GuildChannelRouter guildId={guildId}>{children}</GuildChannelRouter> : null;
},
});
const channelRoute = createRoute({
getParentRoute: () => channelsRoute,
id: 'channel',
path: '/channels/:guildId/:channelId',
onEnter: (ctx) => {
const {guildId, channelId} = ctx.params;
const channel = ChannelStore.getChannel(channelId);
if (channel && (channel.type === ChannelTypes.GUILD_CATEGORY || channel.type === ChannelTypes.GUILD_LINK)) {
return new Redirect(Routes.guildChannel(guildId));
}
return undefined;
},
component: () => (
<ChannelLayout>
<ChannelIndexPage />
</ChannelLayout>
),
});
const messageRoute = createRoute({
getParentRoute: () => channelRoute,
id: 'message',
path: '/channels/:guildId/:channelId/:messageId',
onEnter: (ctx) => {
const {guildId, channelId} = ctx.params;
const channel = ChannelStore.getChannel(channelId);
if (channel && (channel.type === ChannelTypes.GUILD_CATEGORY || channel.type === ChannelTypes.GUILD_LINK)) {
return new Redirect(Routes.guildChannel(guildId));
}
return undefined;
},
component: () => (
<ChannelLayout>
<ChannelIndexPage />
</ChannelLayout>
),
});
const authRouteTree = authLayoutRoute.addChildren([
loginRoute,
registerRoute,
oauthAuthorizeRoute,
inviteRegisterRoute,
inviteLoginRoute,
themeRegisterRoute,
themeLoginRoute,
forgotPasswordRoute,
resetPasswordRoute,
emailRevertRoute,
verifyEmailRoute,
authorizeIPRoute,
pendingVerificationRoute,
reportRoute,
...(RuntimeConfigStore.isSelfHosted() ? [] : [giftRegisterRoute, giftLoginRoute]),
]);
const routeTree = rootRoute.addChildren([
homeRoute,
notFoundRoute,
authRouteTree,
appLayoutRoute.addChildren([
notificationsRoute,
youRoute,
premiumCallbackRoute,
guildsLayoutRoute.addChildren([
bookmarksRoute,
mentionsRoute,
meRoute,
favoritesRoute.addChildren([favoritesChannelRoute]),
channelsRoute.addChildren([channelRoute.addChildren([messageRoute])]),
]),
]),
]);
const routes: Array<RouteConfig> = routeTree.build();
const rootComponentRoute = routes.find((r) => r.id === '__root');
if (rootComponentRoute) {
rootComponentRoute.layout = (props) => <RootComponent>{props.children}</RootComponent>;
}
export const router = createRouter({
routes,
history: RouterUtils.getHistory() ?? undefined,
notFoundRouteId: '__notFound',
scrollRestoration: 'top',
});
NavigationStore.initialize(router);