Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb668b0ad7 |
@@ -0,0 +1,5 @@
|
|||||||
|
abstract interface class SearchHistoryRepository {
|
||||||
|
Future<List<String>> getRecentSearches();
|
||||||
|
Future<void> saveSearch(String query);
|
||||||
|
Future<void> clearHistory();
|
||||||
|
}
|
||||||
@@ -234,6 +234,13 @@ class Drafts extends Table {
|
|||||||
TextColumn get imapServerId => text().nullable()();
|
TextColumn get imapServerId => text().nullable()();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DataClassName('SearchHistoryRow')
|
||||||
|
class SearchHistoryEntries extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get query => text()();
|
||||||
|
DateTimeColumn get searchedAt => dateTime()();
|
||||||
|
}
|
||||||
|
|
||||||
@DataClassName('UndoActionRow')
|
@DataClassName('UndoActionRow')
|
||||||
class UndoActions extends Table {
|
class UndoActions extends Table {
|
||||||
TextColumn get id => text()();
|
TextColumn get id => text()();
|
||||||
@@ -263,13 +270,14 @@ class UndoActions extends Table {
|
|||||||
SyncLogMailboxes,
|
SyncLogMailboxes,
|
||||||
SyncHealth,
|
SyncHealth,
|
||||||
UndoActions,
|
UndoActions,
|
||||||
|
SearchHistoryEntries,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 26;
|
int get schemaVersion => 27;
|
||||||
|
|
||||||
Future<void> _createEmailFts() async {
|
Future<void> _createEmailFts() async {
|
||||||
await customStatement('''
|
await customStatement('''
|
||||||
@@ -492,6 +500,9 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
SELECT rowid, subject, preview, from_json FROM emails
|
SELECT rowid, subject, preview, from_json FROM emails
|
||||||
''');
|
''');
|
||||||
}
|
}
|
||||||
|
if (from < 27) {
|
||||||
|
await m.createTable(searchHistoryEntries);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:sharedinbox/core/repositories/search_history_repository.dart';
|
||||||
|
import 'package:sharedinbox/data/db/database.dart';
|
||||||
|
|
||||||
|
class SearchHistoryRepositoryImpl implements SearchHistoryRepository {
|
||||||
|
SearchHistoryRepositoryImpl(this._db);
|
||||||
|
final AppDatabase _db;
|
||||||
|
|
||||||
|
static const _maxEntries = 10;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getRecentSearches() async {
|
||||||
|
final rows = await (_db.select(_db.searchHistoryEntries)
|
||||||
|
..orderBy([(t) => OrderingTerm.desc(t.searchedAt)])
|
||||||
|
..limit(_maxEntries))
|
||||||
|
.get();
|
||||||
|
return rows.map((r) => r.query).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSearch(String query) async {
|
||||||
|
final trimmed = query.trim();
|
||||||
|
if (trimmed.isEmpty) return;
|
||||||
|
|
||||||
|
await _db.transaction(() async {
|
||||||
|
// Remove existing entry for same query (deduplication).
|
||||||
|
await (_db.delete(_db.searchHistoryEntries)
|
||||||
|
..where((t) => t.query.equals(trimmed)))
|
||||||
|
.go();
|
||||||
|
|
||||||
|
await _db.into(_db.searchHistoryEntries).insert(
|
||||||
|
SearchHistoryEntriesCompanion.insert(
|
||||||
|
query: trimmed,
|
||||||
|
searchedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prune to the most recent _maxEntries.
|
||||||
|
final keepIds = await (_db.select(_db.searchHistoryEntries)
|
||||||
|
..orderBy([(t) => OrderingTerm.desc(t.searchedAt)])
|
||||||
|
..limit(_maxEntries))
|
||||||
|
.map((r) => r.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (keepIds.isNotEmpty) {
|
||||||
|
await (_db.delete(_db.searchHistoryEntries)
|
||||||
|
..where((t) => t.id.isNotIn(keepIds)))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearHistory() async {
|
||||||
|
await _db.delete(_db.searchHistoryEntries).go();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import 'package:sharedinbox/core/repositories/account_repository.dart';
|
|||||||
import 'package:sharedinbox/core/repositories/draft_repository.dart';
|
import 'package:sharedinbox/core/repositories/draft_repository.dart';
|
||||||
import 'package:sharedinbox/core/repositories/email_repository.dart';
|
import 'package:sharedinbox/core/repositories/email_repository.dart';
|
||||||
import 'package:sharedinbox/core/repositories/mailbox_repository.dart';
|
import 'package:sharedinbox/core/repositories/mailbox_repository.dart';
|
||||||
|
import 'package:sharedinbox/core/repositories/search_history_repository.dart';
|
||||||
import 'package:sharedinbox/core/repositories/undo_repository.dart';
|
import 'package:sharedinbox/core/repositories/undo_repository.dart';
|
||||||
import 'package:sharedinbox/core/services/account_discovery_service.dart';
|
import 'package:sharedinbox/core/services/account_discovery_service.dart';
|
||||||
import 'package:sharedinbox/core/services/connection_test_service.dart';
|
import 'package:sharedinbox/core/services/connection_test_service.dart';
|
||||||
@@ -25,6 +26,7 @@ import 'package:sharedinbox/data/repositories/account_repository_impl.dart';
|
|||||||
import 'package:sharedinbox/data/repositories/draft_repository_impl.dart';
|
import 'package:sharedinbox/data/repositories/draft_repository_impl.dart';
|
||||||
import 'package:sharedinbox/data/repositories/email_repository_impl.dart';
|
import 'package:sharedinbox/data/repositories/email_repository_impl.dart';
|
||||||
import 'package:sharedinbox/data/repositories/mailbox_repository_impl.dart';
|
import 'package:sharedinbox/data/repositories/mailbox_repository_impl.dart';
|
||||||
|
import 'package:sharedinbox/data/repositories/search_history_repository_impl.dart';
|
||||||
import 'package:sharedinbox/data/repositories/sync_log_repository_impl.dart';
|
import 'package:sharedinbox/data/repositories/sync_log_repository_impl.dart';
|
||||||
import 'package:sharedinbox/data/repositories/undo_repository_impl.dart';
|
import 'package:sharedinbox/data/repositories/undo_repository_impl.dart';
|
||||||
import 'package:sharedinbox/data/storage/flutter_secure_storage_impl.dart';
|
import 'package:sharedinbox/data/storage/flutter_secure_storage_impl.dart';
|
||||||
@@ -87,6 +89,11 @@ final undoRepositoryProvider = Provider<UndoRepository>((ref) {
|
|||||||
return UndoRepositoryImpl(ref.watch(dbProvider));
|
return UndoRepositoryImpl(ref.watch(dbProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final searchHistoryRepositoryProvider =
|
||||||
|
Provider<SearchHistoryRepository>((ref) {
|
||||||
|
return SearchHistoryRepositoryImpl(ref.watch(dbProvider));
|
||||||
|
});
|
||||||
|
|
||||||
final syncLogRepositoryProvider = Provider((ref) {
|
final syncLogRepositoryProvider = Provider((ref) {
|
||||||
return SyncLogRepositoryImpl(ref.watch(dbProvider));
|
return SyncLogRepositoryImpl(ref.watch(dbProvider));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import 'package:sharedinbox/core/utils/logger.dart';
|
|||||||
import 'package:sharedinbox/di.dart';
|
import 'package:sharedinbox/di.dart';
|
||||||
import 'package:sharedinbox/ui/widgets/email_tile.dart';
|
import 'package:sharedinbox/ui/widgets/email_tile.dart';
|
||||||
|
|
||||||
|
final _searchHistoryProvider =
|
||||||
|
FutureProvider.autoDispose<List<String>>((ref) async {
|
||||||
|
return ref.watch(searchHistoryRepositoryProvider).getRecentSearches();
|
||||||
|
});
|
||||||
|
|
||||||
class SearchScreen extends ConsumerStatefulWidget {
|
class SearchScreen extends ConsumerStatefulWidget {
|
||||||
const SearchScreen({super.key, this.accountId});
|
const SearchScreen({super.key, this.accountId});
|
||||||
final String? accountId;
|
final String? accountId;
|
||||||
@@ -20,13 +25,24 @@ class SearchScreen extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _SearchScreenState extends ConsumerState<SearchScreen> {
|
class _SearchScreenState extends ConsumerState<SearchScreen> {
|
||||||
final _ctrl = TextEditingController();
|
final _ctrl = TextEditingController();
|
||||||
|
final _focusNode = FocusNode();
|
||||||
Timer? _debounce;
|
Timer? _debounce;
|
||||||
_SearchResults? _results;
|
_SearchResults? _results;
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
bool _fieldFocused = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode.addListener(() {
|
||||||
|
if (mounted) setState(() => _fieldFocused = _focusNode.hasFocus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_ctrl.dispose();
|
_ctrl.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
_debounce?.cancel();
|
_debounce?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -45,6 +61,12 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||||||
|
|
||||||
Future<void> _search(String query) async {
|
Future<void> _search(String query) async {
|
||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
|
unawaited(
|
||||||
|
ref
|
||||||
|
.read(searchHistoryRepositoryProvider)
|
||||||
|
.saveSearch(query)
|
||||||
|
.then((_) => ref.invalidate(_searchHistoryProvider)),
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
final emailRepo = ref.read(emailRepositoryProvider);
|
final emailRepo = ref.read(emailRepositoryProvider);
|
||||||
final mailboxRepo = ref.read(mailboxRepositoryProvider);
|
final mailboxRepo = ref.read(mailboxRepositoryProvider);
|
||||||
@@ -112,6 +134,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: TextField(
|
title: TextField(
|
||||||
controller: _ctrl,
|
controller: _ctrl,
|
||||||
|
focusNode: _focusNode,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Search folders, addresses, emails…',
|
hintText: 'Search folders, addresses, emails…',
|
||||||
@@ -137,6 +160,9 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (_loading) return const Center(child: CircularProgressIndicator());
|
if (_loading) return const Center(child: CircularProgressIndicator());
|
||||||
if (_results == null) {
|
if (_results == null) {
|
||||||
|
if (_fieldFocused && _ctrl.text.isEmpty) {
|
||||||
|
return _buildHistoryPanel();
|
||||||
|
}
|
||||||
return const Center(child: Text('Type 3+ characters to search'));
|
return const Center(child: Text('Type 3+ characters to search'));
|
||||||
}
|
}
|
||||||
final r = _results!;
|
final r = _results!;
|
||||||
@@ -169,6 +195,66 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildHistoryPanel() {
|
||||||
|
final history = ref.watch(_searchHistoryProvider);
|
||||||
|
return history.when(
|
||||||
|
loading: () => const Center(child: Text('Type 3+ characters to search')),
|
||||||
|
error: (_, __) =>
|
||||||
|
const Center(child: Text('Type 3+ characters to search')),
|
||||||
|
data: (terms) {
|
||||||
|
if (terms.isEmpty) {
|
||||||
|
return const Center(child: Text('Type 3+ characters to search'));
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Recent searches',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await ref
|
||||||
|
.read(searchHistoryRepositoryProvider)
|
||||||
|
.clearHistory();
|
||||||
|
ref.invalidate(_searchHistoryProvider);
|
||||||
|
},
|
||||||
|
child: const Text('Clear'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
for (final term in terms)
|
||||||
|
ActionChip(
|
||||||
|
label: Text(term),
|
||||||
|
onPressed: () {
|
||||||
|
_ctrl.text = term;
|
||||||
|
_ctrl.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: term.length),
|
||||||
|
);
|
||||||
|
unawaited(_search(term));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchResults {
|
class _SearchResults {
|
||||||
|
|||||||
+4
-8
@@ -43,10 +43,7 @@ Files: `lib/ui/screens/email_list_screen.dart`, `lib/core/utils/format_utils.dar
|
|||||||
|
|
||||||
### R4 — Done: https://codeberg.org/guettli/sharedinbox/pulls/23
|
### R4 — Done: https://codeberg.org/guettli/sharedinbox/pulls/23
|
||||||
|
|
||||||
### R5 🟡 Handle TLS certificate changes gracefully
|
### R5 — Done: https://codeberg.org/guettli/sharedinbox/pulls/45
|
||||||
`tls_error.dart` detects TLS errors but they bubble up as generic errors in the sync loop.
|
|
||||||
Detect `TlsError` specifically in `_AccountSync` and show a user-facing dialog offering to re-add the account or trust the new certificate.
|
|
||||||
Files: `lib/data/imap/tls_error.dart`, `lib/core/sync/account_sync_manager.dart`.
|
|
||||||
|
|
||||||
### R6 — Done: https://codeberg.org/guettli/sharedinbox/pulls/24
|
### R6 — Done: https://codeberg.org/guettli/sharedinbox/pulls/24
|
||||||
|
|
||||||
@@ -107,6 +104,8 @@ Files: `lib/ui/screens/email_list_screen.dart`, `lib/core/repositories/email_rep
|
|||||||
|
|
||||||
### T2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/31
|
### T2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/31
|
||||||
|
|
||||||
|
### T3 — Done: https://codeberg.org/guettli/sharedinbox/pulls/43
|
||||||
|
|
||||||
### T3 🟡 Contract tests for all Repository interfaces
|
### T3 🟡 Contract tests for all Repository interfaces
|
||||||
The interfaces in `core/repositories/` have no shared contract test suite. Concrete impls can silently diverge.
|
The interfaces in `core/repositories/` have no shared contract test suite. Concrete impls can silently diverge.
|
||||||
Add a shared `EmailRepositoryContract` abstract test class; run it against both `EmailRepositoryImpl` and any future mock/fake. Mirror this for `MailboxRepository` and `AccountRepository`.
|
Add a shared `EmailRepositoryContract` abstract test class; run it against both `EmailRepositoryImpl` and any future mock/fake. Mirror this for `MailboxRepository` and `AccountRepository`.
|
||||||
@@ -127,10 +126,7 @@ Files: `test/widget/email_list_screen_test.dart`.
|
|||||||
|
|
||||||
### A2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/33
|
### A2 — Done: https://codeberg.org/guettli/sharedinbox/pulls/33
|
||||||
|
|
||||||
### A3 🟡 Make AccountSyncManager testable without real IMAP connections
|
### A3 — Done: https://codeberg.org/guettli/sharedinbox/pulls/46
|
||||||
`AccountSyncManager` accepts `ImapConnectFn` as a dependency but `_JmapAccountSync` constructs its HTTP client internally.
|
|
||||||
Pass an injectable `http.Client` to `_JmapAccountSync` (already done in `EmailRepositoryImpl`; mirror the pattern here).
|
|
||||||
Files: `lib/core/sync/account_sync_manager.dart`, `test/unit/account_sync_manager_test.dart`.
|
|
||||||
|
|
||||||
### A4 🟡 Replace raw JSON strings in DB with structured encoding
|
### A4 🟡 Replace raw JSON strings in DB with structured encoding
|
||||||
`fromJson`, `toAddresses`, `ccJson`, `references` are stored as raw JSON strings parsed on every model conversion.
|
`fromJson`, `toAddresses`, `ccJson`, `references` are stored as raw JSON strings parsed on every model conversion.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const _noCode = {
|
|||||||
'lib/core/repositories/mailbox_repository.dart',
|
'lib/core/repositories/mailbox_repository.dart',
|
||||||
'lib/core/repositories/sync_log_repository.dart',
|
'lib/core/repositories/sync_log_repository.dart',
|
||||||
'lib/core/repositories/undo_repository.dart',
|
'lib/core/repositories/undo_repository.dart',
|
||||||
|
'lib/core/repositories/search_history_repository.dart',
|
||||||
'lib/core/models/undo_action.dart',
|
'lib/core/models/undo_action.dart',
|
||||||
'lib/core/storage/secure_storage.dart',
|
'lib/core/storage/secure_storage.dart',
|
||||||
};
|
};
|
||||||
@@ -61,6 +62,7 @@ const _excluded = {
|
|||||||
'lib/data/repositories/mailbox_repository_impl.dart',
|
'lib/data/repositories/mailbox_repository_impl.dart',
|
||||||
'lib/data/repositories/sync_log_repository_impl.dart',
|
'lib/data/repositories/sync_log_repository_impl.dart',
|
||||||
'lib/data/repositories/undo_repository_impl.dart',
|
'lib/data/repositories/undo_repository_impl.dart',
|
||||||
|
'lib/data/repositories/search_history_repository_impl.dart',
|
||||||
};
|
};
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -215,9 +215,9 @@ class MockEmailRepository extends _i1.Mock implements _i9.EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
_i4.Stream<List<_i2.Email>> observeEmails(
|
_i4.Stream<List<_i2.Email>> observeEmails(
|
||||||
String accountId,
|
String? accountId,
|
||||||
String mailboxPath, {
|
String? mailboxPath, {
|
||||||
int limit = 50,
|
int? limit = 50,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -233,9 +233,9 @@ class MockEmailRepository extends _i1.Mock implements _i9.EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
_i4.Stream<List<_i2.EmailThread>> observeThreads(
|
_i4.Stream<List<_i2.EmailThread>> observeThreads(
|
||||||
String accountId,
|
String? accountId,
|
||||||
String mailboxPath, {
|
String? mailboxPath, {
|
||||||
int limit = 50,
|
int? limit = 50,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ void main() {
|
|||||||
group('Migration', () {
|
group('Migration', () {
|
||||||
test('schemaVersion matches expected value', () async {
|
test('schemaVersion matches expected value', () async {
|
||||||
final db = AppDatabase(NativeDatabase.memory());
|
final db = AppDatabase(NativeDatabase.memory());
|
||||||
expect(db.schemaVersion, 26);
|
expect(db.schemaVersion, 27);
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,6 +171,11 @@ void main() {
|
|||||||
// Verify FTS table was created and is queryable.
|
// Verify FTS table was created and is queryable.
|
||||||
await db.customSelect('SELECT count(*) FROM email_fts').get();
|
await db.customSelect('SELECT count(*) FROM email_fts').get();
|
||||||
|
|
||||||
|
// v27: search_history_entries table.
|
||||||
|
await db
|
||||||
|
.customSelect('SELECT count(*) FROM search_history_entries')
|
||||||
|
.get();
|
||||||
|
|
||||||
await db.close();
|
await db.close();
|
||||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
if (dbFile.existsSync()) dbFile.deleteSync();
|
||||||
});
|
});
|
||||||
@@ -301,11 +306,16 @@ void main() {
|
|||||||
);
|
);
|
||||||
await db.customSelect('SELECT count(*) FROM email_fts').get();
|
await db.customSelect('SELECT count(*) FROM email_fts').get();
|
||||||
|
|
||||||
|
// v27: search_history_entries table.
|
||||||
|
await db
|
||||||
|
.customSelect('SELECT count(*) FROM search_history_entries')
|
||||||
|
.get();
|
||||||
|
|
||||||
await db.close();
|
await db.close();
|
||||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
if (dbFile.existsSync()) dbFile.deleteSync();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fresh install creates all tables at schemaVersion 26', () async {
|
test('fresh install creates all tables at schemaVersion 27', () async {
|
||||||
final db = AppDatabase(NativeDatabase.memory());
|
final db = AppDatabase(NativeDatabase.memory());
|
||||||
await db.select(db.accounts).get();
|
await db.select(db.accounts).get();
|
||||||
|
|
||||||
@@ -328,6 +338,7 @@ void main() {
|
|||||||
'threads',
|
'threads',
|
||||||
'sync_health',
|
'sync_health',
|
||||||
'undo_actions',
|
'undo_actions',
|
||||||
|
'search_history_entries',
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,9 @@ class MockEmailRepository extends _i1.Mock implements _i3.EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
_i4.Stream<List<_i2.Email>> observeEmails(
|
_i4.Stream<List<_i2.Email>> observeEmails(
|
||||||
String accountId,
|
String? accountId,
|
||||||
String mailboxPath, {
|
String? mailboxPath, {
|
||||||
int limit = 50,
|
int? limit = 50,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -93,9 +93,9 @@ class MockEmailRepository extends _i1.Mock implements _i3.EmailRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
_i4.Stream<List<_i2.EmailThread>> observeThreads(
|
_i4.Stream<List<_i2.EmailThread>> observeThreads(
|
||||||
String accountId,
|
String? accountId,
|
||||||
String mailboxPath, {
|
String? mailboxPath, {
|
||||||
int limit = 50,
|
int? limit = 50,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import 'package:sharedinbox/core/repositories/account_repository.dart';
|
|||||||
import 'package:sharedinbox/core/repositories/draft_repository.dart';
|
import 'package:sharedinbox/core/repositories/draft_repository.dart';
|
||||||
import 'package:sharedinbox/core/repositories/email_repository.dart';
|
import 'package:sharedinbox/core/repositories/email_repository.dart';
|
||||||
import 'package:sharedinbox/core/repositories/mailbox_repository.dart';
|
import 'package:sharedinbox/core/repositories/mailbox_repository.dart';
|
||||||
|
import 'package:sharedinbox/core/repositories/search_history_repository.dart';
|
||||||
import 'package:sharedinbox/core/services/account_discovery_service.dart';
|
import 'package:sharedinbox/core/services/account_discovery_service.dart';
|
||||||
import 'package:sharedinbox/core/services/connection_test_service.dart';
|
import 'package:sharedinbox/core/services/connection_test_service.dart';
|
||||||
import 'package:sharedinbox/core/services/managesieve_probe_service.dart';
|
import 'package:sharedinbox/core/services/managesieve_probe_service.dart';
|
||||||
@@ -508,3 +509,20 @@ Email testEmail({
|
|||||||
isFlagged: isFlagged,
|
isFlagged: isFlagged,
|
||||||
hasAttachment: hasAttachment,
|
hasAttachment: hasAttachment,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class FakeSearchHistoryRepository implements SearchHistoryRepository {
|
||||||
|
final List<String> _history = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getRecentSearches() async => List.unmodifiable(_history);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSearch(String query) async {
|
||||||
|
_history.remove(query);
|
||||||
|
_history.insert(0, query);
|
||||||
|
if (_history.length > 10) _history.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearHistory() async => _history.clear();
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ void main() {
|
|||||||
FakeMailboxRepository(),
|
FakeMailboxRepository(),
|
||||||
),
|
),
|
||||||
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -42,6 +45,9 @@ void main() {
|
|||||||
FakeMailboxRepository(),
|
FakeMailboxRepository(),
|
||||||
),
|
),
|
||||||
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -68,6 +74,9 @@ void main() {
|
|||||||
FakeMailboxRepository(),
|
FakeMailboxRepository(),
|
||||||
),
|
),
|
||||||
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -97,6 +106,9 @@ void main() {
|
|||||||
emailRepositoryProvider.overrideWithValue(
|
emailRepositoryProvider.overrideWithValue(
|
||||||
FakeEmailRepository(searchResults: [email]),
|
FakeEmailRepository(searchResults: [email]),
|
||||||
),
|
),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -132,6 +144,9 @@ void main() {
|
|||||||
FakeMailboxRepository([archiveMailbox]),
|
FakeMailboxRepository([archiveMailbox]),
|
||||||
),
|
),
|
||||||
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -162,6 +177,9 @@ void main() {
|
|||||||
emailRepositoryProvider.overrideWithValue(
|
emailRepositoryProvider.overrideWithValue(
|
||||||
FakeEmailRepository(searchResults: [email]),
|
FakeEmailRepository(searchResults: [email]),
|
||||||
),
|
),
|
||||||
|
searchHistoryRepositoryProvider.overrideWithValue(
|
||||||
|
FakeSearchHistoryRepository(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -175,8 +193,9 @@ void main() {
|
|||||||
await tester.tap(find.byIcon(Icons.clear));
|
await tester.tap(find.byIcon(Icons.clear));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Results are gone; the recent-search chip for the prior query appears.
|
||||||
expect(find.text('Found email'), findsNothing);
|
expect(find.text('Found email'), findsNothing);
|
||||||
expect(find.text('Type 3+ characters to search'), findsOneWidget);
|
expect(find.text('found'), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user