diff --git a/lib/database/drift_db.dart b/lib/database/drift_db.dart index b7e86fde..1dafda0e 100644 --- a/lib/database/drift_db.dart +++ b/lib/database/drift_db.dart @@ -5,16 +5,19 @@ import 'package:island/database/draft.dart'; import 'package:island/models/account.dart'; import 'package:island/models/chat.dart'; import 'package:island/models/post.dart'; +import 'package:island/models/realm.dart'; part 'drift_db.g.dart'; // Define the database -@DriftDatabase(tables: [ChatRooms, ChatMembers, ChatMessages, PostDrafts]) +@DriftDatabase( + tables: [Realms, ChatRooms, ChatMembers, ChatMessages, PostDrafts], +) class AppDatabase extends _$AppDatabase { AppDatabase(super.e); @override - int get schemaVersion => 9; + int get schemaVersion => 10; @override MigrationStrategy get migration => MigrationStrategy( @@ -71,6 +74,11 @@ class AppDatabase extends _$AppDatabase { '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 + } }, ); @@ -92,11 +100,10 @@ class AppDatabase extends _$AppDatabase { // Migrate existing data if any try { - final oldDrafts = - await customSelect( - 'SELECT id, post, lastModified FROM post_drafts_old', - readsFrom: {postDrafts}, - ).get(); + final oldDrafts = await customSelect( + 'SELECT id, post, lastModified FROM post_drafts_old', + readsFrom: {postDrafts}, + ).get(); for (final row in oldDrafts) { final postJson = row.read('post'); @@ -150,9 +157,9 @@ class AppDatabase extends _$AppDatabase { } Future updateMessageStatus(String id, MessageStatus status) { - return (update(chatMessages)..where( - (m) => m.id.equals(id), - )).write(ChatMessagesCompanion(status: Value(status))); + return (update(chatMessages)..where((m) => m.id.equals(id))).write( + ChatMessagesCompanion(status: Value(status)), + ); } Future deleteMessage(String id) { @@ -176,29 +183,28 @@ class AppDatabase extends _$AppDatabase { 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), - ); + 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()); + 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(); + final messageFutures = messages + .map((msg) => companionToMessage(msg, fetchAccount: fetchAccount)) + .toList(); return await Future.wait(messageFutures); } @@ -234,9 +240,9 @@ class AppDatabase extends _$AppDatabase { final data = jsonDecode(dbMessage.data); SnChatMember? sender; try { - final senderRow = - await (select(chatMembers) - ..where((m) => m.id.equals(dbMessage.senderId))).getSingle(); + final senderRow = await (select( + chatMembers, + )..where((m) => m.id.equals(dbMessage.senderId))).getSingle(); SnAccount senderAccount; senderAccount = SnAccount.fromJson(senderRow.account); @@ -358,6 +364,20 @@ class AppDatabase extends _$AppDatabase { ); } + RealmsCompanion companionFromRealm(SnRealm realm) { + return RealmsCompanion( + id: Value(realm.id), + name: Value(realm.name), + description: Value(realm.description), + 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 saveChatRooms( List rooms, { bool override = false, @@ -373,17 +393,35 @@ class AppDatabase extends _$AppDatabase { if (idsToRemove.isNotEmpty) { final idsList = idsToRemove.toList(); // Remove messages - await (delete(chatMessages) - ..where((t) => t.roomId.isIn(idsList))).go(); + await (delete( + chatMessages, + )..where((t) => t.roomId.isIn(idsList))).go(); // Remove members - await (delete(chatMembers) - ..where((t) => t.chatRoomId.isIn(idsList))).go(); + await (delete( + chatMembers, + )..where((t) => t.chatRoomId.isIn(idsList))).go(); // Remove rooms await (delete(chatRooms)..where((t) => t.id.isIn(idsList))).go(); } } - // 2. Upsert remote rooms + // 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) { for (final room in rooms) { batch.insert( @@ -445,8 +483,9 @@ class AppDatabase extends _$AppDatabase { } Future getPostDraftById(String id) async { - return await (select(postDrafts) - ..where((tbl) => tbl.id.equals(id))).getSingleOrNull(); + return await (select( + postDrafts, + )..where((tbl) => tbl.id.equals(id))).getSingleOrNull(); } Future saveMember(SnChatMember member) async { diff --git a/lib/database/drift_db.g.dart b/lib/database/drift_db.g.dart index 75cddb9f..3ea2f691 100644 --- a/lib/database/drift_db.g.dart +++ b/lib/database/drift_db.g.dart @@ -3,6 +3,581 @@ part of 'drift_db.dart'; // ignore_for_file: type=lint +class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $RealmsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _descriptionMeta = const VerificationMeta( + 'description', + ); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> + picture = GeneratedColumn( + 'picture', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($RealmsTable.$converterpicturen); + @override + late final GeneratedColumnWithTypeConverter?, String> + background = GeneratedColumn( + 'background', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($RealmsTable.$converterbackgroundn); + static const VerificationMeta _accountIdMeta = const VerificationMeta( + 'accountId', + ); + @override + late final GeneratedColumn accountId = GeneratedColumn( + 'account_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _updatedAtMeta = const VerificationMeta( + 'updatedAt', + ); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta( + 'deletedAt', + ); + @override + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + description, + picture, + background, + accountId, + createdAt, + updatedAt, + deletedAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'realms'; + @override + VerificationContext validateIntegrity( + Insertable 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('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, + _descriptionMeta, + ), + ); + } + if (data.containsKey('account_id')) { + context.handle( + _accountIdMeta, + accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + if (data.containsKey('updated_at')) { + context.handle( + _updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta), + ); + } else if (isInserting) { + context.missing(_updatedAtMeta); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + Realm map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return Realm( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + picture: $RealmsTable.$converterpicturen.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}picture'], + ), + ), + background: $RealmsTable.$converterbackgroundn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}background'], + ), + ), + accountId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}account_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @override + $RealmsTable createAlias(String alias) { + return $RealmsTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converterpicture = + const MapConverter(); + static TypeConverter?, String?> $converterpicturen = + NullAwareTypeConverter.wrap($converterpicture); + static TypeConverter, String> $converterbackground = + const MapConverter(); + static TypeConverter?, String?> $converterbackgroundn = + NullAwareTypeConverter.wrap($converterbackground); +} + +class Realm extends DataClass implements Insertable { + final String id; + final String? name; + final String? description; + final Map? picture; + final Map? background; + final String? accountId; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + const Realm({ + required this.id, + this.name, + this.description, + this.picture, + this.background, + this.accountId, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || name != null) { + map['name'] = Variable(name); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || picture != null) { + map['picture'] = Variable( + $RealmsTable.$converterpicturen.toSql(picture), + ); + } + if (!nullToAbsent || background != null) { + map['background'] = Variable( + $RealmsTable.$converterbackgroundn.toSql(background), + ); + } + if (!nullToAbsent || accountId != null) { + map['account_id'] = Variable(accountId); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + return map; + } + + RealmsCompanion toCompanion(bool nullToAbsent) { + return RealmsCompanion( + id: Value(id), + name: name == null && nullToAbsent ? const Value.absent() : Value(name), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + picture: picture == null && nullToAbsent + ? const Value.absent() + : Value(picture), + background: background == null && nullToAbsent + ? const Value.absent() + : Value(background), + accountId: accountId == null && nullToAbsent + ? const Value.absent() + : Value(accountId), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt), + deletedAt: deletedAt == null && nullToAbsent + ? const Value.absent() + : Value(deletedAt), + ); + } + + factory Realm.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return Realm( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + picture: serializer.fromJson?>(json['picture']), + background: serializer.fromJson?>( + json['background'], + ), + accountId: serializer.fromJson(json['accountId']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'picture': serializer.toJson?>(picture), + 'background': serializer.toJson?>(background), + 'accountId': serializer.toJson(accountId), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + Realm copyWith({ + String? id, + Value name = const Value.absent(), + Value description = const Value.absent(), + Value?> picture = const Value.absent(), + Value?> background = const Value.absent(), + Value accountId = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + }) => Realm( + id: id ?? this.id, + name: name.present ? name.value : this.name, + description: description.present ? description.value : this.description, + picture: picture.present ? picture.value : this.picture, + background: background.present ? background.value : this.background, + accountId: accountId.present ? accountId.value : this.accountId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + Realm copyWithCompanion(RealmsCompanion data) { + return Realm( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + picture: data.picture.present ? data.picture.value : this.picture, + background: data.background.present + ? data.background.value + : this.background, + accountId: data.accountId.present ? data.accountId.value : this.accountId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @override + String toString() { + return (StringBuffer('Realm(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('picture: $picture, ') + ..write('background: $background, ') + ..write('accountId: $accountId, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + picture, + background, + accountId, + createdAt, + updatedAt, + deletedAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Realm && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.picture == this.picture && + other.background == this.background && + other.accountId == this.accountId && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt); +} + +class RealmsCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value?> picture; + final Value?> background; + final Value accountId; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value rowid; + const RealmsCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.picture = const Value.absent(), + this.background = const Value.absent(), + this.accountId = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + RealmsCompanion.insert({ + required String id, + this.name = const Value.absent(), + this.description = const Value.absent(), + this.picture = const Value.absent(), + this.background = const Value.absent(), + this.accountId = const Value.absent(), + required DateTime createdAt, + required DateTime updatedAt, + this.deletedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + createdAt = Value(createdAt), + updatedAt = Value(updatedAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? picture, + Expression? background, + Expression? accountId, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (picture != null) 'picture': picture, + if (background != null) 'background': background, + if (accountId != null) 'account_id': accountId, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (rowid != null) 'rowid': rowid, + }); + } + + RealmsCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value?>? picture, + Value?>? background, + Value? accountId, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? rowid, + }) { + return RealmsCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + picture: picture ?? this.picture, + background: background ?? this.background, + accountId: accountId ?? this.accountId, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (picture.present) { + map['picture'] = Variable( + $RealmsTable.$converterpicturen.toSql(picture.value), + ); + } + if (background.present) { + map['background'] = Variable( + $RealmsTable.$converterbackgroundn.toSql(background.value), + ); + } + if (accountId.present) { + map['account_id'] = Variable(accountId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RealmsCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('picture: $picture, ') + ..write('background: $background, ') + ..write('accountId: $accountId, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class $ChatRoomsTable extends ChatRooms with TableInfo<$ChatRoomsTable, ChatRoom> { @override @@ -105,6 +680,9 @@ class $ChatRoomsTable extends ChatRooms true, type: DriftSqlType.string, requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES realms (id)', + ), ); static const VerificationMeta _accountIdMeta = const VerificationMeta( 'accountId', @@ -3120,6 +3698,7 @@ class PostDraftsCompanion extends UpdateCompanion { abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $RealmsTable realms = $RealmsTable(this); late final $ChatRoomsTable chatRooms = $ChatRoomsTable(this); late final $ChatMembersTable chatMembers = $ChatMembersTable(this); late final $ChatMessagesTable chatMessages = $ChatMessagesTable(this); @@ -3129,6 +3708,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ + realms, chatRooms, chatMembers, chatMessages, @@ -3136,6 +3716,388 @@ abstract class _$AppDatabase extends GeneratedDatabase { ]; } +typedef $$RealmsTableCreateCompanionBuilder = + RealmsCompanion Function({ + required String id, + Value name, + Value description, + Value?> picture, + Value?> background, + Value accountId, + required DateTime createdAt, + required DateTime updatedAt, + Value deletedAt, + Value rowid, + }); +typedef $$RealmsTableUpdateCompanionBuilder = + RealmsCompanion Function({ + Value id, + Value name, + Value description, + Value?> picture, + Value?> background, + Value accountId, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value rowid, + }); + +final class $$RealmsTableReferences + extends BaseReferences<_$AppDatabase, $RealmsTable, Realm> { + $$RealmsTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$ChatRoomsTable, List> + _chatRoomsRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.chatRooms, + aliasName: $_aliasNameGenerator(db.realms.id, db.chatRooms.realmId), + ); + + $$ChatRoomsTableProcessedTableManager get chatRoomsRefs { + final manager = $$ChatRoomsTableTableManager( + $_db, + $_db.chatRooms, + ).filter((f) => f.realmId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_chatRoomsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$RealmsTableFilterComposer + extends Composer<_$AppDatabase, $RealmsTable> { + $$RealmsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnFilters(column), + ); + + ColumnWithTypeConverterFilters< + Map?, + Map, + String + > + get picture => $composableBuilder( + column: $table.picture, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnWithTypeConverterFilters< + Map?, + Map, + String + > + get background => $composableBuilder( + column: $table.background, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnFilters get accountId => $composableBuilder( + column: $table.accountId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnFilters(column), + ); + + Expression chatRoomsRefs( + Expression Function($$ChatRoomsTableFilterComposer f) f, + ) { + final $$ChatRoomsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.chatRooms, + getReferencedColumn: (t) => t.realmId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ChatRoomsTableFilterComposer( + $db: $db, + $table: $db.chatRooms, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$RealmsTableOrderingComposer + extends Composer<_$AppDatabase, $RealmsTable> { + $$RealmsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get picture => $composableBuilder( + column: $table.picture, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get background => $composableBuilder( + column: $table.background, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get accountId => $composableBuilder( + column: $table.accountId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$RealmsTableAnnotationComposer + extends Composer<_$AppDatabase, $RealmsTable> { + $$RealmsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, + builder: (column) => column, + ); + + GeneratedColumnWithTypeConverter?, String> get picture => + $composableBuilder(column: $table.picture, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> + get background => $composableBuilder( + column: $table.background, + builder: (column) => column, + ); + + GeneratedColumn get accountId => + $composableBuilder(column: $table.accountId, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + Expression chatRoomsRefs( + Expression Function($$ChatRoomsTableAnnotationComposer a) f, + ) { + final $$ChatRoomsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.chatRooms, + getReferencedColumn: (t) => t.realmId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$ChatRoomsTableAnnotationComposer( + $db: $db, + $table: $db.chatRooms, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$RealmsTableTableManager + extends + RootTableManager< + _$AppDatabase, + $RealmsTable, + Realm, + $$RealmsTableFilterComposer, + $$RealmsTableOrderingComposer, + $$RealmsTableAnnotationComposer, + $$RealmsTableCreateCompanionBuilder, + $$RealmsTableUpdateCompanionBuilder, + (Realm, $$RealmsTableReferences), + Realm, + PrefetchHooks Function({bool chatRoomsRefs}) + > { + $$RealmsTableTableManager(_$AppDatabase db, $RealmsTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$RealmsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$RealmsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$RealmsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value description = const Value.absent(), + Value?> picture = const Value.absent(), + Value?> background = const Value.absent(), + Value accountId = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => RealmsCompanion( + id: id, + name: name, + description: description, + picture: picture, + background: background, + accountId: accountId, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + Value name = const Value.absent(), + Value description = const Value.absent(), + Value?> picture = const Value.absent(), + Value?> background = const Value.absent(), + Value accountId = const Value.absent(), + required DateTime createdAt, + required DateTime updatedAt, + Value deletedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => RealmsCompanion.insert( + id: id, + name: name, + description: description, + picture: picture, + background: background, + accountId: accountId, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => + (e.readTable(table), $$RealmsTableReferences(db, table, e)), + ) + .toList(), + prefetchHooksCallback: ({chatRoomsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [if (chatRoomsRefs) db.chatRooms], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (chatRoomsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$RealmsTableReferences + ._chatRoomsRefsTable(db), + managerFromTypedResult: (p0) => + $$RealmsTableReferences(db, table, p0).chatRoomsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where((e) => e.realmId == item.id), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$RealmsTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $RealmsTable, + Realm, + $$RealmsTableFilterComposer, + $$RealmsTableOrderingComposer, + $$RealmsTableAnnotationComposer, + $$RealmsTableCreateCompanionBuilder, + $$RealmsTableUpdateCompanionBuilder, + (Realm, $$RealmsTableReferences), + Realm, + PrefetchHooks Function({bool chatRoomsRefs}) + >; typedef $$ChatRoomsTableCreateCompanionBuilder = ChatRoomsCompanion Function({ required String id, @@ -3175,6 +4137,24 @@ final class $$ChatRoomsTableReferences extends BaseReferences<_$AppDatabase, $ChatRoomsTable, ChatRoom> { $$ChatRoomsTableReferences(super.$_db, super.$_table, super.$_typedResult); + static $RealmsTable _realmIdTable(_$AppDatabase db) => db.realms.createAlias( + $_aliasNameGenerator(db.chatRooms.realmId, db.realms.id), + ); + + $$RealmsTableProcessedTableManager? get realmId { + final $_column = $_itemColumn('realm_id'); + if ($_column == null) return null; + final manager = $$RealmsTableTableManager( + $_db, + $_db.realms, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_realmIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + static MultiTypedResultKey<$ChatMembersTable, List> _chatMembersRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( db.chatMembers, @@ -3271,11 +4251,6 @@ class $$ChatRoomsTableFilterComposer builder: (column) => ColumnWithTypeConverterFilters(column), ); - ColumnFilters get realmId => $composableBuilder( - column: $table.realmId, - builder: (column) => ColumnFilters(column), - ); - ColumnFilters get accountId => $composableBuilder( column: $table.accountId, builder: (column) => ColumnFilters(column), @@ -3296,6 +4271,29 @@ class $$ChatRoomsTableFilterComposer builder: (column) => ColumnFilters(column), ); + $$RealmsTableFilterComposer get realmId { + final $$RealmsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.realmId, + referencedTable: $db.realms, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RealmsTableFilterComposer( + $db: $db, + $table: $db.realms, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + Expression chatMembersRefs( Expression Function($$ChatMembersTableFilterComposer f) f, ) { @@ -3396,11 +4394,6 @@ class $$ChatRoomsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get realmId => $composableBuilder( - column: $table.realmId, - builder: (column) => ColumnOrderings(column), - ); - ColumnOrderings get accountId => $composableBuilder( column: $table.accountId, builder: (column) => ColumnOrderings(column), @@ -3420,6 +4413,29 @@ class $$ChatRoomsTableOrderingComposer column: $table.deletedAt, builder: (column) => ColumnOrderings(column), ); + + $$RealmsTableOrderingComposer get realmId { + final $$RealmsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.realmId, + referencedTable: $db.realms, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RealmsTableOrderingComposer( + $db: $db, + $table: $db.realms, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } } class $$ChatRoomsTableAnnotationComposer @@ -3462,9 +4478,6 @@ class $$ChatRoomsTableAnnotationComposer builder: (column) => column, ); - GeneratedColumn get realmId => - $composableBuilder(column: $table.realmId, builder: (column) => column); - GeneratedColumn get accountId => $composableBuilder(column: $table.accountId, builder: (column) => column); @@ -3477,6 +4490,29 @@ class $$ChatRoomsTableAnnotationComposer GeneratedColumn get deletedAt => $composableBuilder(column: $table.deletedAt, builder: (column) => column); + $$RealmsTableAnnotationComposer get realmId { + final $$RealmsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.realmId, + referencedTable: $db.realms, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$RealmsTableAnnotationComposer( + $db: $db, + $table: $db.realms, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + Expression chatMembersRefs( Expression Function($$ChatMembersTableAnnotationComposer a) f, ) { @@ -3541,7 +4577,11 @@ class $$ChatRoomsTableTableManager $$ChatRoomsTableUpdateCompanionBuilder, (ChatRoom, $$ChatRoomsTableReferences), ChatRoom, - PrefetchHooks Function({bool chatMembersRefs, bool chatMessagesRefs}) + PrefetchHooks Function({ + bool realmId, + bool chatMembersRefs, + bool chatMessagesRefs, + }) > { $$ChatRoomsTableTableManager(_$AppDatabase db, $ChatRoomsTable table) : super( @@ -3627,14 +4667,49 @@ class $$ChatRoomsTableTableManager ) .toList(), prefetchHooksCallback: - ({chatMembersRefs = false, chatMessagesRefs = false}) { + ({ + realmId = false, + chatMembersRefs = false, + chatMessagesRefs = false, + }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (chatMembersRefs) db.chatMembers, if (chatMessagesRefs) db.chatMessages, ], - addJoins: null, + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (realmId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.realmId, + referencedTable: $$ChatRoomsTableReferences + ._realmIdTable(db), + referencedColumn: $$ChatRoomsTableReferences + ._realmIdTable(db) + .id, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return [ if (chatMembersRefs) @@ -3699,7 +4774,11 @@ typedef $$ChatRoomsTableProcessedTableManager = $$ChatRoomsTableUpdateCompanionBuilder, (ChatRoom, $$ChatRoomsTableReferences), ChatRoom, - PrefetchHooks Function({bool chatMembersRefs, bool chatMessagesRefs}) + PrefetchHooks Function({ + bool realmId, + bool chatMembersRefs, + bool chatMessagesRefs, + }) >; typedef $$ChatMembersTableCreateCompanionBuilder = ChatMembersCompanion Function({ @@ -5229,6 +6308,8 @@ typedef $$PostDraftsTableProcessedTableManager = class $AppDatabaseManager { final _$AppDatabase _db; $AppDatabaseManager(this._db); + $$RealmsTableTableManager get realms => + $$RealmsTableTableManager(_db, _db.realms); $$ChatRoomsTableTableManager get chatRooms => $$ChatRoomsTableTableManager(_db, _db.chatRooms); $$ChatMembersTableTableManager get chatMembers => diff --git a/lib/database/message.dart b/lib/database/message.dart index 578d046d..5c2600c8 100644 --- a/lib/database/message.dart +++ b/lib/database/message.dart @@ -36,6 +36,21 @@ class ListMapConverter String toSql(List> value) => json.encode(value); } +class Realms extends Table { + TextColumn get id => text()(); + TextColumn get name => text().nullable()(); + TextColumn get description => text().nullable()(); + 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 get primaryKey => {id}; +} + class ChatRooms extends Table { TextColumn get id => text()(); TextColumn get name => text().nullable()(); @@ -47,7 +62,7 @@ class ChatRooms extends Table { 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().nullable()(); + TextColumn get realmId => text().references(Realms, #id).nullable()(); TextColumn get accountId => text().nullable()(); DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get updatedAt => dateTime()(); @@ -91,10 +106,9 @@ class ChatMessages extends Table { 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('[]'))(); + 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('[]'))(); diff --git a/lib/pods/chat/chat_room.dart b/lib/pods/chat/chat_room.dart index 0ab4397f..a9524865 100644 --- a/lib/pods/chat/chat_room.dart +++ b/lib/pods/chat/chat_room.dart @@ -4,6 +4,7 @@ import 'package:island/database/drift_db.dart'; import 'package:island/models/account.dart'; import 'package:island/models/chat.dart'; import 'package:island/models/file.dart'; +import 'package:island/models/realm.dart'; import 'package:island/pods/database.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; @@ -49,96 +50,10 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { if (localRoomsData.isNotEmpty) { final localRooms = await Future.wait( localRoomsData.map((row) async { - final membersRows = - await (db.select(db.chatMembers) - ..where((m) => m.chatRoomId.equals(row.id))).get(); - final members = - membersRows.map((mRow) { - final account = SnAccount.fromJson(mRow.account); - return SnChatMember( - id: mRow.id, - chatRoomId: mRow.chatRoomId, - accountId: mRow.accountId, - account: account, - nick: mRow.nick, - notify: mRow.notify, - joinedAt: mRow.joinedAt, - breakUntil: mRow.breakUntil, - timeoutUntil: mRow.timeoutUntil, - status: null, - createdAt: mRow.createdAt, - updatedAt: mRow.updatedAt, - deletedAt: mRow.deletedAt, - chatRoom: null, - ); - }).toList(); - return SnChatRoom( - id: row.id, - name: row.name, - description: row.description, - type: row.type, - isPublic: row.isPublic!, - isCommunity: row.isCommunity!, - picture: - row.picture != null - ? SnCloudFile.fromJson(row.picture!) - : null, - background: - row.background != null - ? SnCloudFile.fromJson(row.background!) - : null, - realmId: row.realmId, - accountId: row.accountId, - realm: null, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - deletedAt: row.deletedAt, - members: members, - ); - }), - ); - - // Background sync - Future(() async { - try { - final client = ref.read(apiClientProvider); - final resp = await client.get('/sphere/chat'); - final remoteRooms = - resp.data - .map((e) => SnChatRoom.fromJson(e)) - .cast() - .toList(); - await db.saveChatRooms(remoteRooms, override: true); - // Update state with fresh data - state = AsyncData(await _buildRoomsFromDb(db)); - } catch (_) {} - }).ignore(); - - return localRooms; - } - } catch (_) {} - - // Fallback to API - final client = ref.watch(apiClientProvider); - final resp = await client.get('/sphere/chat'); - final rooms = - resp.data - .map((e) => SnChatRoom.fromJson(e)) - .cast() - .toList(); - await db.saveChatRooms(rooms, override: true); - return rooms; - } - - Future> _buildRoomsFromDb(AppDatabase db) async { - final localRoomsData = await db.select(db.chatRooms).get(); - return Future.wait( - localRoomsData.map((row) async { - final membersRows = - await (db.select(db.chatMembers) - ..where((m) => m.chatRoomId.equals(row.id))).get(); - final members = - membersRows.map((mRow) { + final membersRows = await (db.select( + db.chatMembers, + )..where((m) => m.chatRoomId.equals(row.id))).get(); + final members = membersRows.map((mRow) { final account = SnAccount.fromJson(mRow.account); return SnChatMember( id: mRow.id, @@ -157,6 +72,121 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { chatRoom: null, ); }).toList(); + return SnChatRoom( + id: row.id, + name: row.name, + description: row.description, + type: row.type, + isPublic: row.isPublic!, + isCommunity: row.isCommunity!, + picture: row.picture != null + ? SnCloudFile.fromJson(row.picture!) + : null, + background: row.background != null + ? SnCloudFile.fromJson(row.background!) + : null, + realmId: row.realmId, + accountId: row.accountId, + realm: null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + deletedAt: row.deletedAt, + members: members, + ); + }), + ); + + // Background sync + Future(() async { + try { + final client = ref.read(apiClientProvider); + final resp = await client.get('/sphere/chat'); + final remoteRooms = resp.data + .map((e) => SnChatRoom.fromJson(e)) + .cast() + .toList(); + await db.saveChatRooms(remoteRooms, override: true); + // Update state with fresh data + state = AsyncData(await _buildRoomsFromDb(db)); + } catch (_) {} + }).ignore(); + + return localRooms; + } + } catch (_) {} + + // Fallback to API + final client = ref.watch(apiClientProvider); + final resp = await client.get('/sphere/chat'); + final rooms = resp.data + .map((e) => SnChatRoom.fromJson(e)) + .cast() + .toList(); + await db.saveChatRooms(rooms, override: true); + return rooms; + } + + Future> _buildRoomsFromDb(AppDatabase db) async { + final localRoomsData = await db.select(db.chatRooms).get(); + return Future.wait( + localRoomsData.map((row) async { + final membersRows = await (db.select( + db.chatMembers, + )..where((m) => m.chatRoomId.equals(row.id))).get(); + final members = membersRows.map((mRow) { + final account = SnAccount.fromJson(mRow.account); + return SnChatMember( + id: mRow.id, + chatRoomId: mRow.chatRoomId, + accountId: mRow.accountId, + account: account, + nick: mRow.nick, + notify: mRow.notify, + joinedAt: mRow.joinedAt, + breakUntil: mRow.breakUntil, + timeoutUntil: mRow.timeoutUntil, + status: null, + createdAt: mRow.createdAt, + updatedAt: mRow.updatedAt, + deletedAt: mRow.deletedAt, + chatRoom: null, + ); + }).toList(); + + // Load realm if it exists + SnRealm? realm; + if (row.realmId != null) { + try { + final realmRow = await (db.select( + db.realms, + )..where((r) => r.id.equals(row.realmId!))).getSingleOrNull(); + if (realmRow != null) { + realm = SnRealm( + id: realmRow.id, + slug: '', // Not stored in DB + name: realmRow.name ?? '', + description: realmRow.description ?? '', + verifiedAs: null, // Not stored in DB + verifiedAt: null, // Not stored in DB + isCommunity: false, // Not stored in DB + isPublic: true, // Not stored in DB + picture: realmRow.picture != null + ? SnCloudFile.fromJson(realmRow.picture!) + : null, + background: realmRow.background != null + ? SnCloudFile.fromJson(realmRow.background!) + : null, + accountId: realmRow.accountId ?? '', + createdAt: realmRow.createdAt, + updatedAt: realmRow.updatedAt, + deletedAt: realmRow.deletedAt, + ); + } + } catch (_) { + // Realm not found, keep as null + } + } + return SnChatRoom( id: row.id, name: row.name, @@ -164,15 +194,15 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { type: row.type, isPublic: row.isPublic!, isCommunity: row.isCommunity!, - picture: - row.picture != null ? SnCloudFile.fromJson(row.picture!) : null, - background: - row.background != null - ? SnCloudFile.fromJson(row.background!) - : null, + picture: row.picture != null + ? SnCloudFile.fromJson(row.picture!) + : null, + background: row.background != null + ? SnCloudFile.fromJson(row.background!) + : null, realmId: row.realmId, accountId: row.accountId, - realm: null, + realm: realm, createdAt: row.createdAt, updatedAt: row.updatedAt, deletedAt: row.deletedAt, @@ -192,35 +222,34 @@ class ChatRoomNotifier extends _$ChatRoomNotifier { try { // Try to get from local database first - final localRoomData = - await (db.select(db.chatRooms) - ..where((r) => r.id.equals(identifier))).getSingleOrNull(); + final localRoomData = await (db.select( + db.chatRooms, + )..where((r) => r.id.equals(identifier))).getSingleOrNull(); if (localRoomData != null) { // Fetch members for this room - final membersRows = - await (db.select(db.chatMembers) - ..where((m) => m.chatRoomId.equals(localRoomData.id))).get(); - final members = - membersRows.map((mRow) { - final account = SnAccount.fromJson(mRow.account); - return SnChatMember( - id: mRow.id, - chatRoomId: mRow.chatRoomId, - accountId: mRow.accountId, - account: account, - nick: mRow.nick, - notify: mRow.notify, - joinedAt: mRow.joinedAt, - breakUntil: mRow.breakUntil, - timeoutUntil: mRow.timeoutUntil, - status: null, - createdAt: mRow.createdAt, - updatedAt: mRow.updatedAt, - deletedAt: mRow.deletedAt, - chatRoom: null, - ); - }).toList(); + final membersRows = await (db.select( + db.chatMembers, + )..where((m) => m.chatRoomId.equals(localRoomData.id))).get(); + final members = membersRows.map((mRow) { + final account = SnAccount.fromJson(mRow.account); + return SnChatMember( + id: mRow.id, + chatRoomId: mRow.chatRoomId, + accountId: mRow.accountId, + account: account, + nick: mRow.nick, + notify: mRow.notify, + joinedAt: mRow.joinedAt, + breakUntil: mRow.breakUntil, + timeoutUntil: mRow.timeoutUntil, + status: null, + createdAt: mRow.createdAt, + updatedAt: mRow.updatedAt, + deletedAt: mRow.deletedAt, + chatRoom: null, + ); + }).toList(); final localRoom = SnChatRoom( id: localRoomData.id, @@ -229,14 +258,12 @@ class ChatRoomNotifier extends _$ChatRoomNotifier { type: localRoomData.type, isPublic: localRoomData.isPublic!, isCommunity: localRoomData.isCommunity!, - picture: - localRoomData.picture != null - ? SnCloudFile.fromJson(localRoomData.picture!) - : null, - background: - localRoomData.background != null - ? SnCloudFile.fromJson(localRoomData.background!) - : null, + picture: localRoomData.picture != null + ? SnCloudFile.fromJson(localRoomData.picture!) + : null, + background: localRoomData.background != null + ? SnCloudFile.fromJson(localRoomData.background!) + : null, realmId: localRoomData.realmId, accountId: localRoomData.accountId, realm: null, diff --git a/lib/pods/chat/chat_room.g.dart b/lib/pods/chat/chat_room.g.dart index 85ee0d41..3b31a92f 100644 --- a/lib/pods/chat/chat_room.g.dart +++ b/lib/pods/chat/chat_room.g.dart @@ -34,7 +34,7 @@ final class ChatRoomJoinedNotifierProvider } String _$chatRoomJoinedNotifierHash() => - r'c8092225ba0d9c08b2b5bca6f800f1877303b4ff'; + r'65961aac28b5188900c4b25308f6fd080a14d5ab'; abstract class _$ChatRoomJoinedNotifier extends $AsyncNotifier> { diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index a3d878d9..249e9bcd 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -14,208 +14,15 @@ import 'package:island/services/event_bus.dart'; import 'package:island/services/responsive.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; -import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/chat_room_widgets.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/navigation/fab_menu.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:relative_time/relative_time.dart'; -import 'package:skeletonizer/skeletonizer.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:island/pods/chat/chat_room.dart'; -class ChatRoomListTile extends HookConsumerWidget { - final SnChatRoom room; - final bool isDirect; - final Widget? subtitle; - final Widget? trailing; - final VoidCallback? onTap; - - const ChatRoomListTile({ - super.key, - required this.room, - this.isDirect = false, - this.subtitle, - this.trailing, - this.onTap, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final summary = ref - .watch(chatSummaryProvider) - .whenData((summaries) => summaries[room.id]); - - var validMembers = room.members ?? []; - if (validMembers.isNotEmpty) { - final userInfo = ref.watch(userInfoProvider); - if (userInfo.value != null) { - validMembers = validMembers - .where((e) => e.accountId != userInfo.value!.id) - .toList(); - } - } - - Widget buildSubtitle() { - if (subtitle != null) return subtitle!; - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - layoutBuilder: (currentChild, previousChildren) => Stack( - alignment: Alignment.centerLeft, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ), - child: summary.when( - data: (data) => Container( - key: const ValueKey('data'), - child: data == null - ? isDirect && room.description == null - ? Text( - validMembers - .map((e) => '@${e.account.name}') - .join(', '), - maxLines: 1, - ) - : Text( - room.description ?? 'descriptionNone'.tr(), - maxLines: 1, - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (data.unreadCount > 0) - Text( - 'unreadMessages'.plural(data.unreadCount), - style: Theme.of(context).textTheme.bodySmall - ?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - if (data.lastMessage == null) - Text( - room.description ?? 'descriptionNone'.tr(), - maxLines: 1, - ) - else - Row( - spacing: 4, - children: [ - Badge( - label: Text( - data.lastMessage!.sender.account.nick, - ), - textColor: Theme.of( - context, - ).colorScheme.onPrimary, - backgroundColor: Theme.of( - context, - ).colorScheme.primary, - ), - Expanded( - child: Text( - (data.lastMessage!.content?.isNotEmpty ?? false) - ? data.lastMessage!.content! - : 'messageNone'.tr(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - ), - Align( - alignment: Alignment.centerRight, - child: Text( - RelativeTime( - context, - ).format(data.lastMessage!.createdAt), - style: Theme.of(context).textTheme.bodySmall, - ), - ), - ], - ), - ], - ), - ), - loading: () => Container( - key: const ValueKey('loading'), - child: Builder( - builder: (context) { - final seed = DateTime.now().microsecondsSinceEpoch; - final len = 4 + (seed % 17); // 4..20 inclusive - const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; - var s = seed; - final buffer = StringBuffer(); - for (var i = 0; i < len; i++) { - s = (s * 1103515245 + 12345) & 0x7fffffff; - buffer.write(chars[s % chars.length]); - } - return Skeletonizer( - enabled: true, - child: Text(buffer.toString()), - ); - }, - ), - ), - error: (_, _) => Container( - key: const ValueKey('error'), - child: isDirect && room.description == null - ? Text( - validMembers.map((e) => '@${e.account.name}').join(', '), - maxLines: 1, - ) - : Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1), - ), - ), - ); - } - - String titleText; - if (isDirect && room.name == null) { - if (room.members?.isNotEmpty ?? false) { - titleText = validMembers.map((e) => e.account.nick).join(', '); - } else { - titleText = 'Direct Message'; - } - } else { - titleText = room.name ?? ''; - } - - return ListTile( - leading: Badge( - isLabelVisible: summary.when( - data: (data) => (data?.unreadCount ?? 0) > 0, - loading: () => false, - error: (_, _) => false, - ), - child: (isDirect && room.picture?.id == null) - ? SplitAvatarWidget( - filesId: validMembers - .map((e) => e.account.profile.picture?.id) - .toList(), - ) - : room.picture?.id == null - ? CircleAvatar(child: Text(room.name![0].toUpperCase())) - : ProfilePictureWidget(fileId: room.picture?.id), - ), - title: Text(titleText), - subtitle: buildSubtitle(), - trailing: trailing, // Add this line - onTap: () async { - // Clear unread count if there are unread messages - ref.read(chatSummaryProvider.future).then((summary) { - if ((summary[room.id]?.unreadCount ?? 0) > 0) { - ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id); - } - }); - onTap?.call(); - }, - ); - } -} - class ChatListBodyWidget extends HookConsumerWidget { final bool isFloating; final TabController tabController; @@ -648,3 +455,75 @@ class _ChatInvitesSheet extends HookConsumerWidget { ); } } + +class ChatRoomListTile extends HookConsumerWidget { + final SnChatRoom room; + final bool isDirect; + final Widget? subtitle; + final Widget? trailing; + final VoidCallback? onTap; + + const ChatRoomListTile({ + super.key, + required this.room, + this.isDirect = false, + this.subtitle, + this.trailing, + this.onTap, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final summary = ref + .watch(chatSummaryProvider) + .whenData((summaries) => summaries[room.id]); + + var validMembers = room.members ?? []; + if (validMembers.isNotEmpty) { + final userInfo = ref.watch(userInfoProvider); + if (userInfo.value != null) { + validMembers = validMembers + .where((e) => e.accountId != userInfo.value!.id) + .toList(); + } + } + + String titleText; + if (isDirect && room.name == null) { + if (room.members?.isNotEmpty ?? false) { + titleText = validMembers.map((e) => e.account.nick).join(', '); + } else { + titleText = 'Direct Message'; + } + } else { + titleText = room.name ?? ''; + } + + return ListTile( + leading: ChatRoomAvatar( + room: room, + isDirect: isDirect, + summary: summary, + validMembers: validMembers, + ), + title: Text(titleText), + subtitle: ChatRoomSubtitle( + room: room, + isDirect: isDirect, + validMembers: validMembers, + summary: summary, + subtitle: subtitle, + ), + trailing: trailing, // Add this line + onTap: () async { + // Clear unread count if there are unread messages + ref.read(chatSummaryProvider.future).then((summary) { + if ((summary[room.id]?.unreadCount ?? 0) > 0) { + ref.read(chatSummaryProvider.notifier).clearUnreadCount(room.id); + } + }); + onTap?.call(); + }, + ); + } +} diff --git a/lib/widgets/chat_room_widgets.dart b/lib/widgets/chat_room_widgets.dart new file mode 100644 index 00000000..58f890c8 --- /dev/null +++ b/lib/widgets/chat_room_widgets.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/chat.dart'; +import 'package:island/widgets/content/cloud_files.dart'; +import 'package:relative_time/relative_time.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class ChatRoomAvatar extends StatelessWidget { + final SnChatRoom room; + final bool isDirect; + final AsyncValue summary; + final List validMembers; + + const ChatRoomAvatar({ + super.key, + required this.room, + required this.isDirect, + required this.summary, + required this.validMembers, + }); + + @override + Widget build(BuildContext context) { + final avatarChild = (isDirect && room.picture?.id == null) + ? SplitAvatarWidget( + filesId: validMembers + .map((e) => e.account.profile.picture?.id) + .toList(), + ) + : room.picture?.id == null + ? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase())) + : ProfilePictureWidget(fileId: room.picture?.id); + + final badgeChild = Badge( + isLabelVisible: summary.when( + data: (data) => (data?.unreadCount ?? 0) > 0, + loading: () => false, + error: (_, _) => false, + ), + child: avatarChild, + ); + + // Show realm avatar as small overlay if chat belongs to a realm + if (room.realm != null) { + return Stack( + children: [ + badgeChild, + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipOval( + child: ProfilePictureWidget(file: room.realm!.picture), + ), + ), + ), + ], + ); + } + + return badgeChild; + } +} + +class ChatRoomSubtitle extends StatelessWidget { + final SnChatRoom room; + final bool isDirect; + final List validMembers; + final AsyncValue summary; + final Widget? subtitle; + + const ChatRoomSubtitle({ + super.key, + required this.room, + required this.isDirect, + required this.validMembers, + required this.summary, + this.subtitle, + }); + + @override + Widget build(BuildContext context) { + if (subtitle != null) return subtitle!; + + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + layoutBuilder: (currentChild, previousChildren) => Stack( + alignment: Alignment.centerLeft, + children: [...previousChildren, if (currentChild != null) currentChild], + ), + child: summary.when( + data: (data) => Container( + key: const ValueKey('data'), + child: data == null + ? isDirect && room.description == null + ? Text( + validMembers + .map((e) => '@${e.account.name}') + .join(', '), + maxLines: 1, + ) + : Text( + room.description ?? 'descriptionNone'.tr(), + maxLines: 1, + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (data.unreadCount > 0) + Text( + 'unreadMessages'.plural(data.unreadCount), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + if (data.lastMessage == null) + Text( + room.description ?? 'descriptionNone'.tr(), + maxLines: 1, + ) + else + Row( + spacing: 4, + children: [ + Badge( + label: Text(data.lastMessage!.sender.account.nick), + textColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of( + context, + ).colorScheme.primary, + ), + Expanded( + child: Text( + (data.lastMessage!.content?.isNotEmpty ?? false) + ? data.lastMessage!.content! + : 'messageNone'.tr(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + Align( + alignment: Alignment.centerRight, + child: Text( + RelativeTime( + context, + ).format(data.lastMessage!.createdAt), + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ], + ), + ], + ), + ), + loading: () => Container( + key: const ValueKey('loading'), + child: Builder( + builder: (context) { + final seed = DateTime.now().microsecondsSinceEpoch; + final len = 4 + (seed % 17); // 4..20 inclusive + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + var s = seed; + final buffer = StringBuffer(); + for (var i = 0; i < len; i++) { + s = (s * 1103515245 + 12345) & 0x7fffffff; + buffer.write(chars[s % chars.length]); + } + return Skeletonizer( + enabled: true, + child: Text(buffer.toString()), + ); + }, + ), + ), + error: (_, _) => Container( + key: const ValueKey('error'), + child: isDirect && room.description == null + ? Text( + validMembers.map((e) => '@${e.account.name}').join(', '), + maxLines: 1, + ) + : Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1), + ), + ), + ); + } +} diff --git a/lib/widgets/cmp/pattle.dart b/lib/widgets/cmp/pattle.dart index 28bea3f1..99136c28 100644 --- a/lib/widgets/cmp/pattle.dart +++ b/lib/widgets/cmp/pattle.dart @@ -14,7 +14,7 @@ import 'package:island/pods/chat/chat_summary.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/route.dart'; import 'package:island/services/responsive.dart'; -import 'package:island/widgets/content/cloud_files.dart'; +import 'package:island/widgets/chat_room_widgets.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:relative_time/relative_time.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -698,22 +698,11 @@ class _ChatRoomSearchResult extends HookConsumerWidget { borderRadius: const BorderRadius.all(Radius.circular(24)), ), child: ListTile( - leading: Badge( - isLabelVisible: summary.maybeWhen( - data: (data) => (data?.unreadCount ?? 0) > 0, - orElse: () => false, - ), - child: (isDirect && room.picture?.id == null) - ? SplitAvatarWidget( - filesId: validMembers - .map((e) => e.account.profile.picture?.id) - .toList(), - ) - : room.picture?.id == null - ? CircleAvatar(child: Text((room.name ?? 'DM')[0].toUpperCase())) - : ProfilePictureWidget( - fileId: room.picture?.id, - ), // Placeholder for now + leading: ChatRoomAvatar( + room: room, + isDirect: isDirect, + summary: summary, + validMembers: validMembers, ), title: Text(titleText), subtitle: buildSubtitle(), diff --git a/pubspec.lock b/pubspec.lock index 6092eb55..760501cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2635,6 +2635,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + snow_fall_animation: + dependency: "direct main" + description: + name: snow_fall_animation + sha256: "41b97ec1e5c47aeb5886924346f95b1b12e917b85b03d394c925e98a67d8f90e" + url: "https://pub.dev" + source: hosted + version: "0.0.1+3" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0003d06b..149d25cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -172,6 +172,7 @@ dependencies: hotkey_manager: ^0.2.3 shake: ^3.0.0 in_app_review: ^2.0.11 + snow_fall_animation: ^0.0.1+3 dev_dependencies: flutter_test: