Offline Support
This commit is contained in:
@@ -1,14 +1,26 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
import '../brick/cache_helpers.dart';
|
||||
import '../models/profile.dart';
|
||||
import '../utils/snackbar.dart' show isOfflineSaveError;
|
||||
import 'auth_provider.dart';
|
||||
import 'connectivity_provider.dart';
|
||||
import 'supabase_provider.dart';
|
||||
import 'stream_recovery.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offline-pending state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Pending full-name updates made offline, keyed by userId.
|
||||
final offlinePendingProfileUpdatesProvider =
|
||||
StateProvider<Map<String, String>>((ref) => const {});
|
||||
|
||||
final currentUserIdProvider = Provider<String?>((ref) {
|
||||
final authState = ref.watch(authStateChangesProvider);
|
||||
// Be explicit about loading/error to avoid dynamic dispatch problems.
|
||||
@@ -37,9 +49,52 @@ final currentProfileProvider = StreamProvider<Profile?>((ref) {
|
||||
return data == null ? [] : [Profile.fromMap(data)];
|
||||
},
|
||||
fromMap: Profile.fromMap,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Profile>();
|
||||
return all.where((p) => p.id == userId).toList();
|
||||
},
|
||||
onCacheMirror: (rows) => mirrorBatchToBrick<Profile>(
|
||||
rows.whereType<Profile>().toList(),
|
||||
tag: 'profile_current',
|
||||
),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
|
||||
ref.listen<bool>(isOnlineProvider, (wasOnline, isNowOnline) async {
|
||||
if (wasOnline == true || !isNowOnline) return;
|
||||
final pending = Map<String, String>.from(
|
||||
ref.read(offlinePendingProfileUpdatesProvider),
|
||||
);
|
||||
if (pending.isEmpty) return;
|
||||
debugPrint(
|
||||
'[currentProfileProvider] reconnected — syncing ${pending.length} profile update(s)',
|
||||
);
|
||||
final synced = <String>[];
|
||||
for (final entry in pending.entries) {
|
||||
try {
|
||||
await client
|
||||
.from('profiles')
|
||||
.update({'full_name': entry.value})
|
||||
.eq('id', entry.key);
|
||||
synced.add(entry.key);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[currentProfileProvider] failed to sync full_name for ${entry.key}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (synced.isNotEmpty) {
|
||||
final remaining = Map<String, String>.from(
|
||||
ref.read(offlinePendingProfileUpdatesProvider),
|
||||
);
|
||||
for (final id in synced) {
|
||||
remaining.remove(id);
|
||||
}
|
||||
ref.read(offlinePendingProfileUpdatesProvider.notifier).state = remaining;
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper.stream.map((result) {
|
||||
return result.data.isEmpty ? null : result.data.first;
|
||||
});
|
||||
@@ -58,6 +113,13 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
|
||||
return data.map(Profile.fromMap).toList();
|
||||
},
|
||||
fromMap: Profile.fromMap,
|
||||
onOfflineData: () async {
|
||||
final all = await cachedListFromBrick<Profile>();
|
||||
all.sort((a, b) => a.fullName.compareTo(b.fullName));
|
||||
return all;
|
||||
},
|
||||
onCacheMirror: (rows) =>
|
||||
mirrorBatchToBrick<Profile>(rows, tag: 'profiles'),
|
||||
);
|
||||
|
||||
ref.onDispose(wrapper.dispose);
|
||||
@@ -67,23 +129,36 @@ final profilesProvider = StreamProvider<List<Profile>>((ref) {
|
||||
/// Controller for the current user's profile (update full name / password).
|
||||
final profileControllerProvider = Provider<ProfileController>((ref) {
|
||||
final client = ref.watch(supabaseClientProvider);
|
||||
return ProfileController(client);
|
||||
return ProfileController(client, ref);
|
||||
});
|
||||
|
||||
class ProfileController {
|
||||
ProfileController(this._client);
|
||||
ProfileController(this._client, [this._ref]);
|
||||
|
||||
final SupabaseClient _client;
|
||||
final Ref? _ref;
|
||||
|
||||
/// Update the `profiles.full_name` for the given user id.
|
||||
/// Offline: queues the update for replay on reconnect.
|
||||
Future<void> updateFullName({
|
||||
required String userId,
|
||||
required String fullName,
|
||||
}) async {
|
||||
await _client
|
||||
.from('profiles')
|
||||
.update({'full_name': fullName})
|
||||
.eq('id', userId);
|
||||
try {
|
||||
await _client
|
||||
.from('profiles')
|
||||
.update({'full_name': fullName})
|
||||
.eq('id', userId);
|
||||
} catch (e) {
|
||||
if (!isOfflineSaveError(e)) rethrow;
|
||||
final ref = _ref;
|
||||
if (ref == null) return;
|
||||
final current = Map<String, String>.from(
|
||||
ref.read(offlinePendingProfileUpdatesProvider),
|
||||
);
|
||||
current[userId] = fullName;
|
||||
ref.read(offlinePendingProfileUpdatesProvider.notifier).state = current;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the current user's password (works for OAuth users too).
|
||||
|
||||
Reference in New Issue
Block a user