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
262 lines
7.9 KiB
Dart
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();
|
|
}
|
|
}
|