- LiveKit/MatrixRTC voice+video calls with full call screen UI - Incoming call overlay (accept/decline) - Media upload/download — file picker, image rendering, file download - Spaces navigation — space list + expandable child rooms - Drift persistence — rooms + messages written on every sync - Sync persistence auto-starts on login and session restore - Chat: typing indicators, long-press menu, reply, emoji reactions - User search dialog + start DM from rooms screen - Android: INTERNET + CAMERA + RECORD_AUDIO permissions in main manifest - Emoji picker for reactions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
143 lines
4.8 KiB
Dart
143 lines
4.8 KiB
Dart
// 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<Column<Object>> 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<Column<Object>> 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<void> upsertRoom(RoomsTableCompanion room) =>
|
|
into(roomsTable).insertOnConflictUpdate(room);
|
|
|
|
/// Watch all rooms ordered by unread count desc then last activity desc.
|
|
Stream<List<RoomsTableData>> 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<RoomsTableData?> 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<void> upsertMessage(MessagesTableCompanion message) =>
|
|
into(messagesTable).insertOnConflictUpdate(message);
|
|
|
|
/// Watch all messages for [roomId] ordered oldest-first.
|
|
Stream<List<MessagesTableData>> 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<List<MessagesTableData>> 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;
|
|
}
|