Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
089913c6fa | ||
|
|
f92f3debd7 | ||
|
|
692fa14d4d |
@@ -1,250 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
analyze-and-test:
|
||||
name: Analyze & unit test
|
||||
runs-on: sharedinbox-runner
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.41.6"
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate Drift code
|
||||
run: flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Check formatting
|
||||
run: dart format --set-exit-if-changed .
|
||||
|
||||
- name: Analyze
|
||||
run: flutter analyze --fatal-infos
|
||||
|
||||
- name: Unit + widget tests with coverage
|
||||
run: flutter test test/unit/ test/widget/ --coverage
|
||||
|
||||
- name: Coverage gate
|
||||
run: dart run scripts/check_coverage.dart
|
||||
|
||||
integration:
|
||||
name: Integration tests (Stalwart)
|
||||
runs-on: sharedinbox-runner
|
||||
# Run integration tests only on push to main, not on every PR.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v14
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Cache FVM Flutter SDK
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.fvm
|
||||
key: fvm-${{ hashFiles('.fvm/fvm_config.json') }}
|
||||
|
||||
- name: Cache pub packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pub-cache
|
||||
key: pub-${{ hashFiles('pubspec.lock') }}
|
||||
restore-keys: pub-
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
nix develop --command bash -c "
|
||||
fvm install --skip-pub-get &&
|
||||
fvm flutter pub get &&
|
||||
fvm flutter pub run build_runner build --delete-conflicting-outputs &&
|
||||
stalwart-dev/test.sh
|
||||
"
|
||||
|
||||
integration-ui:
|
||||
name: UI Integration tests (Stalwart + Xvfb)
|
||||
runs-on: sharedinbox-runner
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v14
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Install Flutter Linux build dependencies
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libgtk-3-dev pkg-config cmake ninja-build clang \
|
||||
libsecret-1-dev
|
||||
|
||||
- name: Cache FVM Flutter SDK
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.fvm
|
||||
key: fvm-${{ hashFiles('.fvm/fvm_config.json') }}
|
||||
|
||||
- name: Cache pub packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pub-cache
|
||||
key: pub-${{ hashFiles('pubspec.lock') }}
|
||||
restore-keys: pub-
|
||||
|
||||
- name: Cache Linux debug build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
build/linux
|
||||
.dart_tool/flutter_build
|
||||
key: linux-debug-${{ hashFiles('pubspec.lock', 'lib/**/*.dart', 'integration_test/**/*.dart') }}
|
||||
restore-keys: linux-debug-
|
||||
|
||||
- name: Run UI integration tests
|
||||
run: |
|
||||
nix develop --command bash -c "
|
||||
fvm install --skip-pub-get &&
|
||||
fvm flutter pub get &&
|
||||
fvm flutter pub run build_runner build --delete-conflicting-outputs &&
|
||||
stalwart-dev/integration_ui_test.sh
|
||||
"
|
||||
|
||||
build-linux:
|
||||
name: Build Linux desktop
|
||||
runs-on: sharedinbox-runner
|
||||
needs: analyze-and-test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install GTK3, build tools and libsecret
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libgtk-3-dev pkg-config cmake ninja-build clang \
|
||||
libsecret-1-dev
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.41.6"
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate Drift code
|
||||
run: flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Build Linux release
|
||||
run: flutter build linux --release
|
||||
|
||||
deploy:
|
||||
name: Deploy Linux build & publish website
|
||||
runs-on: sharedinbox-runner
|
||||
needs: build-linux
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install build & deploy dependencies
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libgtk-3-dev pkg-config cmake ninja-build clang \
|
||||
libsecret-1-dev hugo rsync
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: "3.41.6"
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Cache pub packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pub-cache
|
||||
key: pub-${{ hashFiles('pubspec.lock') }}
|
||||
restore-keys: pub-
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate Drift code
|
||||
run: flutter pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Generate changelog
|
||||
run: |
|
||||
mkdir -p assets
|
||||
git log -n 50 \
|
||||
--pretty=format:'* %ad [%h](https://codeberg.org/guettli/sharedinbox/commit/%H): %s' \
|
||||
--date=short > assets/changelog.txt
|
||||
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
printf '%s\n' "${{ secrets.SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
|
||||
chmod 644 ~/.ssh/known_hosts
|
||||
|
||||
- name: Build Linux release
|
||||
run: |
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
flutter build linux --release --no-pub --dart-define=GIT_HASH=$HASH
|
||||
|
||||
- name: Deploy Linux build to server
|
||||
run: |
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
DATE_PATH=$(date -u +%Y/%m/%d)
|
||||
REMOTE_DIR="public_html/builds/$DATE_PATH"
|
||||
TARBALL="sharedinbox-linux-amd64-$HASH.tar.gz"
|
||||
tar -czf /tmp/$TARBALL -C build/linux/x64/release bundle
|
||||
ssh "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_DIR"
|
||||
scp /tmp/$TARBALL "$SSH_USER@$SSH_HOST:$REMOTE_DIR/$TARBALL"
|
||||
DOWNLOAD_URL="https://sharedinbox.de/builds/$DATE_PATH/$TARBALL"
|
||||
EXISTING=$(ssh "$SSH_USER@$SSH_HOST" \
|
||||
"cat public_html/latest.json 2>/dev/null || echo '{}'")
|
||||
WINDOWS_URL=$(echo "$EXISTING" | \
|
||||
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('windows',''))" \
|
||||
2>/dev/null || true)
|
||||
if [ -n "$WINDOWS_URL" ]; then
|
||||
echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\",\"windows\":\"$WINDOWS_URL\"}" | \
|
||||
ssh "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
|
||||
else
|
||||
echo "{\"version\":\"$HASH\",\"linux\":\"$DOWNLOAD_URL\"}" | \
|
||||
ssh "$SSH_USER@$SSH_HOST" "cat > public_html/latest.json"
|
||||
fi
|
||||
|
||||
- name: Generate build history pages
|
||||
run: python3 scripts/generate_build_history.py
|
||||
|
||||
- name: Build website
|
||||
env:
|
||||
HUGO_PARAMS_GITVERSION: ${{ github.sha }}
|
||||
run: hugo --source website --minify
|
||||
|
||||
- name: Deploy website
|
||||
run: |
|
||||
rsync -avz --delete \
|
||||
--exclude='*.apk' \
|
||||
--exclude='*.tar.gz' \
|
||||
website/public/ \
|
||||
"$SSH_USER@$SSH_HOST:public_html/"
|
||||
@@ -1 +1 @@
|
||||
const int dbSchemaVersion = 36;
|
||||
const int dbSchemaVersion = 37;
|
||||
|
||||
@@ -5,4 +5,8 @@ abstract class UserPreferencesRepository {
|
||||
Future<void> updateMenuPosition(MenuPosition position);
|
||||
Future<void> updateMailViewButtonPosition(MenuPosition position);
|
||||
Future<void> updateAfterMailViewAction(AfterMailViewAction action);
|
||||
|
||||
Stream<List<String>> observeTrustedImageSenders();
|
||||
Future<void> addTrustedImageSender(String senderEmail);
|
||||
Future<void> removeTrustedImageSender(String senderEmail);
|
||||
}
|
||||
|
||||
@@ -307,6 +307,17 @@ class LocalSieveApplied extends Table {
|
||||
Set<Column> get primaryKey => {accountId, messageId};
|
||||
}
|
||||
|
||||
/// Senders for whom remote images are loaded automatically.
|
||||
/// Per-device/per-user — not tied to any email account.
|
||||
@DataClassName('ImageTrustedSenderRow')
|
||||
class ImageTrustedSenders extends Table {
|
||||
TextColumn get senderEmail => text()();
|
||||
DateTimeColumn get addedAt => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {senderEmail};
|
||||
}
|
||||
|
||||
/// App-wide user preferences, stored as a singleton row (id always 1).
|
||||
@DataClassName('UserPreferencesRow')
|
||||
class UserPreferences extends Table {
|
||||
@@ -345,6 +356,7 @@ class UserPreferences extends Table {
|
||||
LocalSieveApplied,
|
||||
ShareKeys,
|
||||
UserPreferences,
|
||||
ImageTrustedSenders,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
@@ -611,6 +623,9 @@ class AppDatabase extends _$AppDatabase {
|
||||
userPreferences.afterMailViewAction,
|
||||
);
|
||||
}
|
||||
if (from < 37) {
|
||||
await m.createTable(imageTrustedSenders);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,31 @@ class UserPreferencesRepositoryImpl implements UserPreferencesRepository {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<String>> observeTrustedImageSenders() {
|
||||
return (_db.select(_db.imageTrustedSenders)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.addedAt)]))
|
||||
.watch()
|
||||
.map((rows) => rows.map((r) => r.senderEmail).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addTrustedImageSender(String senderEmail) async {
|
||||
await _db.into(_db.imageTrustedSenders).insertOnConflictUpdate(
|
||||
ImageTrustedSendersCompanion(
|
||||
senderEmail: Value(senderEmail.toLowerCase()),
|
||||
addedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrustedImageSender(String senderEmail) async {
|
||||
await (_db.delete(_db.imageTrustedSenders)
|
||||
..where((t) => t.senderEmail.equals(senderEmail.toLowerCase())))
|
||||
.go();
|
||||
}
|
||||
|
||||
static pref.UserPreferences _rowToModel(UserPreferencesRow? row) {
|
||||
if (row == null) return const pref.UserPreferences();
|
||||
return pref.UserPreferences(
|
||||
|
||||
+31
@@ -211,8 +211,32 @@ class EmailDetailNotifier extends AsyncNotifier<(Email?, EmailBody)> {
|
||||
repo.getEmailBody(_emailId),
|
||||
]);
|
||||
unawaited(repo.setFlag(_emailId, seen: true));
|
||||
final header = results[0] as Email?;
|
||||
if (header != null) {
|
||||
unawaited(_prefetchNextEmailBody(repo, header));
|
||||
}
|
||||
return (results[0] as Email?, results[1] as EmailBody);
|
||||
}
|
||||
|
||||
Future<void> _prefetchNextEmailBody(
|
||||
EmailRepository repo,
|
||||
Email header,
|
||||
) async {
|
||||
final prefs = ref.read(userPreferencesProvider).value;
|
||||
final action =
|
||||
prefs?.afterMailViewAction ?? AfterMailViewAction.nextMessage;
|
||||
if (action != AfterMailViewAction.nextMessage) return;
|
||||
|
||||
final threads =
|
||||
await repo.observeThreads(header.accountId, header.mailboxPath).first;
|
||||
final currentIndex = threads.indexWhere(
|
||||
(t) => t.emailIds.contains(_emailId),
|
||||
);
|
||||
if (currentIndex < 0 || currentIndex + 1 >= threads.length) return;
|
||||
|
||||
final nextId = threads[currentIndex + 1].latestEmailId;
|
||||
await repo.getEmailBody(nextId);
|
||||
}
|
||||
}
|
||||
|
||||
final accountByIdProvider =
|
||||
@@ -247,3 +271,10 @@ final userPreferencesProvider = StreamProvider.autoDispose<UserPreferences>((
|
||||
) {
|
||||
return ref.watch(userPreferencesRepositoryProvider).observePreferences();
|
||||
});
|
||||
|
||||
final trustedImageSendersProvider =
|
||||
StreamProvider.autoDispose<List<String>>((ref) {
|
||||
return ref
|
||||
.watch(userPreferencesRepositoryProvider)
|
||||
.observeTrustedImageSenders();
|
||||
});
|
||||
|
||||
@@ -171,19 +171,35 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
||||
body: detail.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(child: Text('Error: $e')),
|
||||
data: (d) => _buildBody(context, d.$1, d.$2),
|
||||
data: (d) {
|
||||
final trusted =
|
||||
ref.watch(trustedImageSendersProvider).value ?? const <String>[];
|
||||
return _buildBody(context, d.$1, d.$2, trusted);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext ctx, Email? header, EmailBody body) {
|
||||
Widget _buildBody(
|
||||
BuildContext ctx,
|
||||
Email? header,
|
||||
EmailBody body,
|
||||
List<String> trustedSenders,
|
||||
) {
|
||||
final hasHtml = (body.htmlBody ?? '').trim().isNotEmpty;
|
||||
final senderEmail = header?.from.isNotEmpty == true
|
||||
? header!.from.first.email.toLowerCase()
|
||||
: null;
|
||||
final isTrusted =
|
||||
senderEmail != null && trustedSenders.contains(senderEmail);
|
||||
final effectiveLoadImages = _loadRemoteImages || isTrusted;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (header != null) ...[_buildHeader(ctx, header), const Divider()],
|
||||
if (hasHtml) ...[
|
||||
if (!_loadRemoteImages)
|
||||
if (!effectiveLoadImages)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
@@ -191,13 +207,40 @@ class _EmailDetailScreenState extends ConsumerState<EmailDetailScreen> {
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.image_outlined, size: 18),
|
||||
label: const Text('Load remote images'),
|
||||
onPressed: () => setState(() => _loadRemoteImages = true),
|
||||
onPressed: () {
|
||||
setState(() => _loadRemoteImages = true);
|
||||
if (senderEmail != null) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(userPreferencesRepositoryProvider)
|
||||
.addTrustedImageSender(senderEmail),
|
||||
);
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 3),
|
||||
content: const Text(
|
||||
'Images will be loaded automatically for this sender.',
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'Settings',
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
context.push('/accounts/preferences'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SecureEmailWebView(
|
||||
htmlBody: body.htmlBody!,
|
||||
loadRemoteImages: _loadRemoteImages,
|
||||
loadRemoteImages: effectiveLoadImages,
|
||||
),
|
||||
] else
|
||||
SelectableText(
|
||||
|
||||
@@ -113,6 +113,14 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trustedSenders =
|
||||
ref.watch(trustedImageSendersProvider).value ?? const <String>[];
|
||||
final senderEmail = widget.email.from.isNotEmpty
|
||||
? widget.email.from.first.email.toLowerCase()
|
||||
: null;
|
||||
final isTrusted =
|
||||
senderEmail != null && trustedSenders.contains(senderEmail);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
@@ -147,13 +155,13 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_expanded) _buildExpandedBody(),
|
||||
if (_expanded) _buildExpandedBody(isTrusted, senderEmail),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExpandedBody() {
|
||||
Widget _buildExpandedBody(bool isTrusted, String? senderEmail) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
@@ -184,21 +192,48 @@ class _EmailMessageCardState extends ConsumerState<_EmailMessageCard> {
|
||||
}
|
||||
final body = snapshot.data!;
|
||||
final hasHtml = (body.htmlBody ?? '').trim().isNotEmpty;
|
||||
final effectiveLoadImages = _loadRemoteImages || isTrusted;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasHtml) ...[
|
||||
if (!_loadRemoteImages)
|
||||
if (!effectiveLoadImages)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.image_outlined, size: 16),
|
||||
label: const Text('Load remote images'),
|
||||
onPressed: () =>
|
||||
setState(() => _loadRemoteImages = true),
|
||||
onPressed: () {
|
||||
setState(() => _loadRemoteImages = true);
|
||||
if (senderEmail != null) {
|
||||
unawaited(
|
||||
ref
|
||||
.read(userPreferencesRepositoryProvider)
|
||||
.addTrustedImageSender(senderEmail),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 3),
|
||||
content: const Text(
|
||||
'Images will be loaded automatically for this sender.',
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'Settings',
|
||||
onPressed: () {
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
context.push('/accounts/preferences'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
SecureEmailWebView(
|
||||
htmlBody: body.htmlBody!,
|
||||
loadRemoteImages: _loadRemoteImages,
|
||||
loadRemoteImages: effectiveLoadImages,
|
||||
),
|
||||
] else
|
||||
SelectableText(
|
||||
|
||||
@@ -12,6 +12,7 @@ class UserPreferencesScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final prefsAsync = ref.watch(userPreferencesProvider);
|
||||
final trustedSendersAsync = ref.watch(trustedImageSendersProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Preferences')),
|
||||
@@ -131,6 +132,45 @@ class UserPreferencesScreen extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Trusted image senders',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Remote images are loaded automatically for these senders.',
|
||||
),
|
||||
),
|
||||
...trustedSendersAsync.when(
|
||||
loading: () => const [],
|
||||
error: (_, __) => const [],
|
||||
data: (senders) => senders.isEmpty
|
||||
? [
|
||||
const Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text('No trusted senders yet.'),
|
||||
),
|
||||
]
|
||||
: [
|
||||
for (final sender in senders)
|
||||
ListTile(
|
||||
title: Text(sender),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Remove',
|
||||
onPressed: () {
|
||||
unawaited(
|
||||
ref
|
||||
.read(userPreferencesRepositoryProvider)
|
||||
.removeTrustedImageSender(sender),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ void main() {
|
||||
group('Migration', () {
|
||||
test('schemaVersion matches expected value', () async {
|
||||
final db = AppDatabase(NativeDatabase.memory());
|
||||
expect(db.schemaVersion, 36);
|
||||
expect(db.schemaVersion, 37);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
@@ -209,6 +209,9 @@ void main() {
|
||||
// v36: after_mail_view_action column on user_preferences.
|
||||
expect(userPrefsColumns, contains('after_mail_view_action'));
|
||||
|
||||
// v37: image_trusted_senders table.
|
||||
await db.customSelect('SELECT count(*) FROM image_trusted_senders').get();
|
||||
|
||||
await db.close();
|
||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
||||
});
|
||||
@@ -412,12 +415,17 @@ void main() {
|
||||
// v36: after_mail_view_action column on user_preferences.
|
||||
expect(userPrefsColumns, contains('after_mail_view_action'));
|
||||
|
||||
// v37: image_trusted_senders table.
|
||||
await db
|
||||
.customSelect('SELECT count(*) FROM image_trusted_senders')
|
||||
.get();
|
||||
|
||||
await db.close();
|
||||
if (dbFile.existsSync()) dbFile.deleteSync();
|
||||
},
|
||||
);
|
||||
|
||||
test('fresh install creates all tables at schemaVersion 36', () async {
|
||||
test('fresh install creates all tables at schemaVersion 37', () async {
|
||||
final db = AppDatabase(NativeDatabase.memory());
|
||||
await db.select(db.accounts).get();
|
||||
|
||||
@@ -445,6 +453,7 @@ void main() {
|
||||
'share_keys', // v31
|
||||
'local_sieve_applied', // v32
|
||||
'user_preferences', // v34
|
||||
'image_trusted_senders', // v37
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -473,6 +482,9 @@ void main() {
|
||||
// v36: after_mail_view_action column on user_preferences.
|
||||
expect(userPrefsColumns, contains('after_mail_view_action'));
|
||||
|
||||
// v37: image_trusted_senders table.
|
||||
await db.customSelect('SELECT count(*) FROM image_trusted_senders').get();
|
||||
|
||||
await db.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -627,11 +627,13 @@ class FakeUserPreferencesRepository implements UserPreferencesRepository {
|
||||
this.menuPosition = MenuPosition.bottom,
|
||||
this.mailViewButtonPosition = MenuPosition.bottom,
|
||||
this.afterMailViewAction = AfterMailViewAction.nextMessage,
|
||||
});
|
||||
List<String>? trustedImageSenders,
|
||||
}) : _trustedImageSenders = trustedImageSenders ?? [];
|
||||
|
||||
MenuPosition menuPosition;
|
||||
MenuPosition mailViewButtonPosition;
|
||||
AfterMailViewAction afterMailViewAction;
|
||||
final List<String> _trustedImageSenders;
|
||||
|
||||
@override
|
||||
Stream<UserPreferences> observePreferences() => Stream.value(
|
||||
@@ -656,6 +658,23 @@ class FakeUserPreferencesRepository implements UserPreferencesRepository {
|
||||
Future<void> updateAfterMailViewAction(AfterMailViewAction action) async {
|
||||
afterMailViewAction = action;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<String>> observeTrustedImageSenders() =>
|
||||
Stream.value(List.of(_trustedImageSenders));
|
||||
|
||||
@override
|
||||
Future<void> addTrustedImageSender(String senderEmail) async {
|
||||
final normalized = senderEmail.toLowerCase();
|
||||
if (!_trustedImageSenders.contains(normalized)) {
|
||||
_trustedImageSenders.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeTrustedImageSender(String senderEmail) async {
|
||||
_trustedImageSenders.remove(senderEmail.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
class FakeSearchHistoryRepository implements SearchHistoryRepository {
|
||||
|
||||
Reference in New Issue
Block a user