🎨 Use feature based folder structure

This commit is contained in:
2026-02-06 00:37:02 +08:00
parent 62a3ea26e3
commit 862e3b451b
539 changed files with 8406 additions and 5056 deletions

View File

@@ -0,0 +1,16 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:island/data/drift_db.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
AppDatabase constructDb() {
final db = LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'solar_network_data.sqlite'));
return NativeDatabase(file);
});
return AppDatabase(db);
}

View File

@@ -0,0 +1,26 @@
import 'package:drift/drift.dart';
import 'package:drift/wasm.dart';
import 'package:island/data/drift_db.dart';
import 'package:island/talker.dart';
AppDatabase constructDb() {
return AppDatabase(connectOnWeb());
}
DatabaseConnection connectOnWeb() {
return DatabaseConnection.delayed(
Future(() async {
try {
final result = await WasmDatabase.open(
databaseName: 'solar_network_data',
sqlite3Uri: Uri.parse('sqlite3.wasm'),
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
);
return result.resolvedExecutor;
} catch (e, stackTrace) {
talker.error('Failed to open WASM database...', e, stackTrace);
rethrow;
}
}),
);
}

17
lib/data/draft.dart Normal file
View File

@@ -0,0 +1,17 @@
import 'package:drift/drift.dart';
class PostDrafts extends Table {
TextColumn get id => text()();
// Searchable fields stored separately for performance
TextColumn get title => text().nullable()();
TextColumn get description => text().nullable()();
TextColumn get content => text().nullable()();
IntColumn get visibility => integer().withDefault(const Constant(0))();
IntColumn get type => integer().withDefault(const Constant(0))();
DateTimeColumn get lastModified => dateTime()();
// Full post data stored as JSON for complete restoration
TextColumn get postData => text()();
@override
Set<Column> get primaryKey => {id};
}

553
lib/data/drift_db.dart Normal file
View File

@@ -0,0 +1,553 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:island/data/draft.dart';
import 'package:island/data/message.dart';
import 'package:island/accounts/accounts_models/account.dart';
import 'package:island/chat/chat_models/chat.dart';
import 'package:island/posts/posts_models/post.dart';
import 'package:island/realms/realms_models/realm.dart';
part 'drift_db.g.dart';
// Define the database
@DriftDatabase(
tables: [Realms, ChatRooms, ChatMembers, ChatMessages, PostDrafts],
)
class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);
@override
int get schemaVersion => 12;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (Migrator m) async {
await m.createAll();
},
onUpgrade: (Migrator m, int from, int to) async {
if (from < 2) {
// Add isDeleted column with default value false
await m.addColumn(chatMessages, chatMessages.isDeleted);
}
if (from < 4) {
// Drop old draft tables if they exist
await m.createTable(postDrafts);
}
if (from < 6) {
// Migrate from old schema to new schema with separate searchable fields
await _migrateToVersion6(m);
}
if (from < 7) {
// Add new columns from SnChatMessage, ignore if they already exist
final columnsToAdd = [
chatMessages.updatedAt,
chatMessages.deletedAt,
chatMessages.type,
chatMessages.meta,
chatMessages.membersMentioned,
chatMessages.editedAt,
chatMessages.attachments,
chatMessages.reactions,
chatMessages.repliedMessageId,
chatMessages.forwardedMessageId,
];
for (final column in columnsToAdd) {
try {
await m.addColumn(chatMessages, column);
} catch (e) {
// Column already exists, skip
}
}
}
if (from < 8) {
// Add new tables for separate sender and room data
await m.createTable(chatRooms);
await m.createTable(chatMembers);
}
if (from < 9) {
// Remove unused columns from chat_members
await customStatement('ALTER TABLE chat_members DROP COLUMN role');
await customStatement('ALTER TABLE chat_members DROP COLUMN is_bot');
await customStatement('ALTER TABLE chat_members DROP COLUMN status');
await customStatement(
'ALTER TABLE chat_members DROP COLUMN last_typed',
);
}
if (from < 10) {
// Add realms table and update chat_rooms foreign key
await m.createTable(realms);
// The realmId column in chat_rooms already exists, just need to ensure the foreign key constraint
}
if (from < 11) {
// Add isPinned column to chat_rooms table
await customStatement(
'ALTER TABLE chat_rooms ADD COLUMN is_pinned INTEGER DEFAULT 0',
);
}
if (from < 12) {
// Add new columns to realms table
await customStatement(
'ALTER TABLE realms ADD COLUMN slug TEXT NOT NULL DEFAULT \'\'',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN verified_as TEXT NULL',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN verified_at DATETIME NULL',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN is_community INTEGER NOT NULL DEFAULT 0',
);
await customStatement(
'ALTER TABLE realms ADD COLUMN is_public INTEGER NOT NULL DEFAULT 0',
);
}
},
);
Future<void> _migrateToVersion6(Migrator m) async {
// Rename existing table to old if it exists
try {
await customStatement(
'ALTER TABLE post_drafts RENAME TO post_drafts_old',
);
} catch (e) {
// Table might not exist
}
// Drop the table
await customStatement('DROP TABLE IF EXISTS post_drafts');
// Create new table
await m.createTable(postDrafts);
// Migrate existing data if any
try {
final oldDrafts = await customSelect(
'SELECT id, post, lastModified FROM post_drafts_old',
readsFrom: {postDrafts},
).get();
for (final row in oldDrafts) {
final postJson = row.read<String>('post');
final id = row.read<String>('id');
final lastModified = row.read<DateTime>('lastModified');
if (postJson.isNotEmpty) {
final post = SnPost.fromJson(jsonDecode(postJson));
await into(postDrafts).insert(
PostDraftsCompanion(
id: Value(id),
title: Value(post.title),
description: Value(post.description),
content: Value(post.content),
visibility: Value(post.visibility),
type: Value(post.type),
lastModified: Value(lastModified),
postData: Value(postJson),
),
);
}
}
// Drop old table
await customStatement('DROP TABLE IF EXISTS post_drafts_old');
} catch (e) {
// If migration fails, just recreate the table
await m.createTable(postDrafts);
}
}
// Methods for chat messages
Future<List<ChatMessage>> getMessagesForRoom(
String 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> updateMessage(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();
}
Future<int> getTotalMessagesForRoom(String roomId) {
return (select(
chatMessages,
)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length);
}
Future<List<LocalChatMessage>> searchMessages(
String roomId,
String query, {
bool? withAttachments,
Future<SnAccount?> Function(String accountId)? fetchAccount,
}) async {
var selectStatement = select(chatMessages)
..where((m) => m.roomId.equals(roomId));
if (query.isNotEmpty) {
final searchTerm = '%$query%';
selectStatement = selectStatement
..where(
(m) =>
m.content.like(searchTerm) |
m.meta.like(searchTerm) |
m.attachments.like(searchTerm) |
m.type.like(searchTerm),
);
}
if (withAttachments == true) {
selectStatement = selectStatement
..where((m) => m.attachments.equals('[]').not());
}
final messages =
await (selectStatement
..orderBy([(m) => OrderingTerm.desc(m.createdAt)]))
.get();
final messageFutures = messages
.map((msg) => companionToMessage(msg, fetchAccount: fetchAccount))
.toList();
return await Future.wait(messageFutures);
}
// Convert between Drift and model objects
ChatMessagesCompanion messageToCompanion(LocalChatMessage message) {
final remote = message.toRemoteMessage();
return ChatMessagesCompanion(
id: Value(message.id),
roomId: Value(message.roomId),
senderId: Value(message.senderId),
content: Value(remote.content),
nonce: Value(message.nonce),
data: Value(jsonEncode(message.data)),
createdAt: Value(message.createdAt),
status: Value(message.status),
updatedAt: Value(remote.updatedAt),
deletedAt: Value(remote.deletedAt),
type: Value(remote.type),
meta: Value(remote.meta),
membersMentioned: Value(remote.membersMentioned),
editedAt: Value(remote.editedAt),
attachments: Value(remote.attachments.map((e) => e.toJson()).toList()),
reactions: Value(remote.reactions.map((e) => e.toJson()).toList()),
repliedMessageId: Value(remote.repliedMessageId),
forwardedMessageId: Value(remote.forwardedMessageId),
);
}
Future<LocalChatMessage> companionToMessage(
ChatMessage dbMessage, {
Future<SnAccount?> Function(String accountId)? fetchAccount,
}) async {
final data = jsonDecode(dbMessage.data);
SnChatMember? sender;
try {
final senderRow = await (select(
chatMembers,
)..where((m) => m.id.equals(dbMessage.senderId))).getSingle();
SnAccount senderAccount;
senderAccount = SnAccount.fromJson(senderRow.account);
sender = SnChatMember(
id: senderRow.id,
chatRoomId: senderRow.chatRoomId,
accountId: senderRow.accountId,
account: senderAccount,
nick: senderRow.nick,
notify: senderRow.notify,
joinedAt: senderRow.joinedAt,
breakUntil: senderRow.breakUntil,
timeoutUntil: senderRow.timeoutUntil,
status: null,
createdAt: senderRow.createdAt,
updatedAt: senderRow.updatedAt,
deletedAt: senderRow.deletedAt,
chatRoom: null,
);
} catch (err) {
// Fallback to dummy sender with senderId as display name
sender = SnChatMember(
id: 'unknown',
chatRoomId: dbMessage.roomId,
accountId: dbMessage.senderId,
account: SnAccount(
id: 'unknown',
name: 'unknown',
nick: dbMessage.senderId, // Show the ID instead of Unknown
activatedAt: null,
profile: SnAccountProfile(
picture: null,
id: 'unknown',
experience: 0,
level: 1,
levelingProgress: 0.0,
background: null,
verification: null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
),
language: '',
isSuperuser: false,
automatedId: null,
perkSubscription: null,
deletedAt: null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
nick: dbMessage.senderId, // Show the senderId as fallback
notify: 0,
joinedAt: null,
breakUntil: null,
timeoutUntil: null,
status: null,
lastTyped: null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
chatRoom: null,
);
}
return LocalChatMessage(
id: dbMessage.id,
roomId: dbMessage.roomId,
senderId: dbMessage.senderId,
sender: sender,
data: data,
createdAt: dbMessage.createdAt,
status: dbMessage.status,
nonce: dbMessage.nonce,
content: dbMessage.content,
isDeleted: dbMessage.isDeleted,
updatedAt: dbMessage.updatedAt,
deletedAt: dbMessage.deletedAt,
type: dbMessage.type,
meta: dbMessage.meta,
membersMentioned: dbMessage.membersMentioned,
editedAt: dbMessage.editedAt,
attachments: dbMessage.attachments,
reactions: dbMessage.reactions,
repliedMessageId: dbMessage.repliedMessageId,
forwardedMessageId: dbMessage.forwardedMessageId,
);
}
ChatRoomsCompanion companionFromRoom(SnChatRoom room) {
return ChatRoomsCompanion(
id: Value(room.id),
name: Value(room.name),
description: Value(room.description),
type: Value(room.type),
isPublic: Value(room.isPublic),
isCommunity: Value(room.isCommunity),
picture: Value(room.picture?.toJson()),
background: Value(room.background?.toJson()),
realmId: Value(room.realmId),
accountId: Value(room.accountId),
createdAt: Value(room.createdAt),
updatedAt: Value(room.updatedAt),
deletedAt: Value(room.deletedAt),
);
}
ChatMembersCompanion companionFromMember(SnChatMember member) {
return ChatMembersCompanion(
id: Value(member.id),
chatRoomId: Value(member.chatRoomId),
accountId: Value(member.accountId),
account: Value(member.account.toJson()),
nick: Value(member.nick),
notify: Value(member.notify),
joinedAt: Value(member.joinedAt),
breakUntil: Value(member.breakUntil),
timeoutUntil: Value(member.timeoutUntil),
createdAt: Value(member.createdAt),
updatedAt: Value(member.updatedAt),
deletedAt: Value(member.deletedAt),
);
}
RealmsCompanion companionFromRealm(SnRealm realm) {
return RealmsCompanion(
id: Value(realm.id),
slug: Value(realm.slug),
name: Value(realm.name),
description: Value(realm.description),
verifiedAs: Value(realm.verifiedAs),
verifiedAt: Value(realm.verifiedAt),
isCommunity: Value(realm.isCommunity),
isPublic: Value(realm.isPublic),
picture: Value(realm.picture?.toJson()),
background: Value(realm.background?.toJson()),
accountId: Value(realm.accountId),
createdAt: Value(realm.createdAt),
updatedAt: Value(realm.updatedAt),
deletedAt: Value(realm.deletedAt),
);
}
Future<void> saveChatRooms(
List<SnChatRoom> rooms, {
bool override = false,
}) async {
await transaction(() async {
if (override) {
// 1. Identify rooms to remove
final remoteRoomIds = rooms.map((r) => r.id).toSet();
final currentRooms = await select(chatRooms).get();
final currentRoomIds = currentRooms.map((r) => r.id).toSet();
final idsToRemove = currentRoomIds.difference(remoteRoomIds);
if (idsToRemove.isNotEmpty) {
final idsList = idsToRemove.toList();
// Remove messages
await (delete(
chatMessages,
)..where((t) => t.roomId.isIn(idsList))).go();
// Remove members
await (delete(
chatMembers,
)..where((t) => t.chatRoomId.isIn(idsList))).go();
// Remove rooms
await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go();
}
}
// 2. Upsert realms first
final realmsToSave = rooms
.where((room) => room.realm != null)
.map((room) => room.realm!)
.toSet()
.toList();
await batch((batch) {
for (final realm in realmsToSave) {
batch.insert(
realms,
companionFromRealm(realm),
mode: InsertMode.insertOrReplace,
);
}
});
// 3. Upsert remote rooms
await batch((batch) async {
for (final room in rooms) {
// Preserve local isPinned status
final currentRoom = await (select(
chatRooms,
)..where((r) => r.id.equals(room.id))).getSingleOrNull();
final isPinned = currentRoom?.isPinned ?? false;
batch.insert(
chatRooms,
companionFromRoom(room).copyWith(isPinned: Value(isPinned)),
mode: InsertMode.insertOrReplace,
);
for (final member in room.members ?? []) {
batch.insert(
chatMembers,
companionFromMember(member),
mode: InsertMode.insertOrReplace,
);
}
}
});
});
}
// Methods for post drafts
Future<List<SnPost>> getAllPostDrafts() async {
final drafts = await select(postDrafts).get();
return drafts
.map((draft) => SnPost.fromJson(jsonDecode(draft.postData)))
.toList();
}
Future<List<PostDraft>> getAllPostDraftRecords() async {
return await select(postDrafts).get();
}
Future<List<PostDraft>> searchPostDrafts(String query) async {
if (query.isEmpty) {
return await select(postDrafts).get();
}
final searchTerm = '%${query.toLowerCase()}%';
return await (select(postDrafts)
..where(
(draft) =>
draft.title.like(searchTerm) |
draft.description.like(searchTerm) |
draft.content.like(searchTerm),
)
..orderBy([(draft) => OrderingTerm.desc(draft.lastModified)]))
.get();
}
Future<void> addPostDraft(PostDraftsCompanion entry) async {
await into(postDrafts).insert(entry, mode: InsertMode.replace);
}
Future<void> deletePostDraft(String id) async {
await (delete(postDrafts)..where((tbl) => tbl.id.equals(id))).go();
}
Future<void> clearAllPostDrafts() async {
await delete(postDrafts).go();
}
Future<PostDraft?> getPostDraftById(String id) async {
return await (select(
postDrafts,
)..where((tbl) => tbl.id.equals(id))).getSingleOrNull();
}
Future<void> saveMember(SnChatMember member) async {
await into(
chatMembers,
).insert(companionFromMember(member), mode: InsertMode.insertOrReplace);
}
Future<int> saveMessageWithSender(LocalChatMessage message) async {
// First save the sender if it exists
if (message.sender != null) {
await saveMember(message.sender!);
}
// Then save the message
return await saveMessage(messageToCompanion(message));
}
Future<void> toggleChatRoomPinned(String roomId) async {
final room = await (select(
chatRooms,
)..where((r) => r.id.equals(roomId))).getSingleOrNull();
if (room != null) {
final newPinnedStatus = !(room.isPinned ?? false);
await (update(chatRooms)..where((r) => r.id.equals(roomId))).write(
ChatRoomsCompanion(isPinned: Value(newPinnedStatus)),
);
}
}
}

6749
lib/data/drift_db.g.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,616 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
// GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema7 extends i0.VersionedSchema {
Schema7({required super.database}) : super(version: 7);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
chatRooms,
chatMembers,
chatMessages,
postDrafts,
];
late final Shape0 chatRooms = Shape0(
source: i0.VersionedTable(
entityName: 'chat_rooms',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape1 chatMembers = Shape1(
source: i0.VersionedTable(
entityName: 'chat_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_12,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_9,
_column_10,
_column_11,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape2 chatMessages = Shape2(
source: i0.VersionedTable(
entityName: 'chat_messages',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_24,
_column_25,
_column_26,
_column_27,
_column_28,
_column_9,
_column_29,
_column_30,
_column_31,
_column_11,
_column_32,
_column_33,
_column_34,
_column_35,
_column_36,
_column_37,
_column_38,
_column_39,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 postDrafts = Shape3(
source: i0.VersionedTable(
entityName: 'post_drafts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_40,
_column_2,
_column_26,
_column_41,
_column_42,
_column_43,
_column_44,
],
attachedDatabase: database,
),
alias: null,
);
}
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get isPublic =>
columnsByName['is_public']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get isCommunity =>
columnsByName['is_community']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get picture =>
columnsByName['picture']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get background =>
columnsByName['background']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get realmId =>
columnsByName['realm_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
i1.GeneratedColumn<String>(
'id',
aliasedName,
false,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>(
'name',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
i1.GeneratedColumn<String>(
'description',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<int> _column_3(String aliasedName) =>
i1.GeneratedColumn<int>(
'type',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<bool> _column_4(String aliasedName) =>
i1.GeneratedColumn<bool>(
'is_public',
aliasedName,
true,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_public" IN (0, 1))',
),
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<bool> _column_5(String aliasedName) =>
i1.GeneratedColumn<bool>(
'is_community',
aliasedName,
true,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_community" IN (0, 1))',
),
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<String> _column_6(String aliasedName) =>
i1.GeneratedColumn<String>(
'picture',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
i1.GeneratedColumn<String>(
'background',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
i1.GeneratedColumn<String>(
'realm_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'created_at',
aliasedName,
false,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<DateTime> _column_10(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
false,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'deleted_at',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get chatRoomId =>
columnsByName['chat_room_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get account =>
columnsByName['account']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get nick =>
columnsByName['nick']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get role =>
columnsByName['role']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get notify =>
columnsByName['notify']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get joinedAt =>
columnsByName['joined_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get breakUntil =>
columnsByName['break_until']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get timeoutUntil =>
columnsByName['timeout_until']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<bool> get isBot =>
columnsByName['is_bot']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get status =>
columnsByName['status']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get lastTyped =>
columnsByName['last_typed']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
i1.GeneratedColumn<String>(
'chat_room_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES chat_rooms (id)',
),
);
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
i1.GeneratedColumn<String>(
'account_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
i1.GeneratedColumn<String>(
'account',
aliasedName,
false,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
i1.GeneratedColumn<String>(
'nick',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<int> _column_16(String aliasedName) =>
i1.GeneratedColumn<int>(
'role',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<int> _column_17(String aliasedName) =>
i1.GeneratedColumn<int>(
'notify',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'joined_at',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<DateTime> _column_19(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'break_until',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<DateTime> _column_20(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'timeout_until',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<bool> _column_21(String aliasedName) =>
i1.GeneratedColumn<bool>(
'is_bot',
aliasedName,
false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_bot" IN (0, 1))',
),
);
i1.GeneratedColumn<String> _column_22(String aliasedName) =>
i1.GeneratedColumn<String>(
'status',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<DateTime> _column_23(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'last_typed',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get roomId =>
columnsByName['room_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get senderId =>
columnsByName['sender_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get nonce =>
columnsByName['nonce']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get data =>
columnsByName['data']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get status =>
columnsByName['status']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get isDeleted =>
columnsByName['is_deleted']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get meta =>
columnsByName['meta']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get membersMentioned =>
columnsByName['members_mentioned']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get editedAt =>
columnsByName['edited_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get attachments =>
columnsByName['attachments']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get reactions =>
columnsByName['reactions']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get repliedMessageId =>
columnsByName['replied_message_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get forwardedMessageId =>
columnsByName['forwarded_message_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_24(String aliasedName) =>
i1.GeneratedColumn<String>(
'room_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES chat_rooms (id)',
),
);
i1.GeneratedColumn<String> _column_25(String aliasedName) =>
i1.GeneratedColumn<String>(
'sender_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES chat_members (id)',
),
);
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
i1.GeneratedColumn<String>(
'content',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
i1.GeneratedColumn<String>(
'nonce',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_28(String aliasedName) =>
i1.GeneratedColumn<String>(
'data',
aliasedName,
false,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
i1.GeneratedColumn<int>(
'status',
aliasedName,
false,
type: i1.DriftSqlType.int,
);
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
i1.GeneratedColumn<bool>(
'is_deleted',
aliasedName,
true,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_deleted" IN (0, 1))',
),
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<DateTime> _column_31(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'updated_at',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
i1.GeneratedColumn<String>(
'type',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'text\''),
);
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
i1.GeneratedColumn<String>(
'meta',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'{}\''),
);
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
i1.GeneratedColumn<String>(
'members_mentioned',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'[]\''),
);
i1.GeneratedColumn<DateTime> _column_35(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'edited_at',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
i1.GeneratedColumn<String>(
'attachments',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'[]\''),
);
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
i1.GeneratedColumn<String>(
'reactions',
aliasedName,
false,
type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'[]\''),
);
i1.GeneratedColumn<String> _column_38(String aliasedName) =>
i1.GeneratedColumn<String>(
'replied_message_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
i1.GeneratedColumn<String>(
'forwarded_message_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get title =>
columnsByName['title']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get visibility =>
columnsByName['visibility']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get lastModified =>
columnsByName['last_modified']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get postData =>
columnsByName['post_data']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
i1.GeneratedColumn<String>(
'title',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i1.GeneratedColumn<int> _column_41(String aliasedName) =>
i1.GeneratedColumn<int>(
'visibility',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
i1.GeneratedColumn<int>(
'type',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<DateTime> _column_43(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'last_modified',
aliasedName,
false,
type: i1.DriftSqlType.dateTime,
);
i1.GeneratedColumn<String> _column_44(String aliasedName) =>
i1.GeneratedColumn<String>(
'post_data',
aliasedName,
false,
type: i1.DriftSqlType.string,
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
case 6:
final schema = Schema7(database: database);
final migrator = i1.Migrator(database, schema);
await from6To7(migrator, schema);
return 7;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
};
}
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(from6To7: from6To7),
);

231
lib/data/message.dart Normal file
View File

@@ -0,0 +1,231 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:island/chat/chat_models/chat.dart';
import 'package:island/drive/drive_models/file.dart';
class MapConverter extends TypeConverter<Map<String, dynamic>, String> {
const MapConverter();
@override
Map<String, dynamic> fromSql(String fromDb) => json.decode(fromDb);
@override
String toSql(Map<String, dynamic> value) => json.encode(value);
}
class ListStringConverter extends TypeConverter<List<String>, String> {
const ListStringConverter();
@override
List<String> fromSql(String fromDb) => List<String>.from(json.decode(fromDb));
@override
String toSql(List<String> value) => json.encode(value);
}
class ListMapConverter
extends TypeConverter<List<Map<String, dynamic>>, String> {
const ListMapConverter();
@override
List<Map<String, dynamic>> fromSql(String fromDb) =>
List<Map<String, dynamic>>.from(json.decode(fromDb));
@override
String toSql(List<Map<String, dynamic>> value) => json.encode(value);
}
class Realms extends Table {
TextColumn get id => text()();
TextColumn get slug => text()();
TextColumn get name => text().nullable()();
TextColumn get description => text().nullable()();
TextColumn get verifiedAs => text().nullable()();
DateTimeColumn get verifiedAt => dateTime().nullable()();
BoolColumn get isCommunity => boolean()();
BoolColumn get isPublic => boolean()();
TextColumn get picture => text().map(const MapConverter()).nullable()();
TextColumn get background => text().map(const MapConverter()).nullable()();
TextColumn get accountId => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class ChatRooms extends Table {
TextColumn get id => text()();
TextColumn get name => text().nullable()();
TextColumn get description => text().nullable()();
IntColumn get type => integer()();
BoolColumn get isPublic =>
boolean().nullable().withDefault(const Constant(false))();
BoolColumn get isCommunity =>
boolean().nullable().withDefault(const Constant(false))();
TextColumn get picture => text().map(const MapConverter()).nullable()();
TextColumn get background => text().map(const MapConverter()).nullable()();
TextColumn get realmId => text().references(Realms, #id).nullable()();
TextColumn get accountId => text().nullable()();
BoolColumn get isPinned =>
boolean().nullable().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class ChatMembers extends Table {
TextColumn get id => text()();
TextColumn get chatRoomId => text().references(ChatRooms, #id)();
TextColumn get accountId => text()();
TextColumn get account => text().map(const MapConverter())();
TextColumn get nick => text().nullable()();
IntColumn get notify => integer()();
DateTimeColumn get joinedAt => dateTime().nullable()();
DateTimeColumn get breakUntil => dateTime().nullable()();
DateTimeColumn get timeoutUntil => dateTime().nullable()();
DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class ChatMessages extends Table {
TextColumn get id => text()();
TextColumn get roomId => text().references(ChatRooms, #id)();
TextColumn get senderId => text().references(ChatMembers, #id)();
TextColumn get content => text().nullable()();
TextColumn get nonce => text().nullable()();
TextColumn get data => text()();
DateTimeColumn get createdAt => dateTime()();
IntColumn get status => intEnum<MessageStatus>()();
BoolColumn get isDeleted =>
boolean().nullable().withDefault(const Constant(false))();
DateTimeColumn get updatedAt => dateTime().nullable()();
DateTimeColumn get deletedAt => dateTime().nullable()();
TextColumn get type => text().withDefault(const Constant('text'))();
TextColumn get meta =>
text().map(const MapConverter()).withDefault(const Constant('{}'))();
TextColumn get membersMentioned => text()
.map(const ListStringConverter())
.withDefault(const Constant('[]'))();
DateTimeColumn get editedAt => dateTime().nullable()();
TextColumn get attachments =>
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
TextColumn get reactions =>
text().map(const ListMapConverter()).withDefault(const Constant('[]'))();
TextColumn get repliedMessageId => text().nullable()();
TextColumn get forwardedMessageId => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}
class LocalChatMessage {
final String id;
final String roomId;
final String senderId;
final SnChatMember? sender;
final Map<String, dynamic> data;
final DateTime createdAt;
MessageStatus status;
final String? nonce;
final String? content;
final bool? isDeleted;
final DateTime? updatedAt;
final DateTime? deletedAt;
final String type;
final Map<String, dynamic> meta;
final List<String> membersMentioned;
final DateTime? editedAt;
final List<Map<String, dynamic>> attachments;
final List<Map<String, dynamic>> reactions;
final String? repliedMessageId;
final String? forwardedMessageId;
List<UniversalFile>? localAttachments;
LocalChatMessage({
required this.id,
required this.roomId,
required this.senderId,
required this.sender,
required this.data,
required this.createdAt,
required this.nonce,
required this.status,
this.content,
this.isDeleted,
this.updatedAt,
this.deletedAt,
required this.type,
required this.meta,
required this.membersMentioned,
this.editedAt,
required this.attachments,
required this.reactions,
this.repliedMessageId,
this.forwardedMessageId,
this.localAttachments,
});
SnChatMessage toRemoteMessage() {
if (sender == null) {
throw Exception('Cannot create remote message without sender');
}
final msgData = Map<String, dynamic>.from(data);
msgData['sender'] = sender!.toJson();
return SnChatMessage.fromJson(msgData);
}
static LocalChatMessage fromRemoteMessage(
SnChatMessage message,
MessageStatus status, {
String? nonce,
}) {
final jsonData = message.toJson();
jsonData.remove('sender');
// Ensure proper defaults for collections to prevent type cast errors
if (jsonData['meta'] == null) jsonData['meta'] = <String, dynamic>{};
if (jsonData['members_mentioned'] == null) {
jsonData['members_mentioned'] = <String>[];
}
if (jsonData['attachments'] == null) {
jsonData['attachments'] = <Map<String, dynamic>>[];
}
if (jsonData['reactions'] == null) {
jsonData['reactions'] = <Map<String, dynamic>>[];
}
final msgData = Map<String, dynamic>.from(jsonData);
return LocalChatMessage(
id: message.id,
roomId: message.chatRoomId,
senderId: message.senderId,
sender: message.sender,
data: msgData,
createdAt: message.createdAt,
status: status,
nonce: nonce ?? message.nonce,
content: message.content,
isDeleted: false,
updatedAt: message.updatedAt,
deletedAt: null,
type: message.type,
meta: message.meta,
membersMentioned: message.membersMentioned,
editedAt: message.editedAt,
attachments: message.attachments.map((e) => e.toJson()).toList(),
reactions: message.reactions.map((e) => e.toJson()).toList(),
repliedMessageId: message.repliedMessageId,
forwardedMessageId: message.forwardedMessageId,
);
}
}
enum MessageStatus { pending, sent, failed }