Basic messages & loading

This commit is contained in:
2025-05-03 13:15:41 +08:00
parent 63ec82891f
commit b2c31bcf13
19 changed files with 2484 additions and 6 deletions

View File

@ -0,0 +1,81 @@
import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:island/database/message.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'drift_db.g.dart';
// Define the database
@DriftDatabase(tables: [ChatMessages])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
// Methods for chat messages
Future<List<ChatMessage>> getMessagesForRoom(
int roomId, {
int offset = 0,
int limit = 20,
}) {
return (select(chatMessages)
..where((m) => m.roomId.equals(roomId))
..orderBy([(m) => OrderingTerm.desc(m.createdAt)])
..limit(limit, offset: offset))
.get();
}
Future<int> saveMessage(ChatMessagesCompanion message) {
return into(chatMessages).insert(message, mode: InsertMode.insertOrReplace);
}
Future<int> updateMessageStatus(String id, MessageStatus status) {
return (update(chatMessages)..where(
(m) => m.id.equals(id),
)).write(ChatMessagesCompanion(status: Value(status)));
}
Future<int> deleteMessage(String id) {
return (delete(chatMessages)..where((m) => m.id.equals(id))).go();
}
// Convert between Drift and model objects
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
return ChatMessagesCompanion(
id: Value(message.id),
roomId: Value(message.roomId),
senderId: Value(message.senderId),
content: Value(message.toRemoteMessage().content),
nonce: Value(message.nonce),
data: Value(jsonEncode(message.data)),
createdAt: Value(message.createdAt),
status: Value(message.status),
);
}
LocalChatMessage companionToMessage(ChatMessage dbMessage) {
final data = jsonDecode(dbMessage.data);
return LocalChatMessage(
id: dbMessage.id,
roomId: dbMessage.roomId,
senderId: dbMessage.senderId,
data: data,
createdAt: dbMessage.createdAt,
status: dbMessage.status,
nonce: dbMessage.nonce,
);
}
}
// Helper to open the database connection
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'island_chat.sqlite'));
return NativeDatabase(file);
});
}

View File

@ -0,0 +1,807 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'drift_db.dart';
// ignore_for_file: type=lint
class $ChatMessagesTable extends ChatMessages
with TableInfo<$ChatMessagesTable, ChatMessage> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$ChatMessagesTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _roomIdMeta = const VerificationMeta('roomId');
@override
late final GeneratedColumn<int> roomId = GeneratedColumn<int>(
'room_id',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
);
static const VerificationMeta _senderIdMeta = const VerificationMeta(
'senderId',
);
@override
late final GeneratedColumn<String> senderId = GeneratedColumn<String>(
'sender_id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _contentMeta = const VerificationMeta(
'content',
);
@override
late final GeneratedColumn<String> content = GeneratedColumn<String>(
'content',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _nonceMeta = const VerificationMeta('nonce');
@override
late final GeneratedColumn<String> nonce = GeneratedColumn<String>(
'nonce',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override
late final GeneratedColumn<String> data = GeneratedColumn<String>(
'data',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _createdAtMeta = const VerificationMeta(
'createdAt',
);
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
);
@override
late final GeneratedColumnWithTypeConverter<MessageStatus, int> status =
GeneratedColumn<int>(
'status',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
).withConverter<MessageStatus>($ChatMessagesTable.$converterstatus);
@override
List<GeneratedColumn> get $columns => [
id,
roomId,
senderId,
content,
nonce,
data,
createdAt,
status,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'chat_messages';
@override
VerificationContext validateIntegrity(
Insertable<ChatMessage> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('room_id')) {
context.handle(
_roomIdMeta,
roomId.isAcceptableOrUnknown(data['room_id']!, _roomIdMeta),
);
} else if (isInserting) {
context.missing(_roomIdMeta);
}
if (data.containsKey('sender_id')) {
context.handle(
_senderIdMeta,
senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta),
);
} else if (isInserting) {
context.missing(_senderIdMeta);
}
if (data.containsKey('content')) {
context.handle(
_contentMeta,
content.isAcceptableOrUnknown(data['content']!, _contentMeta),
);
}
if (data.containsKey('nonce')) {
context.handle(
_nonceMeta,
nonce.isAcceptableOrUnknown(data['nonce']!, _nonceMeta),
);
}
if (data.containsKey('data')) {
context.handle(
_dataMeta,
this.data.isAcceptableOrUnknown(data['data']!, _dataMeta),
);
} else if (isInserting) {
context.missing(_dataMeta);
}
if (data.containsKey('created_at')) {
context.handle(
_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
);
} else if (isInserting) {
context.missing(_createdAtMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
ChatMessage map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return ChatMessage(
id:
attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}id'],
)!,
roomId:
attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}room_id'],
)!,
senderId:
attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}sender_id'],
)!,
content: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}content'],
),
nonce: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}nonce'],
),
data:
attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}data'],
)!,
createdAt:
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
status: $ChatMessagesTable.$converterstatus.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}status'],
)!,
),
);
}
@override
$ChatMessagesTable createAlias(String alias) {
return $ChatMessagesTable(attachedDatabase, alias);
}
static JsonTypeConverter2<MessageStatus, int, int> $converterstatus =
const EnumIndexConverter<MessageStatus>(MessageStatus.values);
}
class ChatMessage extends DataClass implements Insertable<ChatMessage> {
final String id;
final int roomId;
final String senderId;
final String? content;
final String? nonce;
final String data;
final DateTime createdAt;
final MessageStatus status;
const ChatMessage({
required this.id,
required this.roomId,
required this.senderId,
this.content,
this.nonce,
required this.data,
required this.createdAt,
required this.status,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
map['room_id'] = Variable<int>(roomId);
map['sender_id'] = Variable<String>(senderId);
if (!nullToAbsent || content != null) {
map['content'] = Variable<String>(content);
}
if (!nullToAbsent || nonce != null) {
map['nonce'] = Variable<String>(nonce);
}
map['data'] = Variable<String>(data);
map['created_at'] = Variable<DateTime>(createdAt);
{
map['status'] = Variable<int>(
$ChatMessagesTable.$converterstatus.toSql(status),
);
}
return map;
}
ChatMessagesCompanion toCompanion(bool nullToAbsent) {
return ChatMessagesCompanion(
id: Value(id),
roomId: Value(roomId),
senderId: Value(senderId),
content:
content == null && nullToAbsent
? const Value.absent()
: Value(content),
nonce:
nonce == null && nullToAbsent ? const Value.absent() : Value(nonce),
data: Value(data),
createdAt: Value(createdAt),
status: Value(status),
);
}
factory ChatMessage.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return ChatMessage(
id: serializer.fromJson<String>(json['id']),
roomId: serializer.fromJson<int>(json['roomId']),
senderId: serializer.fromJson<String>(json['senderId']),
content: serializer.fromJson<String?>(json['content']),
nonce: serializer.fromJson<String?>(json['nonce']),
data: serializer.fromJson<String>(json['data']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
status: $ChatMessagesTable.$converterstatus.fromJson(
serializer.fromJson<int>(json['status']),
),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'roomId': serializer.toJson<int>(roomId),
'senderId': serializer.toJson<String>(senderId),
'content': serializer.toJson<String?>(content),
'nonce': serializer.toJson<String?>(nonce),
'data': serializer.toJson<String>(data),
'createdAt': serializer.toJson<DateTime>(createdAt),
'status': serializer.toJson<int>(
$ChatMessagesTable.$converterstatus.toJson(status),
),
};
}
ChatMessage copyWith({
String? id,
int? roomId,
String? senderId,
Value<String?> content = const Value.absent(),
Value<String?> nonce = const Value.absent(),
String? data,
DateTime? createdAt,
MessageStatus? status,
}) => ChatMessage(
id: id ?? this.id,
roomId: roomId ?? this.roomId,
senderId: senderId ?? this.senderId,
content: content.present ? content.value : this.content,
nonce: nonce.present ? nonce.value : this.nonce,
data: data ?? this.data,
createdAt: createdAt ?? this.createdAt,
status: status ?? this.status,
);
ChatMessage copyWithCompanion(ChatMessagesCompanion data) {
return ChatMessage(
id: data.id.present ? data.id.value : this.id,
roomId: data.roomId.present ? data.roomId.value : this.roomId,
senderId: data.senderId.present ? data.senderId.value : this.senderId,
content: data.content.present ? data.content.value : this.content,
nonce: data.nonce.present ? data.nonce.value : this.nonce,
data: data.data.present ? data.data.value : this.data,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
status: data.status.present ? data.status.value : this.status,
);
}
@override
String toString() {
return (StringBuffer('ChatMessage(')
..write('id: $id, ')
..write('roomId: $roomId, ')
..write('senderId: $senderId, ')
..write('content: $content, ')
..write('nonce: $nonce, ')
..write('data: $data, ')
..write('createdAt: $createdAt, ')
..write('status: $status')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(
id,
roomId,
senderId,
content,
nonce,
data,
createdAt,
status,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is ChatMessage &&
other.id == this.id &&
other.roomId == this.roomId &&
other.senderId == this.senderId &&
other.content == this.content &&
other.nonce == this.nonce &&
other.data == this.data &&
other.createdAt == this.createdAt &&
other.status == this.status);
}
class ChatMessagesCompanion extends UpdateCompanion<ChatMessage> {
final Value<String> id;
final Value<int> roomId;
final Value<String> senderId;
final Value<String?> content;
final Value<String?> nonce;
final Value<String> data;
final Value<DateTime> createdAt;
final Value<MessageStatus> status;
final Value<int> rowid;
const ChatMessagesCompanion({
this.id = const Value.absent(),
this.roomId = const Value.absent(),
this.senderId = const Value.absent(),
this.content = const Value.absent(),
this.nonce = const Value.absent(),
this.data = const Value.absent(),
this.createdAt = const Value.absent(),
this.status = const Value.absent(),
this.rowid = const Value.absent(),
});
ChatMessagesCompanion.insert({
required String id,
required int roomId,
required String senderId,
this.content = const Value.absent(),
this.nonce = const Value.absent(),
required String data,
required DateTime createdAt,
required MessageStatus status,
this.rowid = const Value.absent(),
}) : id = Value(id),
roomId = Value(roomId),
senderId = Value(senderId),
data = Value(data),
createdAt = Value(createdAt),
status = Value(status);
static Insertable<ChatMessage> custom({
Expression<String>? id,
Expression<int>? roomId,
Expression<String>? senderId,
Expression<String>? content,
Expression<String>? nonce,
Expression<String>? data,
Expression<DateTime>? createdAt,
Expression<int>? status,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (roomId != null) 'room_id': roomId,
if (senderId != null) 'sender_id': senderId,
if (content != null) 'content': content,
if (nonce != null) 'nonce': nonce,
if (data != null) 'data': data,
if (createdAt != null) 'created_at': createdAt,
if (status != null) 'status': status,
if (rowid != null) 'rowid': rowid,
});
}
ChatMessagesCompanion copyWith({
Value<String>? id,
Value<int>? roomId,
Value<String>? senderId,
Value<String?>? content,
Value<String?>? nonce,
Value<String>? data,
Value<DateTime>? createdAt,
Value<MessageStatus>? status,
Value<int>? rowid,
}) {
return ChatMessagesCompanion(
id: id ?? this.id,
roomId: roomId ?? this.roomId,
senderId: senderId ?? this.senderId,
content: content ?? this.content,
nonce: nonce ?? this.nonce,
data: data ?? this.data,
createdAt: createdAt ?? this.createdAt,
status: status ?? this.status,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (roomId.present) {
map['room_id'] = Variable<int>(roomId.value);
}
if (senderId.present) {
map['sender_id'] = Variable<String>(senderId.value);
}
if (content.present) {
map['content'] = Variable<String>(content.value);
}
if (nonce.present) {
map['nonce'] = Variable<String>(nonce.value);
}
if (data.present) {
map['data'] = Variable<String>(data.value);
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (status.present) {
map['status'] = Variable<int>(
$ChatMessagesTable.$converterstatus.toSql(status.value),
);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('ChatMessagesCompanion(')
..write('id: $id, ')
..write('roomId: $roomId, ')
..write('senderId: $senderId, ')
..write('content: $content, ')
..write('nonce: $nonce, ')
..write('data: $data, ')
..write('createdAt: $createdAt, ')
..write('status: $status, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $ChatMessagesTable chatMessages = $ChatMessagesTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [chatMessages];
}
typedef $$ChatMessagesTableCreateCompanionBuilder =
ChatMessagesCompanion Function({
required String id,
required int roomId,
required String senderId,
Value<String?> content,
Value<String?> nonce,
required String data,
required DateTime createdAt,
required MessageStatus status,
Value<int> rowid,
});
typedef $$ChatMessagesTableUpdateCompanionBuilder =
ChatMessagesCompanion Function({
Value<String> id,
Value<int> roomId,
Value<String> senderId,
Value<String?> content,
Value<String?> nonce,
Value<String> data,
Value<DateTime> createdAt,
Value<MessageStatus> status,
Value<int> rowid,
});
class $$ChatMessagesTableFilterComposer
extends Composer<_$AppDatabase, $ChatMessagesTable> {
$$ChatMessagesTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get roomId => $composableBuilder(
column: $table.roomId,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get senderId => $composableBuilder(
column: $table.senderId,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get content => $composableBuilder(
column: $table.content,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get nonce => $composableBuilder(
column: $table.nonce,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get data => $composableBuilder(
column: $table.data,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnFilters(column),
);
ColumnWithTypeConverterFilters<MessageStatus, MessageStatus, int>
get status => $composableBuilder(
column: $table.status,
builder: (column) => ColumnWithTypeConverterFilters(column),
);
}
class $$ChatMessagesTableOrderingComposer
extends Composer<_$AppDatabase, $ChatMessagesTable> {
$$ChatMessagesTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get roomId => $composableBuilder(
column: $table.roomId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get senderId => $composableBuilder(
column: $table.senderId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get content => $composableBuilder(
column: $table.content,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get nonce => $composableBuilder(
column: $table.nonce,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get data => $composableBuilder(
column: $table.data,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get status => $composableBuilder(
column: $table.status,
builder: (column) => ColumnOrderings(column),
);
}
class $$ChatMessagesTableAnnotationComposer
extends Composer<_$AppDatabase, $ChatMessagesTable> {
$$ChatMessagesTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<int> get roomId =>
$composableBuilder(column: $table.roomId, builder: (column) => column);
GeneratedColumn<String> get senderId =>
$composableBuilder(column: $table.senderId, builder: (column) => column);
GeneratedColumn<String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column);
GeneratedColumn<String> get nonce =>
$composableBuilder(column: $table.nonce, builder: (column) => column);
GeneratedColumn<String> get data =>
$composableBuilder(column: $table.data, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
GeneratedColumnWithTypeConverter<MessageStatus, int> get status =>
$composableBuilder(column: $table.status, builder: (column) => column);
}
class $$ChatMessagesTableTableManager
extends
RootTableManager<
_$AppDatabase,
$ChatMessagesTable,
ChatMessage,
$$ChatMessagesTableFilterComposer,
$$ChatMessagesTableOrderingComposer,
$$ChatMessagesTableAnnotationComposer,
$$ChatMessagesTableCreateCompanionBuilder,
$$ChatMessagesTableUpdateCompanionBuilder,
(
ChatMessage,
BaseReferences<_$AppDatabase, $ChatMessagesTable, ChatMessage>,
),
ChatMessage,
PrefetchHooks Function()
> {
$$ChatMessagesTableTableManager(_$AppDatabase db, $ChatMessagesTable table)
: super(
TableManagerState(
db: db,
table: table,
createFilteringComposer:
() => $$ChatMessagesTableFilterComposer($db: db, $table: table),
createOrderingComposer:
() => $$ChatMessagesTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer:
() =>
$$ChatMessagesTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<String> id = const Value.absent(),
Value<int> roomId = const Value.absent(),
Value<String> senderId = const Value.absent(),
Value<String?> content = const Value.absent(),
Value<String?> nonce = const Value.absent(),
Value<String> data = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<MessageStatus> status = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => ChatMessagesCompanion(
id: id,
roomId: roomId,
senderId: senderId,
content: content,
nonce: nonce,
data: data,
createdAt: createdAt,
status: status,
rowid: rowid,
),
createCompanionCallback:
({
required String id,
required int roomId,
required String senderId,
Value<String?> content = const Value.absent(),
Value<String?> nonce = const Value.absent(),
required String data,
required DateTime createdAt,
required MessageStatus status,
Value<int> rowid = const Value.absent(),
}) => ChatMessagesCompanion.insert(
id: id,
roomId: roomId,
senderId: senderId,
content: content,
nonce: nonce,
data: data,
createdAt: createdAt,
status: status,
rowid: rowid,
),
withReferenceMapper:
(p0) =>
p0
.map(
(e) => (
e.readTable(table),
BaseReferences(db, table, e),
),
)
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$ChatMessagesTableProcessedTableManager =
ProcessedTableManager<
_$AppDatabase,
$ChatMessagesTable,
ChatMessage,
$$ChatMessagesTableFilterComposer,
$$ChatMessagesTableOrderingComposer,
$$ChatMessagesTableAnnotationComposer,
$$ChatMessagesTableCreateCompanionBuilder,
$$ChatMessagesTableUpdateCompanionBuilder,
(
ChatMessage,
BaseReferences<_$AppDatabase, $ChatMessagesTable, ChatMessage>,
),
ChatMessage,
PrefetchHooks Function()
>;
class $AppDatabaseManager {
final _$AppDatabase _db;
$AppDatabaseManager(this._db);
$$ChatMessagesTableTableManager get chatMessages =>
$$ChatMessagesTableTableManager(_db, _db.chatMessages);
}

58
lib/database/message.dart Normal file
View File

@ -0,0 +1,58 @@
import 'package:drift/drift.dart';
import 'package:island/models/chat.dart';
class ChatMessages extends Table {
TextColumn get id => text()();
IntColumn get roomId => integer()();
TextColumn get senderId => text()();
TextColumn get content => text().nullable()();
TextColumn get nonce => text().nullable()();
TextColumn get data => text()();
DateTimeColumn get createdAt => dateTime()();
IntColumn get status => intEnum<MessageStatus>()();
@override
Set<Column> get primaryKey => {id};
}
class LocalChatMessage {
final String id;
final int roomId;
final String senderId;
final Map<String, dynamic> data;
final DateTime createdAt;
MessageStatus status;
final String? nonce;
LocalChatMessage({
required this.id,
required this.roomId,
required this.senderId,
required this.data,
required this.createdAt,
required this.status,
this.nonce,
});
SnChatMessage toRemoteMessage() {
return SnChatMessage.fromJson(data);
}
static LocalChatMessage fromRemoteMessage(
SnChatMessage message,
MessageStatus status, {
String? nonce,
}) {
return LocalChatMessage(
id: message.id,
roomId: message.chatRoomId,
senderId: message.senderId,
data: message.toJson(),
createdAt: message.createdAt,
status: status,
nonce: nonce ?? message.nonce,
);
}
}
enum MessageStatus { pending, sent, failed }

View File

@ -0,0 +1,263 @@
import 'package:dio/dio.dart';
import 'package:island/database/drift_db.dart';
import 'package:island/database/message.dart';
import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
import 'package:uuid/uuid.dart';
class MessageRepository {
final SnChat room;
final Dio _apiClient;
final AppDatabase _database;
SnChatMember? _identity;
final Map<String, LocalChatMessage> _pendingMessages = {};
MessageRepository(this.room, this._apiClient, this._database) {
initialize();
}
bool initialized = false;
Future<void> initialize() async {
if (initialized) return;
try {
final response = await _apiClient.get('/chat/${room.id}/members/me');
_identity = SnChatMember.fromJson(response.data);
initialized = true;
} catch (e) {
rethrow;
}
}
Future<List<LocalChatMessage>> listMessages({
int offset = 0,
int take = 20,
}) async {
try {
final localMessages = await _getCachedMessages(
room.id,
offset: offset,
take: take,
);
if (offset == 0) {
// Always fetch latest messages in background if we're loading the first page
_fetchAndCacheMessages(room.id, offset: offset, take: take);
if (localMessages.isNotEmpty) {
return localMessages;
}
}
return await _fetchAndCacheMessages(room.id, offset: offset, take: take);
} catch (e) {
// If API fails but we have local messages, return them
final localMessages = await _getCachedMessages(
room.id,
offset: offset,
take: take,
);
if (localMessages.isNotEmpty) {
return localMessages;
}
rethrow;
}
}
Future<List<LocalChatMessage>> _getCachedMessages(
int roomId, {
int offset = 0,
int take = 20,
}) async {
// Get messages from local database
final dbMessages = await _database.getMessagesForRoom(
roomId,
offset: offset,
limit: take,
);
final dbLocalMessages =
dbMessages.map(_database.companionToMessage).toList();
// Combine with pending messages
final pendingForRoom =
_pendingMessages.values.where((msg) => msg.roomId == roomId).toList();
// Sort by timestamp descending (newest first)
final allMessages = [...pendingForRoom, ...dbLocalMessages];
allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
// Apply pagination
if (offset >= allMessages.length) {
return [];
}
final end =
(offset + take) > allMessages.length
? allMessages.length
: (offset + take);
return allMessages.sublist(offset, end);
}
Future<List<LocalChatMessage>> _fetchAndCacheMessages(
int roomId, {
int offset = 0,
int take = 20,
}) async {
final response = await _apiClient.get(
'/chat/$roomId/messages',
queryParameters: {'offset': offset, 'take': take},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final messages =
data.map((json) {
final remoteMessage = SnChatMessage.fromJson(json);
return LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
}).toList();
for (final message in messages) {
await _database.saveMessage(_database.messageToCompanion(message));
if (message.nonce != null) {
_pendingMessages.removeWhere(
(_, pendingMsg) => pendingMsg.nonce == message.nonce,
);
}
}
return messages;
}
Future<LocalChatMessage> sendMessage(
int roomId,
String content, {
List<SnCloudFile>? attachments,
Map<String, dynamic>? meta,
}) async {
if (!initialized) {
throw UnsupportedError(
"The message repository is not ready for send message.",
);
}
// Generate a unique nonce for this message
final nonce = const Uuid().v4();
// Create a local message with pending status
final mockMessage = SnChatMessage(
id: 'pending_$nonce',
chatRoomId: roomId,
senderId: _identity!.id,
content: content,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
nonce: nonce,
sender: _identity!,
);
final localMessage = LocalChatMessage.fromRemoteMessage(
mockMessage,
MessageStatus.pending,
);
// Store in memory and database
_pendingMessages[localMessage.id] = localMessage;
await _database.saveMessage(_database.messageToCompanion(localMessage));
try {
// Send to server
final response = await _apiClient.post(
'/chat/$roomId/messages',
data: {
'content': content,
'attachments_id': attachments,
'meta': meta,
'nonce': nonce,
},
);
// Update with server response
final remoteMessage = SnChatMessage.fromJson(response.data);
final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
// Remove from pending and update in database
_pendingMessages.remove(localMessage.id);
await _database.deleteMessage(localMessage.id);
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
return updatedMessage;
} catch (e) {
// Update status to failed
localMessage.status = MessageStatus.failed;
_pendingMessages[localMessage.id] = localMessage;
await _database.updateMessageStatus(
localMessage.id,
MessageStatus.failed,
);
rethrow;
}
}
Future<LocalChatMessage> retryMessage(String pendingMessageId) async {
final message = _pendingMessages[pendingMessageId];
if (message == null) {
throw Exception('Message not found');
}
// Update status back to pending
message.status = MessageStatus.pending;
_pendingMessages[pendingMessageId] = message;
await _database.updateMessageStatus(
pendingMessageId,
MessageStatus.pending,
);
try {
// Send to server
var remoteMessage = message.toRemoteMessage();
final response = await _apiClient.post(
'/chat/${message.roomId}/messages',
data: {
'content': remoteMessage.content,
'attachments_id': remoteMessage.attachments,
'meta': remoteMessage.meta,
'nonce': message.nonce,
},
);
// Update with server response
remoteMessage = SnChatMessage.fromJson(response.data);
final updatedMessage = LocalChatMessage.fromRemoteMessage(
remoteMessage,
MessageStatus.sent,
);
// Remove from pending and update in database
_pendingMessages.remove(pendingMessageId);
await _database.deleteMessage(pendingMessageId);
await _database.saveMessage(_database.messageToCompanion(updatedMessage));
return updatedMessage;
} catch (e) {
// Update status to failed
message.status = MessageStatus.failed;
_pendingMessages[pendingMessageId] = message;
await _database.updateMessageStatus(
pendingMessageId,
MessageStatus.failed,
);
rethrow;
}
}
}