feat(R4): dismissible sync error banner in email list (#23)
This commit was merged in pull request #23.
This commit is contained in:
@@ -65,6 +65,10 @@ abstract class SyncLogRepository {
|
||||
});
|
||||
|
||||
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 {
|
||||
@@ -90,4 +94,7 @@ class NoOpSyncLogRepository implements SyncLogRepository {
|
||||
@override
|
||||
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
|
||||
Stream.value([]);
|
||||
|
||||
@override
|
||||
Stream<String?> observeLastError(String accountId) => Stream.value(null);
|
||||
}
|
||||
|
||||
@@ -99,4 +99,14 @@ class SyncLogRepositoryImpl implements SyncLogRepository {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,11 @@ final syncLogRepositoryProvider = Provider((ref) {
|
||||
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 runner = ReliabilityRunner(
|
||||
ref.watch(dbProvider),
|
||||
|
||||
@@ -35,6 +35,9 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
|
||||
bool _searchLoading = false;
|
||||
bool get _searching => _searchController.text.isNotEmpty;
|
||||
|
||||
// Error banner — tracks the last error message that the user dismissed.
|
||||
String? _dismissedError;
|
||||
|
||||
// Thread-level selection (key = threadId).
|
||||
final Set<String> _selectedThreadIds = {};
|
||||
// Last-emitted thread list, used to resolve emailIds for batch operations.
|
||||
@@ -131,9 +134,16 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
|
||||
currentMailboxPath: widget.mailboxPath,
|
||||
),
|
||||
bottomNavigationBar: _selecting ? _selectionBottomBar() : null,
|
||||
body: (_searchResults != null || _searchLoading)
|
||||
? _buildSearchBody()
|
||||
: _buildStreamBody(repo),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSyncErrorBanner(),
|
||||
Expanded(
|
||||
child: (_searchResults != null || _searchLoading)
|
||||
? _buildSearchBody()
|
||||
: _buildStreamBody(repo),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,6 +277,39 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
|
||||
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) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
|
||||
@@ -220,4 +220,7 @@ class _FakeLogs implements SyncLogRepository {
|
||||
@override
|
||||
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
|
||||
Stream.value([]);
|
||||
|
||||
@override
|
||||
Stream<String?> observeLastError(String accountId) => Stream.value(null);
|
||||
}
|
||||
|
||||
@@ -132,6 +132,9 @@ class FakeSyncLogRepository implements SyncLogRepository {
|
||||
@override
|
||||
Stream<List<SyncLogEntry>> observeSyncLogs(String accountId) =>
|
||||
Stream.value([]);
|
||||
|
||||
@override
|
||||
Stream<String?> observeLastError(String accountId) => Stream.value(null);
|
||||
}
|
||||
|
||||
class FakeMailboxRepositoryWithInbox implements MailboxRepository {
|
||||
|
||||
Reference in New Issue
Block a user