Change _reactionCountsFor and _myReactionsFor to prefer local state for immediate UI updates after toggle reactions, falling back to post model data when no local state exists.
228 lines
6.9 KiB
Dart
228 lines
6.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/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/profile_screen.dart';
|
|
import '../screens/profile/viewable_profile_screen.dart';
|
|
import '../screens/auth/auth_gate.dart';
|
|
import '../screens/secure_chat/secure_chat_full_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) => ViewableProfileScreen(
|
|
handle: state.pathParameters['username'] ?? '',
|
|
),
|
|
),
|
|
GoRoute(
|
|
path: quipCreate,
|
|
parentNavigatorKey: rootNavigatorKey,
|
|
builder: (_, __) => const QuipCreationFlow(),
|
|
),
|
|
GoRoute(
|
|
path: secureChat,
|
|
parentNavigatorKey: rootNavigatorKey,
|
|
builder: (_, __) => const SecureChatFullScreen(),
|
|
),
|
|
StatefulShellRoute.indexedStack(
|
|
builder: (context, state, navigationShell) => AuthGate(
|
|
authenticatedChild: HomeShell(navigationShell: navigationShell),
|
|
),
|
|
branches: [
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: homeAlias,
|
|
builder: (_, __) => const FeedPersonalScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: beaconPrefix,
|
|
builder: (_, state) {
|
|
final latParam = state.uri.queryParameters['lat'];
|
|
final longParam = state.uri.queryParameters['long'];
|
|
final lat = latParam != null ? double.tryParse(latParam) : null;
|
|
final long = longParam != null ? double.tryParse(longParam) : null;
|
|
|
|
if (lat != null && long != null) {
|
|
return BeaconScreen(initialMapCenter: LatLng(lat, long));
|
|
}
|
|
|
|
return const BeaconScreen();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: quips,
|
|
builder: (_, __) => const QuipsFeedScreen(),
|
|
),
|
|
],
|
|
),
|
|
StatefulShellBranch(
|
|
routes: [
|
|
GoRoute(
|
|
path: profile,
|
|
builder: (_, __) => const ProfileScreen(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
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(),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
|
|
static int _adminIndexForPath(String path) {
|
|
if (path.startsWith('/admin/moderation')) return 1;
|
|
if (path.startsWith('/admin/users')) return 2;
|
|
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://gosojorn.com/u/username
|
|
static String getProfileUrl(
|
|
String username, {
|
|
String baseUrl = 'https://gosojorn.com',
|
|
}) {
|
|
return '$baseUrl/u/$username';
|
|
}
|
|
|
|
/// Get shareable URL for a post (future implementation)
|
|
/// Returns: https://gosojorn.com/p/postid
|
|
static String getPostUrl(
|
|
String postId, {
|
|
String baseUrl = 'https://gosojorn.com',
|
|
}) {
|
|
return '$baseUrl/p/$postId';
|
|
}
|
|
|
|
/// Get shareable URL for a beacon location
|
|
/// Returns: https://gosojorn.com/beacon?lat=...&long=...
|
|
static String getBeaconUrl(
|
|
double lat,
|
|
double long, {
|
|
String baseUrl = 'https://gosojorn.com',
|
|
}) {
|
|
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();
|
|
}
|
|
}
|