Compare commits

..
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 b116b5f9b5 refactor: add TypeConverters for Threads JSON columns (A4)
Add Drift TypeConverters for the two structured JSON columns in the
Threads table (participantsJson → List<EmailAddress>, emailIdsJson →
List<String>). The DB layer now owns serialisation for these fields;
_threadRowToModel no longer calls jsonDecode by hand.

Also extract _parseAddresses() helper to deduplicate the two identical
local functions that decoded Emails address columns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:53:30 +02:00
4 changed files with 72 additions and 111 deletions
-66
View File
@@ -40,69 +40,3 @@ jobs:
- name: Build Linux
run: nix develop --command task build-linux-release
build-macos:
name: Build macOS Debug
runs-on: macos-latest
needs: check
if: github.ref == 'refs/heads/main'
# Requires a macOS runner labelled 'macos-latest'.
# Jobs are skipped automatically when no matching runner is registered.
steps:
- uses: actions/checkout@v4
- name: Install FVM
run: dart pub global activate fvm
- name: Install Flutter via FVM
run: |
fvm install --skip-pub-get
fvm use --skip-pub-get
- name: Pub get
run: fvm flutter pub get --suppress-analytics
- name: Generate code
run: fvm flutter pub run build_runner build
- 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: Build macOS
run: fvm flutter build macos --debug --no-pub
build-windows:
name: Build Windows Debug
runs-on: windows-latest
needs: check
if: github.ref == 'refs/heads/main'
# Requires a Windows runner labelled 'windows-latest'.
# Jobs are skipped automatically when no matching runner is registered.
steps:
- uses: actions/checkout@v4
- name: Install FVM
run: dart pub global activate fvm
- name: Install Flutter via FVM
run: |
fvm install --skip-pub-get
fvm use --skip-pub-get
- name: Pub get
run: fvm flutter pub get --suppress-analytics
- name: Generate code
run: fvm flutter pub run build_runner build
- 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: Build Windows
run: fvm flutter build windows --debug --no-pub
+48 -5
View File
@@ -6,8 +6,40 @@ import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sharedinbox/core/models/email.dart';
part 'database.g.dart';
// ── TypeConverters ────────────────────────────────────────────────────────────
class EmailAddressListConverter
extends TypeConverter<List<EmailAddress>, String> {
const EmailAddressListConverter();
@override
List<EmailAddress> fromSql(String fromDb) {
final list = jsonDecode(fromDb) as List<dynamic>;
return list
.map((e) => EmailAddress.fromJson(e as Map<String, dynamic>))
.toList();
}
@override
String toSql(List<EmailAddress> value) =>
jsonEncode(value.map((e) => e.toJson()).toList());
}
class StringListConverter extends TypeConverter<List<String>, String> {
const StringListConverter();
@override
List<String> fromSql(String fromDb) =>
List<String>.from(jsonDecode(fromDb) as List);
@override
String toSql(List<String> value) => jsonEncode(value);
}
// ── Tables ────────────────────────────────────────────────────────────────────
class Accounts extends Table {
@@ -123,11 +155,14 @@ class Threads extends Table {
IntColumn get messageCount => integer().withDefault(const Constant(1))();
BoolColumn get hasUnread => boolean().withDefault(const Constant(false))();
BoolColumn get isFlagged => boolean().withDefault(const Constant(false))();
// JSON-encoded List<{name,email}>
TextColumn get participantsJson => text().withDefault(const Constant('[]'))();
TextColumn get participantsJson => text()
.withDefault(const Constant('[]'))
.map(const EmailAddressListConverter())();
TextColumn get preview => text().nullable()();
TextColumn get latestEmailId => text()();
TextColumn get emailIdsJson => text().withDefault(const Constant('[]'))();
TextColumn get emailIdsJson => text()
.withDefault(const Constant('[]'))
.map(const StringListConverter())();
@override
Set<Column> get primaryKey => {accountId, mailboxPath, id};
@@ -411,10 +446,18 @@ class AppDatabase extends _$AppDatabase {
preview: Value(latest.preview),
latestEmailId: latest.id,
emailIdsJson: Value(
jsonEncode(threadEmails.map((e) => e.id).toList()),
threadEmails.map((e) => e.id).toList(),
),
participantsJson: Value(
latest.fromJson,
(jsonDecode(latest.fromJson) as List<dynamic>)
.map(
(e) => EmailAddress(
name:
(e as Map<String, dynamic>)['name'] as String?,
email: e['email'] as String,
),
)
.toList(),
), // Good enough for migration
),
);
@@ -92,18 +92,6 @@ class EmailRepositoryImpl implements EmailRepository {
}
model.EmailThread _threadRowToModel(ThreadRow row) {
List<model.EmailAddress> parseAddresses(String json) {
final list = jsonDecode(json) as List<dynamic>;
return list
.map(
(e) => model.EmailAddress(
name: (e as Map<String, dynamic>)['name'] as String?,
email: e['email'] as String,
),
)
.toList();
}
return model.EmailThread(
threadId: row.id,
accountId: row.accountId,
@@ -113,10 +101,10 @@ class EmailRepositoryImpl implements EmailRepository {
messageCount: row.messageCount,
hasUnread: row.hasUnread,
isFlagged: row.isFlagged,
participants: parseAddresses(row.participantsJson),
participants: row.participantsJson,
preview: row.preview,
latestEmailId: row.latestEmailId,
emailIds: List<String>.from(jsonDecode(row.emailIdsJson) as List),
emailIds: row.emailIdsJson,
);
}
@@ -156,13 +144,11 @@ class EmailRepositoryImpl implements EmailRepository {
// Collect unique participants across the whole thread.
final seen = <String>{};
final participants = <Map<String, dynamic>>[];
final participants = <model.EmailAddress>[];
for (final e in threadEmails) {
final from = jsonDecode(e.fromJson) as List<dynamic>;
for (final a in from.cast<Map<String, dynamic>>()) {
final email = a['email'] as String;
if (seen.add(email)) {
participants.add({'name': a['name'], 'email': email});
for (final a in _parseAddresses(e.fromJson)) {
if (seen.add(a.email)) {
participants.add(a);
}
}
}
@@ -177,12 +163,10 @@ class EmailRepositoryImpl implements EmailRepository {
messageCount: Value(threadEmails.length),
hasUnread: Value(threadEmails.any((e) => !e.isSeen)),
isFlagged: Value(threadEmails.any((e) => e.isFlagged)),
participantsJson: Value(jsonEncode(participants)),
participantsJson: Value(participants),
preview: Value(latest.preview),
latestEmailId: latest.id,
emailIdsJson: Value(
jsonEncode(threadEmails.map((e) => e.id).toList()),
),
emailIdsJson: Value(threadEmails.map((e) => e.id).toList()),
),
);
}
@@ -2708,18 +2692,6 @@ class EmailRepositoryImpl implements EmailRepository {
}
model.Email _toModel(Email row) {
List<model.EmailAddress> parseAddresses(String json) {
final list = jsonDecode(json) as List<dynamic>;
return list
.map(
(e) => model.EmailAddress(
name: (e as Map<String, dynamic>)['name'] as String?,
email: e['email'] as String,
),
)
.toList();
}
return model.Email(
id: row.id,
accountId: row.accountId,
@@ -2728,9 +2700,9 @@ class EmailRepositoryImpl implements EmailRepository {
subject: row.subject,
sentAt: row.sentAt,
receivedAt: row.receivedAt,
from: parseAddresses(row.fromJson),
to: parseAddresses(row.toAddresses),
cc: parseAddresses(row.ccJson),
from: _parseAddresses(row.fromJson),
to: _parseAddresses(row.toAddresses),
cc: _parseAddresses(row.ccJson),
preview: row.preview,
isSeen: row.isSeen,
isFlagged: row.isFlagged,
@@ -2766,6 +2738,18 @@ class EmailRepositoryImpl implements EmailRepository {
}
}
List<model.EmailAddress> _parseAddresses(String json) {
final list = jsonDecode(json) as List<dynamic>;
return list
.map(
(e) => model.EmailAddress(
name: (e as Map<String, dynamic>)['name'] as String?,
email: e['email'] as String,
),
)
.toList();
}
List<model.EmailAttachment> _parseAttachments(String json) {
final list = jsonDecode(json) as List<dynamic>;
return list
+1 -1
View File
@@ -332,7 +332,7 @@ void main() {
messageCount: const Value(2),
hasUnread: const Value(true),
latestEmailId: 'acc-1:2',
emailIdsJson: const Value('["acc-1:1", "acc-1:2"]'),
emailIdsJson: const Value(['acc-1:1', 'acc-1:2']),
),
);