Fix reaction UI updates - prioritize local state over post model
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.
This commit is contained in:
parent
a3c609c0cc
commit
558272c9a2
|
|
@ -168,7 +168,7 @@ class AppRoutes {
|
|||
|
||||
/// Navigate to a user profile by username
|
||||
static void navigateToProfile(BuildContext context, String username) {
|
||||
context.go('/u/$username');
|
||||
context.push('/u/$username');
|
||||
}
|
||||
|
||||
/// Get shareable URL for a user profile
|
||||
|
|
@ -203,12 +203,12 @@ class AppRoutes {
|
|||
static void navigateToBeacon(BuildContext context, LatLng location) {
|
||||
final url =
|
||||
'/beacon?lat=${location.latitude.toStringAsFixed(6)}&long=${location.longitude.toStringAsFixed(6)}';
|
||||
context.go(url);
|
||||
context.push(url);
|
||||
}
|
||||
|
||||
/// Navigate to secure chat
|
||||
static void navigateToSecureChat(BuildContext context) {
|
||||
context.go(secureChat);
|
||||
context.push(secureChat);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../routes/app_routes.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
|
@ -106,6 +108,11 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
|||
});
|
||||
|
||||
_seedReactionState(focusContext);
|
||||
|
||||
// Trigger a rebuild to show the seeded reactions
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
_slideController.forward(from: 0);
|
||||
_fadeController.forward(from: 0);
|
||||
}
|
||||
|
|
@ -121,31 +128,13 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
|||
|
||||
Future<void> _navigateToPost(String postId) async {
|
||||
if (_isTransitioning || _focusContext?.targetPost.id == postId) return;
|
||||
setState(() {
|
||||
_isTransitioning = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final focusContext = await api.getPostFocusContext(postId);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_focusContext = focusContext;
|
||||
});
|
||||
_seedReactionState(focusContext);
|
||||
_slideController.forward(from: 0);
|
||||
_fadeController.forward(from: 0);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isTransitioning = false);
|
||||
}
|
||||
}
|
||||
// Instead of just flipping state, we push a new route to maintain history
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ThreadedConversationScreen(rootPostId: postId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -163,7 +152,14 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
|||
elevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
onPressed: () => Navigator.of(context, rootNavigator: true).maybePop(),
|
||||
onPressed: () {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
// Fallback for direct links
|
||||
context.go(AppRoutes.homeAlias);
|
||||
}
|
||||
},
|
||||
icon: Icon(Icons.arrow_back, color: AppTheme.navyBlue),
|
||||
),
|
||||
title: AnimatedSwitcher(
|
||||
|
|
@ -809,14 +805,20 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
|||
}
|
||||
|
||||
void _seedReactionsForPost(Post post) {
|
||||
print('DEBUG: _seedReactionsForPost for post ${post.id}');
|
||||
print('DEBUG: post.reactions = ${post.reactions}');
|
||||
print('DEBUG: post.myReactions = ${post.myReactions}');
|
||||
|
||||
if (post.reactions != null) {
|
||||
_reactionCountsByPost.putIfAbsent(
|
||||
post.id,
|
||||
() => Map<String, int>.from(post.reactions!),
|
||||
);
|
||||
print('DEBUG: Seeded reaction counts: ${_reactionCountsByPost[post.id]}');
|
||||
}
|
||||
if (post.myReactions != null) {
|
||||
_myReactionsByPost.putIfAbsent(post.id, () => post.myReactions!.toSet());
|
||||
print('DEBUG: Seeded my reactions: ${_myReactionsByPost[post.id]}');
|
||||
}
|
||||
if (post.reactionUsers != null) {
|
||||
_reactionUsersByPost.putIfAbsent(
|
||||
|
|
@ -827,11 +829,30 @@ class _ThreadedConversationScreenState extends ConsumerState<ThreadedConversatio
|
|||
}
|
||||
|
||||
Map<String, int> _reactionCountsFor(Post post) {
|
||||
return _reactionCountsByPost[post.id] ?? post.reactions ?? {};
|
||||
// Debug: Check what we're getting from the post model
|
||||
print('DEBUG: _reactionCountsFor for post ${post.id}');
|
||||
print('DEBUG: post.reactions = ${post.reactions}');
|
||||
print('DEBUG: _reactionCountsByPost[${post.id}] = ${_reactionCountsByPost[post.id]}');
|
||||
|
||||
// Prefer local state for immediate updates after toggle reactions
|
||||
final localState = _reactionCountsByPost[post.id];
|
||||
if (localState != null) {
|
||||
print('DEBUG: Using local state: ${localState}');
|
||||
return localState;
|
||||
}
|
||||
// Fall back to post model if no local state
|
||||
print('DEBUG: Using post.reactions: ${post.reactions}');
|
||||
return post.reactions ?? {};
|
||||
}
|
||||
|
||||
Set<String> _myReactionsFor(Post post) {
|
||||
return _myReactionsByPost[post.id] ?? post.myReactions?.toSet() ?? <String>{};
|
||||
// Prefer local state for immediate updates after toggle reactions
|
||||
final localState = _myReactionsByPost[post.id];
|
||||
if (localState != null) {
|
||||
return localState;
|
||||
}
|
||||
// Fall back to post model if no local state
|
||||
return post.myReactions?.toSet() ?? <String>{};
|
||||
}
|
||||
|
||||
Map<String, List<String>>? _reactionUsersFor(Post post) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../routes/app_routes.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../models/post.dart';
|
||||
import '../../models/profile.dart';
|
||||
|
|
@ -527,15 +529,11 @@ class _ViewableProfileScreenState extends ConsumerState<ViewableProfileScreen>
|
|||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.pop();
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const ProfileScreen(),
|
||||
),
|
||||
);
|
||||
// Safely return to home/feed instead of pushing a redundant ProfileScreen
|
||||
context.go(AppRoutes.homeAlias);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import '../models/tone_analysis.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// ApiService - Single source of truth for all backend communication.
|
||||
/// Migration: Supabase direct reads are being replaced byGo API calls.
|
||||
class ApiService {
|
||||
final AuthService _authService;
|
||||
final http.Client _httpClient = http.Client();
|
||||
|
|
@ -28,7 +27,7 @@ class ApiService {
|
|||
static ApiService get instance =>
|
||||
_instance ??= ApiService(AuthService.instance);
|
||||
|
||||
/// Generic function caller for Edge Functions. Handles response parsing
|
||||
/// Generic caller for specialized edge endpoints. Handles response parsing
|
||||
/// and normalization across different response formats.
|
||||
Future<Map<String, dynamic>> _callFunction(
|
||||
String functionName, {
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@ class UploadResult {
|
|||
/// Progress callback for upload operations
|
||||
typedef UploadProgressCallback = void Function(double progress);
|
||||
|
||||
/// Service for uploading images AND videos to Cloudflare R2 via Supabase Edge Functions
|
||||
/// Service for uploading images AND videos to Cloudflare R2 via Go Backend
|
||||
class ImageUploadService {
|
||||
final AuthService _auth = AuthService.instance;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
|
||||
/// Get the current authentication token (Go backend or Supabase fallback)
|
||||
/// Get the current authentication token
|
||||
Future<String?> _getAuthToken() async {
|
||||
return _auth.accessToken;
|
||||
}
|
||||
|
|
@ -469,7 +469,7 @@ class ImageUploadService {
|
|||
|
||||
onProgress?.call(0.3);
|
||||
|
||||
print('Uploading image via edge function...');
|
||||
print('Uploading image via R2 bridge...');
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ dependencies:
|
|||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# Supabase (removed - migrated to Go backend)
|
||||
# Backend Services (Fully migrated to Go API)
|
||||
firebase_core: ^3.4.0
|
||||
firebase_messaging: ^15.1.0
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,4 @@ echo Starting Sojorn on Chrome...
|
|||
echo.
|
||||
|
||||
flutter run -d chrome ^
|
||||
--dart-define=SUPABASE_URL=https://zwkihedetedlatyvplyz.supabase.co ^
|
||||
--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp3a2loZWRldGVkbGF0eXZwbHl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc2Nzk3OTUsImV4cCI6MjA4MzI1NTc5NX0.7YyYOABjm7cpKa1DiefkI9bH8r6SICJ89nDK9sgUa0M ^
|
||||
--dart-define=API_BASE_URL=https://api.gosojorn.com/api/v1
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Get-Content $EnvPath | ForEach-Object {
|
|||
$values[$key] = $value
|
||||
}
|
||||
|
||||
$required = @('SUPABASE_URL', 'SUPABASE_ANON_KEY', 'API_BASE_URL')
|
||||
$required = @('API_BASE_URL')
|
||||
$missing = $required | Where-Object {
|
||||
-not $values.ContainsKey($_) -or [string]::IsNullOrWhiteSpace($values[$_])
|
||||
}
|
||||
|
|
@ -37,24 +37,9 @@ if ($missing.Count -gt 0) {
|
|||
}
|
||||
|
||||
$defineArgs = @(
|
||||
"--dart-define=SUPABASE_URL=$($values['SUPABASE_URL'])",
|
||||
"--dart-define=SUPABASE_ANON_KEY=$($values['SUPABASE_ANON_KEY'])",
|
||||
"--dart-define=API_BASE_URL=$($values['API_BASE_URL'])"
|
||||
)
|
||||
|
||||
$optionalDefines = @(
|
||||
'SUPABASE_PUBLISHABLE_KEY',
|
||||
'SUPABASE_SECRET_KEY',
|
||||
'SUPABASE_JWT_KID',
|
||||
'SUPABASE_JWKS_URI'
|
||||
)
|
||||
|
||||
foreach ($opt in $optionalDefines) {
|
||||
if ($values.ContainsKey($opt) -and -not [string]::IsNullOrWhiteSpace($values[$opt])) {
|
||||
$defineArgs += "--dart-define=$opt=$($values[$opt])"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Starting Sojorn on Chrome..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,4 @@ echo Starting Sojorn in development mode...
|
|||
echo.
|
||||
|
||||
flutter run ^
|
||||
--dart-define=SUPABASE_URL=https://zwkihedetedlatyvplyz.supabase.co ^
|
||||
--dart-define=SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp3a2loZWRldGVkbGF0eXZwbHl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njc2Nzk3OTUsImV4cCI6MjA4MzI1NTc5NX0.7YyYOABjm7cpKa1DiefkI9bH8r6SICJ89nDK9sgUa0M ^
|
||||
--dart-define=API_BASE_URL=https://api.gosojorn.com/api/v1
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Get-Content $EnvPath | ForEach-Object {
|
|||
$values[$key] = $value
|
||||
}
|
||||
|
||||
$required = @('SUPABASE_URL', 'SUPABASE_ANON_KEY', 'API_BASE_URL')
|
||||
$required = @('API_BASE_URL')
|
||||
$missing = $required | Where-Object {
|
||||
-not $values.ContainsKey($_) -or [string]::IsNullOrWhiteSpace($values[$_])
|
||||
}
|
||||
|
|
@ -37,24 +37,9 @@ if ($missing.Count -gt 0) {
|
|||
}
|
||||
|
||||
$defineArgs = @(
|
||||
"--dart-define=SUPABASE_URL=$($values['SUPABASE_URL'])",
|
||||
"--dart-define=SUPABASE_ANON_KEY=$($values['SUPABASE_ANON_KEY'])",
|
||||
"--dart-define=API_BASE_URL=$($values['API_BASE_URL'])"
|
||||
)
|
||||
|
||||
$optionalDefines = @(
|
||||
'SUPABASE_PUBLISHABLE_KEY',
|
||||
'SUPABASE_SECRET_KEY',
|
||||
'SUPABASE_JWT_KID',
|
||||
'SUPABASE_JWKS_URI'
|
||||
)
|
||||
|
||||
foreach ($opt in $optionalDefines) {
|
||||
if ($values.ContainsKey($opt) -and -not [string]::IsNullOrWhiteSpace($values[$opt])) {
|
||||
$defineArgs += "--dart-define=$opt=$($values[$opt])"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Starting Sojorn in development mode..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue