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