// Version: 1.1.0 | Created: 2026-04-01 // Drift database — rooms and messages tables. // Uses driftDatabase() from drift_flutter for cross-platform support: // - Mobile/desktop: SQLite file via sqlite3_flutter_libs // - Web: OPFS (Origin Private File System) via IndexedDB fallback import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'database.g.dart'; // --------------------------------------------------------------------------- // Table definitions // --------------------------------------------------------------------------- /// Persisted room state. Upserted on every sync. class RoomsTable extends Table { @override String get tableName => 'rooms'; TextColumn get id => text()(); TextColumn get name => text()(); TextColumn get avatarUrl => text().nullable()(); TextColumn get lastMessage => text().nullable()(); IntColumn get lastActivityAt => integer().nullable()(); // milliseconds since epoch IntColumn get unreadCount => integer().withDefault(const Constant(0))(); BoolColumn get isDm => boolean().withDefault(const Constant(false))(); @override Set> get primaryKey => {id}; } /// Persisted message events. Upserted when received from sync. class MessagesTable extends Table { @override String get tableName => 'messages'; TextColumn get id => text()(); // eventId TextColumn get roomId => text()(); TextColumn get senderId => text()(); TextColumn get body => text().nullable()(); TextColumn get type => text()(); // MessageType name IntColumn get timestamp => integer()(); // milliseconds since epoch TextColumn get rawJson => text()(); // full event JSON for future use @override Set> get primaryKey => {id}; } // --------------------------------------------------------------------------- // Database class // --------------------------------------------------------------------------- @DriftDatabase(tables: [RoomsTable, MessagesTable]) class AppDatabase extends _$AppDatabase { AppDatabase() : super(driftDatabase(name: 'm8chat')); // Separate constructor for testing — accepts a custom executor. AppDatabase.forTesting(super.executor); @override int get schemaVersion => 1; } // --------------------------------------------------------------------------- // DAOs // --------------------------------------------------------------------------- /// Data access for rooms. extension RoomsDao on AppDatabase { /// Insert or replace a room row. Future upsertRoom(RoomsTableCompanion room) => into(roomsTable).insertOnConflictUpdate(room); /// Watch all rooms ordered by unread count desc then last activity desc. Stream> watchAllRooms() { return (select(roomsTable)..orderBy([ (t) => OrderingTerm(expression: t.unreadCount, mode: OrderingMode.desc), (t) => OrderingTerm( expression: t.lastActivityAt, mode: OrderingMode.desc, nulls: NullsOrder.last, ), ])) .watch(); } /// Get a single room by id, or null if not found. Future getRoomById(String id) => (select(roomsTable)..where((t) => t.id.equals(id))).getSingleOrNull(); } /// Data access for messages. extension MessagesDao on AppDatabase { /// Insert or replace a message row. Future upsertMessage(MessagesTableCompanion message) => into(messagesTable).insertOnConflictUpdate(message); /// Watch all messages for [roomId] ordered oldest-first. Stream> watchByRoom(String roomId) { return (select(messagesTable) ..where((t) => t.roomId.equals(roomId)) ..orderBy([ (t) => OrderingTerm(expression: t.timestamp, mode: OrderingMode.asc), ])) .watch(); } /// Load one page of messages for [roomId], most recent first. Future> getPage( String roomId, { int limit = 50, int offset = 0, }) { return (select(messagesTable) ..where((t) => t.roomId.equals(roomId)) ..orderBy([ (t) => OrderingTerm(expression: t.timestamp, mode: OrderingMode.desc), ]) ..limit(limit, offset: offset)) .get(); } } // --------------------------------------------------------------------------- // Riverpod provider // --------------------------------------------------------------------------- /// Provides the singleton [AppDatabase]. keepAlive: true — opened once for /// the app lifetime. @Riverpod(keepAlive: true) AppDatabase appDatabase(Ref ref) { final db = AppDatabase(); // Close the database when the provider is finally disposed (app shutdown). ref.onDispose(db.close); return db; }