Compare commits

..
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 f0c1b0c2c0 feat(R2): add Force full sync button to account edit screen
Adds an escape hatch for when local DB falls out of sync with server:
- New clearForResync() on EmailRepository/MailboxRepository truncates
  local emails, mailboxes, pending changes and sync-state checkpoints.
  EmailBodies are preserved (FK disabled during delete) so viewed content
  is not re-downloaded after re-sync.
- AccountSyncManager.forceResync() orchestrates clear + loop restart.
- Edit account screen gets an "Force full sync" OutlinedButton with a
  confirmation dialog; shows spinner while the operation runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 22:53:41 +02:00
6 changed files with 3 additions and 74 deletions
@@ -65,10 +65,6 @@ abstract class SyncLogRepository {
}); });
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId); Stream<List<SyncLogEntry>> observeSyncLogs(String accountId);
/// Emits the error message of the most recent sync attempt for [accountId],
/// or null when the last sync succeeded (or no syncs have run yet).
Stream<String?> observeLastError(String accountId);
} }
class NoOpSyncLogRepository implements SyncLogRepository { class NoOpSyncLogRepository implements SyncLogRepository {
@@ -94,7 +90,4 @@ class NoOpSyncLogRepository implements SyncLogRepository {
@override @override
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) => Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
Stream.value([]); Stream.value([]);
@override
Stream<String?> observeLastError(String accountId) => Stream.value(null);
} }
@@ -99,14 +99,4 @@ class SyncLogRepositoryImpl implements SyncLogRepository {
return entries; return entries;
}); });
} }
@override
Stream<String?> observeLastError(String accountId) {
return (_db.select(_db.syncLogs)
..where((t) => t.accountId.equals(accountId))
..orderBy([(t) => OrderingTerm.desc(t.startedAt)])
..limit(1))
.watchSingleOrNull()
.map((row) => (row?.result == 'error') ? row?.errorMessage : null);
}
} }
-5
View File
@@ -85,11 +85,6 @@ final syncLogRepositoryProvider = Provider((ref) {
return SyncLogRepositoryImpl(ref.watch(dbProvider)); return SyncLogRepositoryImpl(ref.watch(dbProvider));
}); });
final syncLastErrorProvider =
StreamProvider.autoDispose.family<String?, String>((ref, accountId) {
return ref.watch(syncLogRepositoryProvider).observeLastError(accountId);
});
final reliabilityRunnerProvider = Provider<ReliabilityRunner>((ref) { final reliabilityRunnerProvider = Provider<ReliabilityRunner>((ref) {
final runner = ReliabilityRunner( final runner = ReliabilityRunner(
ref.watch(dbProvider), ref.watch(dbProvider),
+3 -46
View File
@@ -35,9 +35,6 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
bool _searchLoading = false; bool _searchLoading = false;
bool get _searching => _searchController.text.isNotEmpty; bool get _searching => _searchController.text.isNotEmpty;
// Error banner — tracks the last error message that the user dismissed.
String? _dismissedError;
// Thread-level selection (key = threadId). // Thread-level selection (key = threadId).
final Set<String> _selectedThreadIds = {}; final Set<String> _selectedThreadIds = {};
// Last-emitted thread list, used to resolve emailIds for batch operations. // Last-emitted thread list, used to resolve emailIds for batch operations.
@@ -134,16 +131,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
currentMailboxPath: widget.mailboxPath, currentMailboxPath: widget.mailboxPath,
), ),
bottomNavigationBar: _selecting ? _selectionBottomBar() : null, bottomNavigationBar: _selecting ? _selectionBottomBar() : null,
body: Column( body: (_searchResults != null || _searchLoading)
children: [ ? _buildSearchBody()
_buildSyncErrorBanner(), : _buildStreamBody(repo),
Expanded(
child: (_searchResults != null || _searchLoading)
? _buildSearchBody()
: _buildStreamBody(repo),
),
],
),
); );
} }
@@ -277,39 +267,6 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
return _buildEmailList(_searchResults!); return _buildEmailList(_searchResults!);
} }
Widget _buildSyncErrorBanner() {
final errorAsync = ref.watch(syncLastErrorProvider(widget.accountId));
final error = errorAsync.valueOrNull;
if (error == null || error == _dismissedError) {
return const SizedBox.shrink();
}
return MaterialBanner(
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
content: Text(
error,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
leading: Icon(
Icons.sync_problem,
color: Theme.of(context).colorScheme.error,
),
backgroundColor: Theme.of(context).colorScheme.errorContainer,
actions: [
TextButton(
onPressed: () {
ref.read(syncManagerProvider).syncNow(widget.accountId);
},
child: const Text('Retry'),
),
TextButton(
onPressed: () => setState(() => _dismissedError = error),
child: const Text('Dismiss'),
),
],
);
}
Widget _buildStreamBody(EmailRepository emailRepo) { Widget _buildStreamBody(EmailRepository emailRepo) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@@ -220,7 +220,4 @@ class _FakeLogs implements SyncLogRepository {
@override @override
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) => Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
Stream.value([]); Stream.value([]);
@override
Stream<String?> observeLastError(String accountId) => Stream.value(null);
} }
-3
View File
@@ -132,9 +132,6 @@ class FakeSyncLogRepository implements SyncLogRepository {
@override @override
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) => Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
Stream.value([]); Stream.value([]);
@override
Stream<String?> observeLastError(String accountId) => Stream.value(null);
} }
class FakeMailboxRepositoryWithInbox implements MailboxRepository { class FakeMailboxRepositoryWithInbox implements MailboxRepository {