sojorn/sojorn_app/lib/routes/app_routes.dart
Patrick Britton 257acb0e51 refactor: unify post widgets, profile screens, and tree-shake dead code
Phase 1 - Post Widget Unification:
- Added PostViewMode.thread for compact thread styling
- Updated SojornPostCard, PostBody, PostLinkPreview, PostMedia for thread mode
- Migrated feed_personal_screen, profile_screen, viewable_profile_screen to sojornPostCard
- Deleted deprecated: post_card.dart, post_item.dart, reading_post_card.dart

Phase 2 - Profile Screen Unification:
- Renamed ViewableProfileScreen to UnifiedProfileScreen
- Made handle optional (null = own profile mode)
- Added auth listener, auto-create profile, avatar actions for own profile
- Added connections navigation to stats for own profile
- Updated all routes and references across codebase
- Deleted old profile_screen.dart

Phase 3 - Tree Shaking:
- Deleted 9 orphan files: compose_and_chat_fab, sojorn_top_bar, sojorn_app_bar,
  sojorn_dialog, sojorn_card, glassmorphic_quips_sheet, kinetic_thread_widget,
  reaction_strip, smart_reaction_button
2026-02-09 17:55:39 -06:00

262 lines
7.9 KiB
Dart

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';
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';
import '../screens/beacon/beacon_screen.dart';
import '../screens/home/feed_personal_screen.dart';
import '../screens/home/home_shell.dart';
import '../screens/quips/create/quip_creation_flow.dart';
import '../screens/quips/feed/quips_feed_screen.dart';
import '../screens/profile/viewable_profile_screen.dart';
import '../screens/profile/blocked_users_screen.dart';
import '../screens/auth/auth_gate.dart';
import '../screens/discover/discover_screen.dart';
import '../screens/secure_chat/secure_chat_full_screen.dart';
import '../screens/secure_chat/secure_chat_loader_screen.dart';
import '../screens/post/threaded_conversation_screen.dart';
import '../screens/notifications/notifications_screen.dart';
/// 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 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) => UnifiedProfileScreen(
handle: state.pathParameters['username'] ?? '',
),
),
GoRoute(
path: quipCreate,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const QuipCreationFlow(),
),
GoRoute(
path: secureChat,
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const SecureChatFullScreen(),
routes: [
GoRoute(
path: ':id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => SecureChatLoaderScreen(
conversationId: state.pathParameters['id'] ?? '',
),
),
],
),
GoRoute(
path: '$postPrefix/:id',
parentNavigatorKey: rootNavigatorKey,
builder: (_, state) => ThreadedConversationScreen(
rootPostId: state.pathParameters['id'] ?? '',
),
),
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) => QuipsFeedScreen(
initialPostId: state.uri.queryParameters['postId'],
),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/beacon',
builder: (_, __) => const BeaconScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: profile,
builder: (_, __) => const UnifiedProfileScreen(),
routes: [
GoRoute(
path: 'blocked',
builder: (_, __) => const 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);
}
}
class AuthRefreshNotifier extends ChangeNotifier {
late final StreamSubscription<AuthState> _subscription;
AuthRefreshNotifier(Stream<AuthState> stream) {
_subscription = stream.listen((_) => notifyListeners());
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}