✨ Basic messages & loading
This commit is contained in:
81
lib/database/drift_db.dart
Normal file
81
lib/database/drift_db.dart
Normal 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);
|
||||
});
|
||||
}
|
807
lib/database/drift_db.g.dart
Normal file
807
lib/database/drift_db.g.dart
Normal 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
58
lib/database/message.dart
Normal 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 }
|
263
lib/database/message_repository.dart
Normal file
263
lib/database/message_repository.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user