sojorn/sojorn_app/lib/routes/app_routes.dart
2026-02-15 00:33:24 -06:00

333 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:latlong2/latlong.dart';
import '../services/auth_service.dart';
import '../services/api_service.dart';
// ── Eager (critical path) ─────────────────────────────────────────────
import '../screens/home/feed_personal_screen.dart';
import '../screens/home/home_shell.dart';
import '../screens/auth/auth_gate.dart';
// ── Eager (admin small, behind role check) ──────────────────────────
import '../screens/admin/admin_dashboard_screen.dart';
import '../screens/admin/admin_scaffold.dart';
import '../screens/admin/admin_user_base_screen.dart';
import '../screens/admin/moderation_queue_screen.dart';
import '../screens/admin/admin_content_tools_screen.dart';
// ── Deferred (code-split for web) ─────────────────────────────────────
import '../screens/beacon/beacon_screen.dart' deferred as beacon_lib;
import '../screens/quips/create/quip_creation_flow.dart' deferred as quip_create_lib;
import '../screens/quips/feed/quips_feed_screen.dart' deferred as quips_feed_lib;
import '../screens/profile/viewable_profile_screen.dart' deferred as profile_lib;
import '../screens/profile/blocked_users_screen.dart' deferred as blocked_lib;
import '../screens/secure_chat/secure_chat_full_screen.dart' deferred as secure_chat_lib;
import '../screens/secure_chat/secure_chat_loader_screen.dart' deferred as chat_loader_lib;
import '../screens/post/threaded_conversation_screen.dart' deferred as threaded_lib;
import '../screens/clusters/clusters_screen.dart' deferred as clusters_lib;
import '../screens/clusters/group_screen.dart' deferred as group_screen_lib;
/// App routing config (GoRouter).
class AppRoutes {
static final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
static const String home = '/';
static const String homeAlias = '/home';
static const String userPrefix = '/u';
static const String postPrefix = '/p';
static const String beaconPrefix = '/beacon';
static const String quips = '/quips';
static const String profile = '/profile';
static const String secureChat = '/secure-chat';
static const String quipCreate = '/quips/create';
static const String clusters = '/clusters';
static final AuthRefreshNotifier _authRefreshNotifier =
AuthRefreshNotifier(AuthService.instance.authStateChanges);
static final GoRouter router = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: homeAlias,
refreshListenable: _authRefreshNotifier,
redirect: _adminRedirect,
routes: [
GoRoute(
path: home,
redirect: (_, __) => homeAlias,
),
GoRoute(
path: '$userPrefix/:username',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => _deferred(
profile_lib.loadLibrary,
() => profile_lib.UnifiedProfileScreen(
handle: state.pathParameters['username'] ?? '',
),
),
),
GoRoute(
path: quipCreate,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => _deferred(
quip_create_lib.loadLibrary,
() => quip_create_lib.QuipCreationFlow(),
),
),
GoRoute(
path: secureChat,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => _deferred(
secure_chat_lib.loadLibrary,
() => secure_chat_lib.SecureChatFullScreen(),
),
routes: [
GoRoute(
path: ':id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => _deferred(
chat_loader_lib.loadLibrary,
() => chat_loader_lib.SecureChatLoaderScreen(
conversationId: state.pathParameters['id'] ?? '',
),
),
),
],
),
GoRoute(
path: '$postPrefix/:id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => _deferred(
threaded_lib.loadLibrary,
() => threaded_lib.ThreadedConversationScreen(
rootPostId: state.pathParameters['id'] ?? '',
),
),
),
GoRoute(
path: clusters,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => _deferred(
clusters_lib.loadLibrary,
() => clusters_lib.ClustersScreen(),
),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) => AuthGate(
authenticatedChild: HomeShell(navigationShell: navigationShell),
),
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: homeAlias,
builder: (_, __) => const FeedPersonalScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: quips,
builder: (_, state) => _deferred(
quips_feed_lib.loadLibrary,
() => quips_feed_lib.QuipsFeedScreen(
initialPostId: state.uri.queryParameters['postId'],
),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/beacon',
builder: (_, __) => _deferred(
beacon_lib.loadLibrary,
() => beacon_lib.BeaconScreen(),
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: profile,
builder: (_, __) => _deferred(
profile_lib.loadLibrary,
() => profile_lib.UnifiedProfileScreen(),
),
routes: [
GoRoute(
path: 'blocked',
builder: (_, __) => _deferred(
blocked_lib.loadLibrary,
() => blocked_lib.BlockedUsersScreen(),
),
),
],
),
],
),
],
),
ShellRoute(
builder: (context, state, child) {
final index = _adminIndexForPath(state.uri.path);
return AdminScaffold(selectedIndex: index, child: child);
},
routes: [
GoRoute(
path: '/admin',
builder: (_, __) => const AdminDashboardScreen(),
),
GoRoute(
path: '/admin/moderation',
builder: (_, __) => const ModerationQueueScreen(),
),
GoRoute(
path: '/admin/users',
builder: (_, __) => const AdminUserBaseScreen(),
),
GoRoute(
path: '/admin/content-tools',
builder: (_, __) => const AdminContentToolsScreen(),
),
],
),
],
);
static int _adminIndexForPath(String path) {
if (path.startsWith('/admin/moderation')) return 1;
if (path.startsWith('/admin/users')) return 2;
if (path.startsWith('/admin/content-tools')) return 3;
return 0;
}
static FutureOr<String?> _adminRedirect(
BuildContext context,
GoRouterState state,
) async {
final path = state.uri.path;
if (!path.startsWith('/admin')) return null;
final user = AuthService.instance.currentUser;
if (user == null) return homeAlias;
try {
final data = await ApiService.instance.callGoApi('/profile', method: 'GET');
final profile = data['profile'];
if (profile is Map<String, dynamic>) {
final role = profile['role'] as String?;
if (role == 'admin' || role == 'moderator') {
return null;
}
}
} catch (_) {}
return homeAlias;
}
/// Navigate to a user profile by username
static void navigateToProfile(BuildContext context, String username) {
context.push('/u/$username');
}
/// Get shareable URL for a user profile
/// Returns: https://sojorn.net/u/username
static String getProfileUrl(
String username, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/u/$username';
}
/// Get shareable URL for a quip
/// Returns: https://sojorn.net/quips?postId=postid
static String getQuipUrl(
String postId, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/quips?postId=$postId';
}
/// Get shareable URL for a post
/// Returns: https://sojorn.net/p/postid
static String getPostUrl(
String postId, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/p/$postId';
}
/// Get shareable URL for a beacon location
/// Returns: https://sojorn.net/beacon?lat=...&long=...
static String getBeaconUrl(
double lat,
double long, {
String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/beacon?lat=${lat.toStringAsFixed(6)}&long=${long.toStringAsFixed(6)}';
}
/// Navigate to a beacon location
static void navigateToBeacon(BuildContext context, LatLng location) {
final url =
'/beacon?lat=${location.latitude.toStringAsFixed(6)}&long=${location.longitude.toStringAsFixed(6)}';
context.push(url);
}
/// Navigate to secure chat
static void navigateToSecureChat(BuildContext context) {
context.push(secureChat);
}
/// Navigate to clusters/capsules discovery
static void navigateToClusters(BuildContext context) {
context.push(clusters);
}
}
class AuthRefreshNotifier extends ChangeNotifier {
late final StreamSubscription<AuthState> _subscription;
AuthRefreshNotifier(Stream<AuthState> stream) {
_subscription = stream.listen((_) => notifyListeners());
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
// ── Deferred loading helper ───────────────────────────────────────────
/// Wraps a deferred import in a FutureBuilder that shows a loading
/// spinner until the JS chunk is fetched, then builds the real screen.
Widget _deferred(Future<void> Function() load, Widget Function() build) {
return FutureBuilder<void>(
future: load(),
builder: (_, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snap.hasError) {
return const Scaffold(
body: Center(
child: Text('Failed to load. Please go back and try again.'),
),
);
}
return build();
},
);
}