diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 06bc175f..56ee690c 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -697,9 +697,9 @@ "articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.", "postVisibility": "Post Visibility", "currentMembershipMember": "A member of Stellar Program ยท {}", - "membershipPriceStellar": "1200 NSP per month, level 3+ required", - "membershipPriceNova": "2400 NSP per month, level 6+ required", - "membershipPriceSupernova": "3600 NSP per month, level 9+ required", + "membershipPriceStellar": "1200 NSP per month, level 20+ required", + "membershipPriceNova": "2400 NSP per month, level 40+ required", + "membershipPriceSupernova": "3600 NSP per month, level 60+ required", "sharePostPhoto": "Share Post as Photo", "wouldYouLikeToNavigateToChat": "Would You like to navigate to the chat?", "abuseReports": "Abuse Reports", @@ -1528,5 +1528,10 @@ "postTagsCategories": "Post Tags and Categories", "postTagsCategoriesDescription": "Browse posts by category and tags.", "debugLogs": "Debug Logs", - "debugLogsDescription": "View debug logs for troubleshooting." + "debugLogsDescription": "View debug logs for troubleshooting.", + "pinChatRoom": "Pin Chat Room", + "pinChatRoomDescription": "Pin this chat room to the top.", + "chatRoomPinned": "Chat room pinned successfully.", + "chatRoomUnpinned": "Chat room unpinned successfully.", + "pinnedChatRoom": "Pinned Rooms" } diff --git a/lib/database/drift_db.dart b/lib/database/drift_db.dart index 1dafda0e..1b57db3e 100644 --- a/lib/database/drift_db.dart +++ b/lib/database/drift_db.dart @@ -17,7 +17,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase(super.e); @override - int get schemaVersion => 10; + int get schemaVersion => 12; @override MigrationStrategy get migration => MigrationStrategy( @@ -79,6 +79,30 @@ class AppDatabase extends _$AppDatabase { 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', + ); + } }, ); @@ -341,6 +365,7 @@ class AppDatabase extends _$AppDatabase { 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), @@ -367,8 +392,13 @@ class AppDatabase extends _$AppDatabase { 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), @@ -422,11 +452,17 @@ class AppDatabase extends _$AppDatabase { }); // 3. Upsert remote rooms - await batch((batch) { + 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), + companionFromRoom(room).copyWith(isPinned: Value(isPinned)), mode: InsertMode.insertOrReplace, ); for (final member in room.members ?? []) { @@ -502,4 +538,16 @@ class AppDatabase extends _$AppDatabase { // Then save the message return await saveMessage(messageToCompanion(message)); } + + Future 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)), + ); + } + } } diff --git a/lib/database/drift_db.g.dart b/lib/database/drift_db.g.dart index 3ea2f691..74c8f0d8 100644 --- a/lib/database/drift_db.g.dart +++ b/lib/database/drift_db.g.dart @@ -17,6 +17,15 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { type: DriftSqlType.string, requiredDuringInsert: true, ); + static const VerificationMeta _slugMeta = const VerificationMeta('slug'); + @override + late final GeneratedColumn slug = GeneratedColumn( + 'slug', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( @@ -37,6 +46,56 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _verifiedAsMeta = const VerificationMeta( + 'verifiedAs', + ); + @override + late final GeneratedColumn verifiedAs = GeneratedColumn( + 'verified_as', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _verifiedAtMeta = const VerificationMeta( + 'verifiedAt', + ); + @override + late final GeneratedColumn verifiedAt = GeneratedColumn( + 'verified_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _isCommunityMeta = const VerificationMeta( + 'isCommunity', + ); + @override + late final GeneratedColumn isCommunity = GeneratedColumn( + 'is_community', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_community" IN (0, 1))', + ), + ); + static const VerificationMeta _isPublicMeta = const VerificationMeta( + 'isPublic', + ); + @override + late final GeneratedColumn isPublic = GeneratedColumn( + 'is_public', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_public" IN (0, 1))', + ), + ); @override late final GeneratedColumnWithTypeConverter?, String> picture = GeneratedColumn( @@ -102,8 +161,13 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { @override List get $columns => [ id, + slug, name, description, + verifiedAs, + verifiedAt, + isCommunity, + isPublic, picture, background, accountId, @@ -128,6 +192,14 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { } else if (isInserting) { context.missing(_idMeta); } + if (data.containsKey('slug')) { + context.handle( + _slugMeta, + slug.isAcceptableOrUnknown(data['slug']!, _slugMeta), + ); + } else if (isInserting) { + context.missing(_slugMeta); + } if (data.containsKey('name')) { context.handle( _nameMeta, @@ -143,6 +215,37 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { ), ); } + if (data.containsKey('verified_as')) { + context.handle( + _verifiedAsMeta, + verifiedAs.isAcceptableOrUnknown(data['verified_as']!, _verifiedAsMeta), + ); + } + if (data.containsKey('verified_at')) { + context.handle( + _verifiedAtMeta, + verifiedAt.isAcceptableOrUnknown(data['verified_at']!, _verifiedAtMeta), + ); + } + if (data.containsKey('is_community')) { + context.handle( + _isCommunityMeta, + isCommunity.isAcceptableOrUnknown( + data['is_community']!, + _isCommunityMeta, + ), + ); + } else if (isInserting) { + context.missing(_isCommunityMeta); + } + if (data.containsKey('is_public')) { + context.handle( + _isPublicMeta, + isPublic.isAcceptableOrUnknown(data['is_public']!, _isPublicMeta), + ); + } else if (isInserting) { + context.missing(_isPublicMeta); + } if (data.containsKey('account_id')) { context.handle( _accountIdMeta, @@ -184,6 +287,10 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { DriftSqlType.string, data['${effectivePrefix}id'], )!, + slug: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}slug'], + )!, name: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}name'], @@ -192,6 +299,22 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { DriftSqlType.string, data['${effectivePrefix}description'], ), + verifiedAs: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}verified_as'], + ), + verifiedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}verified_at'], + ), + isCommunity: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_community'], + )!, + isPublic: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_public'], + )!, picture: $RealmsTable.$converterpicturen.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, @@ -240,8 +363,13 @@ class $RealmsTable extends Realms with TableInfo<$RealmsTable, Realm> { class Realm extends DataClass implements Insertable { final String id; + final String slug; final String? name; final String? description; + final String? verifiedAs; + final DateTime? verifiedAt; + final bool isCommunity; + final bool isPublic; final Map? picture; final Map? background; final String? accountId; @@ -250,8 +378,13 @@ class Realm extends DataClass implements Insertable { final DateTime? deletedAt; const Realm({ required this.id, + required this.slug, this.name, this.description, + this.verifiedAs, + this.verifiedAt, + required this.isCommunity, + required this.isPublic, this.picture, this.background, this.accountId, @@ -263,12 +396,21 @@ class Realm extends DataClass implements Insertable { Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); + map['slug'] = Variable(slug); if (!nullToAbsent || name != null) { map['name'] = Variable(name); } if (!nullToAbsent || description != null) { map['description'] = Variable(description); } + if (!nullToAbsent || verifiedAs != null) { + map['verified_as'] = Variable(verifiedAs); + } + if (!nullToAbsent || verifiedAt != null) { + map['verified_at'] = Variable(verifiedAt); + } + map['is_community'] = Variable(isCommunity); + map['is_public'] = Variable(isPublic); if (!nullToAbsent || picture != null) { map['picture'] = Variable( $RealmsTable.$converterpicturen.toSql(picture), @@ -293,10 +435,19 @@ class Realm extends DataClass implements Insertable { RealmsCompanion toCompanion(bool nullToAbsent) { return RealmsCompanion( id: Value(id), + slug: Value(slug), name: name == null && nullToAbsent ? const Value.absent() : Value(name), description: description == null && nullToAbsent ? const Value.absent() : Value(description), + verifiedAs: verifiedAs == null && nullToAbsent + ? const Value.absent() + : Value(verifiedAs), + verifiedAt: verifiedAt == null && nullToAbsent + ? const Value.absent() + : Value(verifiedAt), + isCommunity: Value(isCommunity), + isPublic: Value(isPublic), picture: picture == null && nullToAbsent ? const Value.absent() : Value(picture), @@ -321,8 +472,13 @@ class Realm extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return Realm( id: serializer.fromJson(json['id']), + slug: serializer.fromJson(json['slug']), name: serializer.fromJson(json['name']), description: serializer.fromJson(json['description']), + verifiedAs: serializer.fromJson(json['verifiedAs']), + verifiedAt: serializer.fromJson(json['verifiedAt']), + isCommunity: serializer.fromJson(json['isCommunity']), + isPublic: serializer.fromJson(json['isPublic']), picture: serializer.fromJson?>(json['picture']), background: serializer.fromJson?>( json['background'], @@ -338,8 +494,13 @@ class Realm extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), + 'slug': serializer.toJson(slug), 'name': serializer.toJson(name), 'description': serializer.toJson(description), + 'verifiedAs': serializer.toJson(verifiedAs), + 'verifiedAt': serializer.toJson(verifiedAt), + 'isCommunity': serializer.toJson(isCommunity), + 'isPublic': serializer.toJson(isPublic), 'picture': serializer.toJson?>(picture), 'background': serializer.toJson?>(background), 'accountId': serializer.toJson(accountId), @@ -351,8 +512,13 @@ class Realm extends DataClass implements Insertable { Realm copyWith({ String? id, + String? slug, Value name = const Value.absent(), Value description = const Value.absent(), + Value verifiedAs = const Value.absent(), + Value verifiedAt = const Value.absent(), + bool? isCommunity, + bool? isPublic, Value?> picture = const Value.absent(), Value?> background = const Value.absent(), Value accountId = const Value.absent(), @@ -361,8 +527,13 @@ class Realm extends DataClass implements Insertable { Value deletedAt = const Value.absent(), }) => Realm( id: id ?? this.id, + slug: slug ?? this.slug, name: name.present ? name.value : this.name, description: description.present ? description.value : this.description, + verifiedAs: verifiedAs.present ? verifiedAs.value : this.verifiedAs, + verifiedAt: verifiedAt.present ? verifiedAt.value : this.verifiedAt, + isCommunity: isCommunity ?? this.isCommunity, + isPublic: isPublic ?? this.isPublic, picture: picture.present ? picture.value : this.picture, background: background.present ? background.value : this.background, accountId: accountId.present ? accountId.value : this.accountId, @@ -373,10 +544,21 @@ class Realm extends DataClass implements Insertable { Realm copyWithCompanion(RealmsCompanion data) { return Realm( id: data.id.present ? data.id.value : this.id, + slug: data.slug.present ? data.slug.value : this.slug, name: data.name.present ? data.name.value : this.name, description: data.description.present ? data.description.value : this.description, + verifiedAs: data.verifiedAs.present + ? data.verifiedAs.value + : this.verifiedAs, + verifiedAt: data.verifiedAt.present + ? data.verifiedAt.value + : this.verifiedAt, + isCommunity: data.isCommunity.present + ? data.isCommunity.value + : this.isCommunity, + isPublic: data.isPublic.present ? data.isPublic.value : this.isPublic, picture: data.picture.present ? data.picture.value : this.picture, background: data.background.present ? data.background.value @@ -392,8 +574,13 @@ class Realm extends DataClass implements Insertable { String toString() { return (StringBuffer('Realm(') ..write('id: $id, ') + ..write('slug: $slug, ') ..write('name: $name, ') ..write('description: $description, ') + ..write('verifiedAs: $verifiedAs, ') + ..write('verifiedAt: $verifiedAt, ') + ..write('isCommunity: $isCommunity, ') + ..write('isPublic: $isPublic, ') ..write('picture: $picture, ') ..write('background: $background, ') ..write('accountId: $accountId, ') @@ -407,8 +594,13 @@ class Realm extends DataClass implements Insertable { @override int get hashCode => Object.hash( id, + slug, name, description, + verifiedAs, + verifiedAt, + isCommunity, + isPublic, picture, background, accountId, @@ -421,8 +613,13 @@ class Realm extends DataClass implements Insertable { identical(this, other) || (other is Realm && other.id == this.id && + other.slug == this.slug && other.name == this.name && other.description == this.description && + other.verifiedAs == this.verifiedAs && + other.verifiedAt == this.verifiedAt && + other.isCommunity == this.isCommunity && + other.isPublic == this.isPublic && other.picture == this.picture && other.background == this.background && other.accountId == this.accountId && @@ -433,8 +630,13 @@ class Realm extends DataClass implements Insertable { class RealmsCompanion extends UpdateCompanion { final Value id; + final Value slug; final Value name; final Value description; + final Value verifiedAs; + final Value verifiedAt; + final Value isCommunity; + final Value isPublic; final Value?> picture; final Value?> background; final Value accountId; @@ -444,8 +646,13 @@ class RealmsCompanion extends UpdateCompanion { final Value rowid; const RealmsCompanion({ this.id = const Value.absent(), + this.slug = const Value.absent(), this.name = const Value.absent(), this.description = const Value.absent(), + this.verifiedAs = const Value.absent(), + this.verifiedAt = const Value.absent(), + this.isCommunity = const Value.absent(), + this.isPublic = const Value.absent(), this.picture = const Value.absent(), this.background = const Value.absent(), this.accountId = const Value.absent(), @@ -456,8 +663,13 @@ class RealmsCompanion extends UpdateCompanion { }); RealmsCompanion.insert({ required String id, + required String slug, this.name = const Value.absent(), this.description = const Value.absent(), + this.verifiedAs = const Value.absent(), + this.verifiedAt = const Value.absent(), + required bool isCommunity, + required bool isPublic, this.picture = const Value.absent(), this.background = const Value.absent(), this.accountId = const Value.absent(), @@ -466,12 +678,20 @@ class RealmsCompanion extends UpdateCompanion { this.deletedAt = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), + slug = Value(slug), + isCommunity = Value(isCommunity), + isPublic = Value(isPublic), createdAt = Value(createdAt), updatedAt = Value(updatedAt); static Insertable custom({ Expression? id, + Expression? slug, Expression? name, Expression? description, + Expression? verifiedAs, + Expression? verifiedAt, + Expression? isCommunity, + Expression? isPublic, Expression? picture, Expression? background, Expression? accountId, @@ -482,8 +702,13 @@ class RealmsCompanion extends UpdateCompanion { }) { return RawValuesInsertable({ if (id != null) 'id': id, + if (slug != null) 'slug': slug, if (name != null) 'name': name, if (description != null) 'description': description, + if (verifiedAs != null) 'verified_as': verifiedAs, + if (verifiedAt != null) 'verified_at': verifiedAt, + if (isCommunity != null) 'is_community': isCommunity, + if (isPublic != null) 'is_public': isPublic, if (picture != null) 'picture': picture, if (background != null) 'background': background, if (accountId != null) 'account_id': accountId, @@ -496,8 +721,13 @@ class RealmsCompanion extends UpdateCompanion { RealmsCompanion copyWith({ Value? id, + Value? slug, Value? name, Value? description, + Value? verifiedAs, + Value? verifiedAt, + Value? isCommunity, + Value? isPublic, Value?>? picture, Value?>? background, Value? accountId, @@ -508,8 +738,13 @@ class RealmsCompanion extends UpdateCompanion { }) { return RealmsCompanion( id: id ?? this.id, + slug: slug ?? this.slug, name: name ?? this.name, description: description ?? this.description, + verifiedAs: verifiedAs ?? this.verifiedAs, + verifiedAt: verifiedAt ?? this.verifiedAt, + isCommunity: isCommunity ?? this.isCommunity, + isPublic: isPublic ?? this.isPublic, picture: picture ?? this.picture, background: background ?? this.background, accountId: accountId ?? this.accountId, @@ -526,12 +761,27 @@ class RealmsCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } + if (slug.present) { + map['slug'] = Variable(slug.value); + } if (name.present) { map['name'] = Variable(name.value); } if (description.present) { map['description'] = Variable(description.value); } + if (verifiedAs.present) { + map['verified_as'] = Variable(verifiedAs.value); + } + if (verifiedAt.present) { + map['verified_at'] = Variable(verifiedAt.value); + } + if (isCommunity.present) { + map['is_community'] = Variable(isCommunity.value); + } + if (isPublic.present) { + map['is_public'] = Variable(isPublic.value); + } if (picture.present) { map['picture'] = Variable( $RealmsTable.$converterpicturen.toSql(picture.value), @@ -564,8 +814,13 @@ class RealmsCompanion extends UpdateCompanion { String toString() { return (StringBuffer('RealmsCompanion(') ..write('id: $id, ') + ..write('slug: $slug, ') ..write('name: $name, ') ..write('description: $description, ') + ..write('verifiedAs: $verifiedAs, ') + ..write('verifiedAt: $verifiedAt, ') + ..write('isCommunity: $isCommunity, ') + ..write('isPublic: $isPublic, ') ..write('picture: $picture, ') ..write('background: $background, ') ..write('accountId: $accountId, ') @@ -695,6 +950,21 @@ class $ChatRoomsTable extends ChatRooms type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _isPinnedMeta = const VerificationMeta( + 'isPinned', + ); + @override + late final GeneratedColumn isPinned = GeneratedColumn( + 'is_pinned', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_pinned" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); static const VerificationMeta _createdAtMeta = const VerificationMeta( 'createdAt', ); @@ -740,6 +1010,7 @@ class $ChatRoomsTable extends ChatRooms background, realmId, accountId, + isPinned, createdAt, updatedAt, deletedAt, @@ -811,6 +1082,12 @@ class $ChatRoomsTable extends ChatRooms accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta), ); } + if (data.containsKey('is_pinned')) { + context.handle( + _isPinnedMeta, + isPinned.isAcceptableOrUnknown(data['is_pinned']!, _isPinnedMeta), + ); + } if (data.containsKey('created_at')) { context.handle( _createdAtMeta, @@ -886,6 +1163,10 @@ class $ChatRoomsTable extends ChatRooms DriftSqlType.string, data['${effectivePrefix}account_id'], ), + isPinned: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_pinned'], + ), createdAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}created_at'], @@ -927,6 +1208,7 @@ class ChatRoom extends DataClass implements Insertable { final Map? background; final String? realmId; final String? accountId; + final bool? isPinned; final DateTime createdAt; final DateTime updatedAt; final DateTime? deletedAt; @@ -941,6 +1223,7 @@ class ChatRoom extends DataClass implements Insertable { this.background, this.realmId, this.accountId, + this.isPinned, required this.createdAt, required this.updatedAt, this.deletedAt, @@ -978,6 +1261,9 @@ class ChatRoom extends DataClass implements Insertable { if (!nullToAbsent || accountId != null) { map['account_id'] = Variable(accountId); } + if (!nullToAbsent || isPinned != null) { + map['is_pinned'] = Variable(isPinned); + } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); if (!nullToAbsent || deletedAt != null) { @@ -1012,6 +1298,9 @@ class ChatRoom extends DataClass implements Insertable { accountId: accountId == null && nullToAbsent ? const Value.absent() : Value(accountId), + isPinned: isPinned == null && nullToAbsent + ? const Value.absent() + : Value(isPinned), createdAt: Value(createdAt), updatedAt: Value(updatedAt), deletedAt: deletedAt == null && nullToAbsent @@ -1038,6 +1327,7 @@ class ChatRoom extends DataClass implements Insertable { ), realmId: serializer.fromJson(json['realmId']), accountId: serializer.fromJson(json['accountId']), + isPinned: serializer.fromJson(json['isPinned']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), deletedAt: serializer.fromJson(json['deletedAt']), @@ -1057,6 +1347,7 @@ class ChatRoom extends DataClass implements Insertable { 'background': serializer.toJson?>(background), 'realmId': serializer.toJson(realmId), 'accountId': serializer.toJson(accountId), + 'isPinned': serializer.toJson(isPinned), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'deletedAt': serializer.toJson(deletedAt), @@ -1074,6 +1365,7 @@ class ChatRoom extends DataClass implements Insertable { Value?> background = const Value.absent(), Value realmId = const Value.absent(), Value accountId = const Value.absent(), + Value isPinned = const Value.absent(), DateTime? createdAt, DateTime? updatedAt, Value deletedAt = const Value.absent(), @@ -1088,6 +1380,7 @@ class ChatRoom extends DataClass implements Insertable { background: background.present ? background.value : this.background, realmId: realmId.present ? realmId.value : this.realmId, accountId: accountId.present ? accountId.value : this.accountId, + isPinned: isPinned.present ? isPinned.value : this.isPinned, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, @@ -1110,6 +1403,7 @@ class ChatRoom extends DataClass implements Insertable { : this.background, realmId: data.realmId.present ? data.realmId.value : this.realmId, accountId: data.accountId.present ? data.accountId.value : this.accountId, + isPinned: data.isPinned.present ? data.isPinned.value : this.isPinned, 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, @@ -1129,6 +1423,7 @@ class ChatRoom extends DataClass implements Insertable { ..write('background: $background, ') ..write('realmId: $realmId, ') ..write('accountId: $accountId, ') + ..write('isPinned: $isPinned, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('deletedAt: $deletedAt') @@ -1148,6 +1443,7 @@ class ChatRoom extends DataClass implements Insertable { background, realmId, accountId, + isPinned, createdAt, updatedAt, deletedAt, @@ -1166,6 +1462,7 @@ class ChatRoom extends DataClass implements Insertable { other.background == this.background && other.realmId == this.realmId && other.accountId == this.accountId && + other.isPinned == this.isPinned && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && other.deletedAt == this.deletedAt); @@ -1182,6 +1479,7 @@ class ChatRoomsCompanion extends UpdateCompanion { final Value?> background; final Value realmId; final Value accountId; + final Value isPinned; final Value createdAt; final Value updatedAt; final Value deletedAt; @@ -1197,6 +1495,7 @@ class ChatRoomsCompanion extends UpdateCompanion { this.background = const Value.absent(), this.realmId = const Value.absent(), this.accountId = const Value.absent(), + this.isPinned = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), this.deletedAt = const Value.absent(), @@ -1213,6 +1512,7 @@ class ChatRoomsCompanion extends UpdateCompanion { this.background = const Value.absent(), this.realmId = const Value.absent(), this.accountId = const Value.absent(), + this.isPinned = const Value.absent(), required DateTime createdAt, required DateTime updatedAt, this.deletedAt = const Value.absent(), @@ -1232,6 +1532,7 @@ class ChatRoomsCompanion extends UpdateCompanion { Expression? background, Expression? realmId, Expression? accountId, + Expression? isPinned, Expression? createdAt, Expression? updatedAt, Expression? deletedAt, @@ -1248,6 +1549,7 @@ class ChatRoomsCompanion extends UpdateCompanion { if (background != null) 'background': background, if (realmId != null) 'realm_id': realmId, if (accountId != null) 'account_id': accountId, + if (isPinned != null) 'is_pinned': isPinned, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, if (deletedAt != null) 'deleted_at': deletedAt, @@ -1266,6 +1568,7 @@ class ChatRoomsCompanion extends UpdateCompanion { Value?>? background, Value? realmId, Value? accountId, + Value? isPinned, Value? createdAt, Value? updatedAt, Value? deletedAt, @@ -1282,6 +1585,7 @@ class ChatRoomsCompanion extends UpdateCompanion { background: background ?? this.background, realmId: realmId ?? this.realmId, accountId: accountId ?? this.accountId, + isPinned: isPinned ?? this.isPinned, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, deletedAt: deletedAt ?? this.deletedAt, @@ -1326,6 +1630,9 @@ class ChatRoomsCompanion extends UpdateCompanion { if (accountId.present) { map['account_id'] = Variable(accountId.value); } + if (isPinned.present) { + map['is_pinned'] = Variable(isPinned.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -1354,6 +1661,7 @@ class ChatRoomsCompanion extends UpdateCompanion { ..write('background: $background, ') ..write('realmId: $realmId, ') ..write('accountId: $accountId, ') + ..write('isPinned: $isPinned, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('deletedAt: $deletedAt, ') @@ -3719,8 +4027,13 @@ abstract class _$AppDatabase extends GeneratedDatabase { typedef $$RealmsTableCreateCompanionBuilder = RealmsCompanion Function({ required String id, + required String slug, Value name, Value description, + Value verifiedAs, + Value verifiedAt, + required bool isCommunity, + required bool isPublic, Value?> picture, Value?> background, Value accountId, @@ -3732,8 +4045,13 @@ typedef $$RealmsTableCreateCompanionBuilder = typedef $$RealmsTableUpdateCompanionBuilder = RealmsCompanion Function({ Value id, + Value slug, Value name, Value description, + Value verifiedAs, + Value verifiedAt, + Value isCommunity, + Value isPublic, Value?> picture, Value?> background, Value accountId, @@ -3780,6 +4098,11 @@ class $$RealmsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get slug => $composableBuilder( + column: $table.slug, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get name => $composableBuilder( column: $table.name, builder: (column) => ColumnFilters(column), @@ -3790,6 +4113,26 @@ class $$RealmsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get verifiedAs => $composableBuilder( + column: $table.verifiedAs, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get verifiedAt => $composableBuilder( + column: $table.verifiedAt, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isCommunity => $composableBuilder( + column: $table.isCommunity, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get isPublic => $composableBuilder( + column: $table.isPublic, + builder: (column) => ColumnFilters(column), + ); + ColumnWithTypeConverterFilters< Map?, Map, @@ -3870,6 +4213,11 @@ class $$RealmsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get slug => $composableBuilder( + column: $table.slug, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get name => $composableBuilder( column: $table.name, builder: (column) => ColumnOrderings(column), @@ -3880,6 +4228,26 @@ class $$RealmsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get verifiedAs => $composableBuilder( + column: $table.verifiedAs, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get verifiedAt => $composableBuilder( + column: $table.verifiedAt, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isCommunity => $composableBuilder( + column: $table.isCommunity, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get isPublic => $composableBuilder( + column: $table.isPublic, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get picture => $composableBuilder( column: $table.picture, builder: (column) => ColumnOrderings(column), @@ -3923,6 +4291,9 @@ class $$RealmsTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get slug => + $composableBuilder(column: $table.slug, builder: (column) => column); + GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); @@ -3931,6 +4302,24 @@ class $$RealmsTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get verifiedAs => $composableBuilder( + column: $table.verifiedAs, + builder: (column) => column, + ); + + GeneratedColumn get verifiedAt => $composableBuilder( + column: $table.verifiedAt, + builder: (column) => column, + ); + + GeneratedColumn get isCommunity => $composableBuilder( + column: $table.isCommunity, + builder: (column) => column, + ); + + GeneratedColumn get isPublic => + $composableBuilder(column: $table.isPublic, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get picture => $composableBuilder(column: $table.picture, builder: (column) => column); @@ -4007,8 +4396,13 @@ class $$RealmsTableTableManager updateCompanionCallback: ({ Value id = const Value.absent(), + Value slug = const Value.absent(), Value name = const Value.absent(), Value description = const Value.absent(), + Value verifiedAs = const Value.absent(), + Value verifiedAt = const Value.absent(), + Value isCommunity = const Value.absent(), + Value isPublic = const Value.absent(), Value?> picture = const Value.absent(), Value?> background = const Value.absent(), Value accountId = const Value.absent(), @@ -4018,8 +4412,13 @@ class $$RealmsTableTableManager Value rowid = const Value.absent(), }) => RealmsCompanion( id: id, + slug: slug, name: name, description: description, + verifiedAs: verifiedAs, + verifiedAt: verifiedAt, + isCommunity: isCommunity, + isPublic: isPublic, picture: picture, background: background, accountId: accountId, @@ -4031,8 +4430,13 @@ class $$RealmsTableTableManager createCompanionCallback: ({ required String id, + required String slug, Value name = const Value.absent(), Value description = const Value.absent(), + Value verifiedAs = const Value.absent(), + Value verifiedAt = const Value.absent(), + required bool isCommunity, + required bool isPublic, Value?> picture = const Value.absent(), Value?> background = const Value.absent(), Value accountId = const Value.absent(), @@ -4042,8 +4446,13 @@ class $$RealmsTableTableManager Value rowid = const Value.absent(), }) => RealmsCompanion.insert( id: id, + slug: slug, name: name, description: description, + verifiedAs: verifiedAs, + verifiedAt: verifiedAt, + isCommunity: isCommunity, + isPublic: isPublic, picture: picture, background: background, accountId: accountId, @@ -4110,6 +4519,7 @@ typedef $$ChatRoomsTableCreateCompanionBuilder = Value?> background, Value realmId, Value accountId, + Value isPinned, required DateTime createdAt, required DateTime updatedAt, Value deletedAt, @@ -4127,6 +4537,7 @@ typedef $$ChatRoomsTableUpdateCompanionBuilder = Value?> background, Value realmId, Value accountId, + Value isPinned, Value createdAt, Value updatedAt, Value deletedAt, @@ -4256,6 +4667,11 @@ class $$ChatRoomsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get isPinned => $composableBuilder( + column: $table.isPinned, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column), @@ -4399,6 +4815,11 @@ class $$ChatRoomsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get isPinned => $composableBuilder( + column: $table.isPinned, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column), @@ -4481,6 +4902,9 @@ class $$ChatRoomsTableAnnotationComposer GeneratedColumn get accountId => $composableBuilder(column: $table.accountId, builder: (column) => column); + GeneratedColumn get isPinned => + $composableBuilder(column: $table.isPinned, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); @@ -4606,6 +5030,7 @@ class $$ChatRoomsTableTableManager Value?> background = const Value.absent(), Value realmId = const Value.absent(), Value accountId = const Value.absent(), + Value isPinned = const Value.absent(), Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), Value deletedAt = const Value.absent(), @@ -4621,6 +5046,7 @@ class $$ChatRoomsTableTableManager background: background, realmId: realmId, accountId: accountId, + isPinned: isPinned, createdAt: createdAt, updatedAt: updatedAt, deletedAt: deletedAt, @@ -4638,6 +5064,7 @@ class $$ChatRoomsTableTableManager Value?> background = const Value.absent(), Value realmId = const Value.absent(), Value accountId = const Value.absent(), + Value isPinned = const Value.absent(), required DateTime createdAt, required DateTime updatedAt, Value deletedAt = const Value.absent(), @@ -4653,6 +5080,7 @@ class $$ChatRoomsTableTableManager background: background, realmId: realmId, accountId: accountId, + isPinned: isPinned, createdAt: createdAt, updatedAt: updatedAt, deletedAt: deletedAt, diff --git a/lib/database/message.dart b/lib/database/message.dart index 5c2600c8..27cb63ef 100644 --- a/lib/database/message.dart +++ b/lib/database/message.dart @@ -38,8 +38,13 @@ class ListMapConverter 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()(); @@ -64,6 +69,8 @@ class ChatRooms extends Table { 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()(); diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 242ff669..1c031156 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -24,6 +24,8 @@ sealed class SnChatRoom with _$SnChatRoom { required DateTime updatedAt, required DateTime? deletedAt, required List? members, + // Frontend data + @Default(false) bool isPinned, }) = _SnChatRoom; factory SnChatRoom.fromJson(Map json) => diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index 4bd27cab..8b1bf216 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -15,7 +15,8 @@ T _$identity(T value) => value; /// @nodoc mixin _$SnChatRoom { - String get id; String? get name; String? get description; int get type; bool get isPublic; bool get isCommunity; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; String? get accountId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List? get members; + String get id; String? get name; String? get description; int get type; bool get isPublic; bool get isCommunity; SnCloudFile? get picture; SnCloudFile? get background; String? get realmId; String? get accountId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List? get members;// Frontend data + bool get isPinned; /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +29,16 @@ $SnChatRoomCopyWith get copyWith => _$SnChatRoomCopyWithImpl Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members)); +int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members),isPinned); @override String toString() { - return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; + return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members, isPinned: $isPinned)'; } @@ -48,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res> { factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl; @useResult $Res call({ - String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members + String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members, bool isPinned }); @@ -65,7 +66,7 @@ class _$SnChatRoomCopyWithImpl<$Res> /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,Object? isPinned = null,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable @@ -82,7 +83,8 @@ as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime?,members: freezed == members ? _self.members : members // ignore: cast_nullable_to_non_nullable -as List?, +as List?,isPinned: null == isPinned ? _self.isPinned : isPinned // ignore: cast_nullable_to_non_nullable +as bool, )); } /// Create a copy of SnChatRoom @@ -200,10 +202,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members, bool isPinned)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnChatRoom() when $default != null: -return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members);case _: +return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members,_that.isPinned);case _: return orElse(); } @@ -221,10 +223,10 @@ return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic, /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members, bool isPinned) $default,) {final _that = this; switch (_that) { case _SnChatRoom(): -return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members);} +return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members,_that.isPinned);} } /// A variant of `when` that fallback to returning `null` /// @@ -238,10 +240,10 @@ return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic, /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members, bool isPinned)? $default,) {final _that = this; switch (_that) { case _SnChatRoom() when $default != null: -return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members);case _: +return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic,_that.isCommunity,_that.picture,_that.background,_that.realmId,_that.accountId,_that.realm,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.members,_that.isPinned);case _: return null; } @@ -253,7 +255,7 @@ return $default(_that.id,_that.name,_that.description,_that.type,_that.isPublic, @JsonSerializable() class _SnChatRoom implements SnChatRoom { - const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, this.isPublic = false, this.isCommunity = false, required this.picture, required this.background, required this.realmId, required this.accountId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List? members}): _members = members; + const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, this.isPublic = false, this.isCommunity = false, required this.picture, required this.background, required this.realmId, required this.accountId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List? members, this.isPinned = false}): _members = members; factory _SnChatRoom.fromJson(Map json) => _$SnChatRoomFromJson(json); @override final String id; @@ -279,6 +281,8 @@ class _SnChatRoom implements SnChatRoom { return EqualUnmodifiableListView(value); } +// Frontend data +@override@JsonKey() final bool isPinned; /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @@ -293,16 +297,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChatRoom&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.type, type) || other.type == type)&&(identical(other.isPublic, isPublic) || other.isPublic == isPublic)&&(identical(other.isCommunity, isCommunity) || other.isCommunity == isCommunity)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&const DeepCollectionEquality().equals(other._members, _members)&&(identical(other.isPinned, isPinned) || other.isPinned == isPinned)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members)); +int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,isCommunity,picture,background,realmId,accountId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members),isPinned); @override String toString() { - return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; + return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, isCommunity: $isCommunity, picture: $picture, background: $background, realmId: $realmId, accountId: $accountId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members, isPinned: $isPinned)'; } @@ -313,7 +317,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$ factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl; @override @useResult $Res call({ - String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members + String id, String? name, String? description, int type, bool isPublic, bool isCommunity, SnCloudFile? picture, SnCloudFile? background, String? realmId, String? accountId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members, bool isPinned }); @@ -330,7 +334,7 @@ class __$SnChatRoomCopyWithImpl<$Res> /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? isCommunity = null,Object? picture = freezed,Object? background = freezed,Object? realmId = freezed,Object? accountId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,Object? isPinned = null,}) { return _then(_SnChatRoom( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable @@ -347,7 +351,8 @@ as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable as DateTime?,members: freezed == members ? _self._members : members // ignore: cast_nullable_to_non_nullable -as List?, +as List?,isPinned: null == isPinned ? _self.isPinned : isPinned // ignore: cast_nullable_to_non_nullable +as bool, )); } diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index 3595d029..75ce0f75 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -32,6 +32,7 @@ _SnChatRoom _$SnChatRoomFromJson(Map json) => _SnChatRoom( members: (json['members'] as List?) ?.map((e) => SnChatMember.fromJson(e as Map)) .toList(), + isPinned: json['is_pinned'] as bool? ?? false, ); Map _$SnChatRoomToJson(_SnChatRoom instance) => @@ -51,6 +52,7 @@ Map _$SnChatRoomToJson(_SnChatRoom instance) => 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), 'members': instance.members?.map((e) => e.toJson()).toList(), + 'is_pinned': instance.isPinned, }; _SnChatMessage _$SnChatMessageFromJson(Map json) => diff --git a/lib/pods/chat/chat_room.dart b/lib/pods/chat/chat_room.dart index a9524865..067819db 100644 --- a/lib/pods/chat/chat_room.dart +++ b/lib/pods/chat/chat_room.dart @@ -47,6 +47,7 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { try { final localRoomsData = await db.select(db.chatRooms).get(); + final localRealmsData = await db.select(db.realms).get(); if (localRoomsData.isNotEmpty) { final localRooms = await Future.wait( localRoomsData.map((row) async { @@ -87,11 +88,15 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { : null, realmId: row.realmId, accountId: row.accountId, - realm: null, + realm: localRealmsData + .where((e) => e.id == row.realmId) + .map((e) => _buildRealmFromTableEntry(e)) + .firstOrNull, createdAt: row.createdAt, updatedAt: row.updatedAt, deletedAt: row.deletedAt, members: members, + isPinned: row.isPinned ?? false, ); }), ); @@ -126,6 +131,29 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { return rooms; } + SnRealm _buildRealmFromTableEntry(Realm localRealm) { + return SnRealm( + id: localRealm.id, + slug: localRealm.slug, + name: localRealm.name ?? localRealm.slug, + description: localRealm.description ?? '', + verifiedAs: localRealm.verifiedAs, + verifiedAt: localRealm.verifiedAt, + isCommunity: localRealm.isCommunity, + isPublic: localRealm.isPublic, + picture: localRealm.picture != null + ? SnCloudFile.fromJson(localRealm.picture!) + : null, + background: localRealm.background != null + ? SnCloudFile.fromJson(localRealm.background!) + : null, + accountId: localRealm.accountId ?? '', + createdAt: localRealm.createdAt, + updatedAt: localRealm.updatedAt, + deletedAt: localRealm.deletedAt, + ); + } + Future> _buildRoomsFromDb(AppDatabase db) async { final localRoomsData = await db.select(db.chatRooms).get(); return Future.wait( @@ -207,6 +235,7 @@ class ChatRoomJoinedNotifier extends _$ChatRoomJoinedNotifier { updatedAt: row.updatedAt, deletedAt: row.deletedAt, members: members, + isPinned: row.isPinned ?? false, ); }), ); diff --git a/lib/pods/chat/chat_room.g.dart b/lib/pods/chat/chat_room.g.dart index 3b31a92f..0c185829 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'65961aac28b5188900c4b25308f6fd080a14d5ab'; + r'805b38e477df574c92b1ef3cd54527cfd03a55cb'; abstract class _$ChatRoomJoinedNotifier extends $AsyncNotifier> { diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 249e9bcd..a77ddf37 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -20,8 +20,8 @@ import 'package:island/widgets/navigation/fab_menu.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.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'; +import 'package:super_sliver_list/super_sliver_list.dart'; class ChatListBodyWidget extends HookConsumerWidget { final bool isFloating; @@ -55,50 +55,95 @@ class ChatListBodyWidget extends HookConsumerWidget { ), Expanded( child: chats.when( - data: (items) => RefreshIndicator( - onRefresh: () => Future.sync(() { - ref.invalidate(chatRoomJoinedProvider); - }), - child: SuperListView.builder( - padding: EdgeInsets.only(bottom: 96), - itemCount: items - .where( - (item) => - selectedTab.value == 0 || - (selectedTab.value == 1 && item.type == 1) || - (selectedTab.value == 2 && item.type != 1), - ) - .length, - itemBuilder: (context, index) { - final filteredItems = items - .where( - (item) => - selectedTab.value == 0 || - (selectedTab.value == 1 && item.type == 1) || - (selectedTab.value == 2 && item.type != 1), - ) - .toList(); - final item = filteredItems[index]; - return ChatRoomListTile( - room: item, - isDirect: item.type == 1, - onTap: () { - if (isWideScreen(context)) { - context.replaceNamed( - 'chatRoom', - pathParameters: {'id': item.id}, - ); - } else { - context.pushNamed( - 'chatRoom', - pathParameters: {'id': item.id}, - ); - } - }, - ); - }, - ), - ), + data: (items) { + final filteredItems = items.where( + (item) => + selectedTab.value == 0 || + (selectedTab.value == 1 && item.type == 1) || + (selectedTab.value == 2 && item.type != 1), + ); + final pinnedItems = filteredItems + .where((item) => item.isPinned) + .toList(); + final unpinnedItems = filteredItems + .where((item) => !item.isPinned) + .toList(); + + return RefreshIndicator( + onRefresh: () => Future.sync(() { + ref.invalidate(chatRoomJoinedProvider); + }), + child: Column( + children: [ + ExpansionTile( + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer.withOpacity(0.5), + collapsedBackgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainer.withOpacity(0.5), + title: Text('pinnedChatRoom'.tr()), + leading: const Icon(Symbols.keep, fill: 1), + tilePadding: const EdgeInsets.symmetric(horizontal: 24), + initiallyExpanded: true, + children: [ + for (final item in pinnedItems) + ChatRoomListTile( + room: item, + isDirect: item.type == 1, + onTap: () { + if (isWideScreen(context)) { + context.replaceNamed( + 'chatRoom', + pathParameters: {'id': item.id}, + ); + } else { + context.pushNamed( + 'chatRoom', + pathParameters: {'id': item.id}, + ); + } + }, + ), + ], + ), + Expanded( + child: SuperListView.builder( + padding: EdgeInsets.only(bottom: 96), + itemCount: unpinnedItems + .where( + (item) => + selectedTab.value == 0 || + (selectedTab.value == 1 && item.type == 1) || + (selectedTab.value == 2 && item.type != 1), + ) + .length, + itemBuilder: (context, index) { + final item = unpinnedItems[index]; + return ChatRoomListTile( + room: item, + isDirect: item.type == 1, + onTap: () { + if (isWideScreen(context)) { + context.replaceNamed( + 'chatRoom', + pathParameters: {'id': item.id}, + ); + } else { + context.pushNamed( + 'chatRoom', + pathParameters: {'id': item.id}, + ); + } + }, + ); + }, + ), + ), + ], + ), + ); + }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => ResponseErrorWidget( error: error, diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index f7e924bc..836db20c 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:gap/gap.dart'; @@ -42,6 +43,20 @@ class ChatDetailScreen extends HookConsumerWidget { final roomIdentity = ref.watch(chatRoomIdentityProvider(id)); final totalMessages = ref.watch(totalMessagesCountProvider(id)); + // Local state for pinned status to provide immediate UI feedback + final isPinned = useState(null); + + // Initialize pinned state from database + useEffect(() { + final db = ref.read(databaseProvider); + (db.select( + db.chatRooms, + )..where((r) => r.id.equals(id))).getSingleOrNull().then((room) { + isPinned.value = room?.isPinned ?? false; + }); + return null; + }, [id]); + const kNotifyLevelText = [ 'chatNotifyLevelAll', 'chatNotifyLevelMention', @@ -83,46 +98,45 @@ class ChatDetailScreen extends HookConsumerWidget { showModalBottomSheet( isScrollControlled: true, context: context, - builder: - (context) => SheetScaffold( - height: 320, - titleText: 'chatNotifyLevel'.tr(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: const Text('chatNotifyLevelAll').tr(), - subtitle: const Text('chatNotifyLevelDescription').tr(), - leading: const Icon(Icons.notifications_active), - selected: identity.notify == 0, - onTap: () { - setNotifyLevel(0); - Navigator.pop(context); - }, - ), - ListTile( - title: const Text('chatNotifyLevelMention').tr(), - subtitle: const Text('chatNotifyLevelDescription').tr(), - leading: const Icon(Icons.alternate_email), - selected: identity.notify == 1, - onTap: () { - setNotifyLevel(1); - Navigator.pop(context); - }, - ), - ListTile( - title: const Text('chatNotifyLevelNone').tr(), - subtitle: const Text('chatNotifyLevelDescription').tr(), - leading: const Icon(Icons.notifications_off), - selected: identity.notify == 2, - onTap: () { - setNotifyLevel(2); - Navigator.pop(context); - }, - ), - ], + builder: (context) => SheetScaffold( + height: 320, + titleText: 'chatNotifyLevel'.tr(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('chatNotifyLevelAll').tr(), + subtitle: const Text('chatNotifyLevelDescription').tr(), + leading: const Icon(Icons.notifications_active), + selected: identity.notify == 0, + onTap: () { + setNotifyLevel(0); + Navigator.pop(context); + }, ), - ), + ListTile( + title: const Text('chatNotifyLevelMention').tr(), + subtitle: const Text('chatNotifyLevelDescription').tr(), + leading: const Icon(Icons.alternate_email), + selected: identity.notify == 1, + onTap: () { + setNotifyLevel(1); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('chatNotifyLevelNone').tr(), + subtitle: const Text('chatNotifyLevelDescription').tr(), + leading: const Icon(Icons.notifications_off), + selected: identity.notify == 2, + onTap: () { + setNotifyLevel(2); + Navigator.pop(context); + }, + ), + ], + ), + ), ); } @@ -132,118 +146,117 @@ class ChatDetailScreen extends HookConsumerWidget { showDialog( context: context, - builder: - (context) => AlertDialog( - title: const Text('chatBreak').tr(), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('chatBreakDescription').tr(), - const Gap(16), - ListTile( - title: const Text('chatBreakClearButton').tr(), - subtitle: const Text('chatBreakClear').tr(), - leading: const Icon(Icons.notifications_active), - onTap: () { - setChatBreak(now); - Navigator.pop(context); - if (context.mounted) { - showSnackBar('chatBreakCleared'.tr()); - } - }, - ), - ListTile( - title: const Text('chatBreak5m').tr(), - subtitle: const Text( - 'chatBreakHour', - ).tr(args: ['chatBreak5m'.tr()]), - leading: const Icon(Symbols.circle), - onTap: () { - setChatBreak(now.add(const Duration(minutes: 5))); - Navigator.pop(context); - if (context.mounted) { - showSnackBar('chatBreakSet'.tr(args: ['5m'])); - } - }, - ), - ListTile( - title: const Text('chatBreak10m').tr(), - subtitle: const Text( - 'chatBreakHour', - ).tr(args: ['chatBreak10m'.tr()]), - leading: const Icon(Symbols.circle), - onTap: () { - setChatBreak(now.add(const Duration(minutes: 10))); - Navigator.pop(context); - if (context.mounted) { - showSnackBar('chatBreakSet'.tr(args: ['10m'])); - } - }, - ), - ListTile( - title: const Text('chatBreak15m').tr(), - subtitle: const Text( - 'chatBreakHour', - ).tr(args: ['chatBreak15m'.tr()]), - leading: const Icon(Symbols.timer_3), - onTap: () { - setChatBreak(now.add(const Duration(minutes: 15))); - Navigator.pop(context); - if (context.mounted) { - showSnackBar('chatBreakSet'.tr(args: ['15m'])); - } - }, - ), - ListTile( - title: const Text('chatBreak30m').tr(), - subtitle: const Text( - 'chatBreakHour', - ).tr(args: ['chatBreak30m'.tr()]), - leading: const Icon(Symbols.timer), - onTap: () { - setChatBreak(now.add(const Duration(minutes: 30))); - Navigator.pop(context); - if (context.mounted) { - showSnackBar('chatBreakSet'.tr(args: ['30m'])); - } - }, - ), - const Gap(8), - TextField( - controller: durationController, - decoration: InputDecoration( - labelText: 'chatBreakCustomMinutes'.tr(), - hintText: 'chatBreakEnterMinutes'.tr(), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: const Icon(Icons.check), - onPressed: () { - final minutes = int.tryParse(durationController.text); - if (minutes != null && minutes > 0) { - setChatBreak(now.add(Duration(minutes: minutes))); - Navigator.pop(context); - if (context.mounted) { - showSnackBar( - 'chatBreakSet'.tr(args: ['${minutes}m']), - ); - } - } - }, - ), - ), - keyboardType: TextInputType.number, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), - ), - ], + builder: (context) => AlertDialog( + title: const Text('chatBreak').tr(), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('chatBreakDescription').tr(), + const Gap(16), + ListTile( + title: const Text('chatBreakClearButton').tr(), + subtitle: const Text('chatBreakClear').tr(), + leading: const Icon(Icons.notifications_active), + onTap: () { + setChatBreak(now); + Navigator.pop(context); + if (context.mounted) { + showSnackBar('chatBreakCleared'.tr()); + } + }, ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('cancel').tr(), + ListTile( + title: const Text('chatBreak5m').tr(), + subtitle: const Text( + 'chatBreakHour', + ).tr(args: ['chatBreak5m'.tr()]), + leading: const Icon(Symbols.circle), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 5))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar('chatBreakSet'.tr(args: ['5m'])); + } + }, + ), + ListTile( + title: const Text('chatBreak10m').tr(), + subtitle: const Text( + 'chatBreakHour', + ).tr(args: ['chatBreak10m'.tr()]), + leading: const Icon(Symbols.circle), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 10))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar('chatBreakSet'.tr(args: ['10m'])); + } + }, + ), + ListTile( + title: const Text('chatBreak15m').tr(), + subtitle: const Text( + 'chatBreakHour', + ).tr(args: ['chatBreak15m'.tr()]), + leading: const Icon(Symbols.timer_3), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 15))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar('chatBreakSet'.tr(args: ['15m'])); + } + }, + ), + ListTile( + title: const Text('chatBreak30m').tr(), + subtitle: const Text( + 'chatBreakHour', + ).tr(args: ['chatBreak30m'.tr()]), + leading: const Icon(Symbols.timer), + onTap: () { + setChatBreak(now.add(const Duration(minutes: 30))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar('chatBreakSet'.tr(args: ['30m'])); + } + }, + ), + const Gap(8), + TextField( + controller: durationController, + decoration: InputDecoration( + labelText: 'chatBreakCustomMinutes'.tr(), + hintText: 'chatBreakEnterMinutes'.tr(), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.check), + onPressed: () { + final minutes = int.tryParse(durationController.text); + if (minutes != null && minutes > 0) { + setChatBreak(now.add(Duration(minutes: minutes))); + Navigator.pop(context); + if (context.mounted) { + showSnackBar( + 'chatBreakSet'.tr(args: ['${minutes}m']), + ); + } + } + }, + ), ), - ], + keyboardType: TextInputType.number, + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('cancel').tr(), ), + ], + ), ); } @@ -256,175 +269,197 @@ class ChatDetailScreen extends HookConsumerWidget { return AppScaffold( body: roomState.when( loading: () => const Center(child: CircularProgressIndicator()), - error: - (error, _) => Center( - child: Text('errorGeneric'.tr(args: [error.toString()])), - ), - data: - (currentRoom) => CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: 180, - pinned: true, - leading: PageBackButton(shadows: [iconShadow]), - flexibleSpace: FlexibleSpaceBar( - background: - (currentRoom!.type == 1 && - currentRoom.background?.id != null) - ? CloudImageWidget( - fileId: currentRoom.background!.id, - ) - : (currentRoom.type == 1 && - currentRoom.members!.length == 1 && - currentRoom - .members! - .first - .account - .profile - .background - ?.id != - null) - ? CloudImageWidget( - fileId: - currentRoom - .members! - .first - .account - .profile - .background! - .id, - ) - : currentRoom.background?.id != null - ? CloudImageWidget( - fileId: currentRoom.background!.id, - fit: BoxFit.cover, - ) - : Container( - color: - Theme.of(context).appBarTheme.backgroundColor, - ), - title: Text( - (currentRoom.type == 1 && currentRoom.name == null) - ? currentRoom.members! - .map((e) => e.account.nick) - .join(', ') - : currentRoom.name!, - style: TextStyle( - color: Theme.of(context).appBarTheme.foregroundColor, - shadows: [iconShadow], + error: (error, _) => + Center(child: Text('errorGeneric'.tr(args: [error.toString()]))), + data: (currentRoom) => CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 180, + pinned: true, + leading: PageBackButton(shadows: [iconShadow]), + flexibleSpace: FlexibleSpaceBar( + background: + (currentRoom!.type == 1 && + currentRoom.background?.id != null) + ? CloudImageWidget(fileId: currentRoom.background!.id) + : (currentRoom.type == 1 && + currentRoom.members!.length == 1 && + currentRoom + .members! + .first + .account + .profile + .background + ?.id != + null) + ? CloudImageWidget( + fileId: currentRoom + .members! + .first + .account + .profile + .background! + .id, + ) + : currentRoom.background?.id != null + ? CloudImageWidget( + fileId: currentRoom.background!.id, + fit: BoxFit.cover, + ) + : Container( + color: Theme.of(context).appBarTheme.backgroundColor, ), - ), + title: Text( + (currentRoom.type == 1 && currentRoom.name == null) + ? currentRoom.members! + .map((e) => e.account.nick) + .join(', ') + : currentRoom.name!, + style: TextStyle( + color: Theme.of(context).appBarTheme.foregroundColor, + shadows: [iconShadow], ), - actions: [ - IconButton( - icon: const Icon(Icons.people, shadows: [iconShadow]), - onPressed: () { - showModalBottomSheet( - isScrollControlled: true, - context: context, - builder: - (context) => _ChatMemberListSheet(roomId: id), - ); - }, - ), - _ChatRoomActionMenu(id: id, iconShadow: iconShadow), - const Gap(8), - ], ), - SliverToBoxAdapter( - child: Column( + ), + actions: [ + IconButton( + icon: const Icon(Icons.people, shadows: [iconShadow]), + onPressed: () { + showModalBottomSheet( + isScrollControlled: true, + context: context, + builder: (context) => _ChatMemberListSheet(roomId: id), + ); + }, + ), + _ChatRoomActionMenu(id: id, iconShadow: iconShadow), + const Gap(8), + ], + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentRoom.description ?? 'descriptionNone'.tr(), + style: const TextStyle(fontSize: 16), + ).padding(all: 24), + const Divider(height: 1), + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - currentRoom.description ?? 'descriptionNone'.tr(), - style: const TextStyle(fontSize: 16), - ).padding(all: 24), - const Divider(height: 1), + // Pin/Unpin Switch + if (isPinned.value != null) + SwitchListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 24), + secondary: Icon( + Symbols.push_pin, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + title: const Text('pinChatRoom').tr(), + subtitle: const Text('pinChatRoomDescription').tr(), + value: isPinned.value!, + onChanged: (value) async { + // Update local state immediately for instant UI feedback + isPinned.value = value; + final db = ref.read(databaseProvider); + await db.toggleChatRoomPinned(id); + // Re-verify the state from database in case of error + final room = await (db.select( + db.chatRooms, + )..where((r) => r.id.equals(id))).getSingleOrNull(); + final actualPinned = room?.isPinned ?? false; + if (actualPinned != value) { + // Revert if database operation failed + isPinned.value = actualPinned; + } + showSnackBar( + value + ? 'chatRoomPinned'.tr() + : 'chatRoomUnpinned'.tr(), + ); + }, + ), roomIdentity.when( - data: - (identity) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListTile( - contentPadding: EdgeInsets.symmetric( - horizontal: 24, - ), - leading: const Icon(Symbols.notifications), - trailing: const Icon(Symbols.chevron_right), - title: const Text('chatNotifyLevel').tr(), - subtitle: Text( - kNotifyLevelText[identity!.notify].tr(), - ), - onTap: - () => - showNotifyLevelBottomSheet(identity), - ), - ListTile( - contentPadding: EdgeInsets.symmetric( - horizontal: 24, - ), - leading: const Icon(Icons.timer), - trailing: const Icon(Symbols.chevron_right), - title: const Text('chatBreak').tr(), - subtitle: - identity.breakUntil != null && - identity.breakUntil!.isAfter( - DateTime.now(), - ) - ? Text( - DateFormat( - 'yyyy-MM-dd HH:mm', - ).format(identity.breakUntil!), - ) - : const Text('chatBreakNone').tr(), - onTap: () => showChatBreakDialog(), - ), - ListTile( - contentPadding: EdgeInsets.symmetric( - horizontal: 24, - ), - leading: const Icon(Icons.search), - trailing: const Icon(Symbols.chevron_right), - title: const Text('searchMessages').tr(), - subtitle: totalMessages.when( - data: - (count) => Text( - 'messagesCount'.tr( - args: [count.toString()], - ), - ), - loading: - () => const CircularProgressIndicator(), - error: - (err, stack) => Text( - 'errorGeneric'.tr( - args: [err.toString()], - ), - ), - ), - onTap: () async { - final result = await context.pushNamed( - 'searchMessages', - pathParameters: {'id': id}, - ); - if (result is SearchMessagesResult) { - // Navigate back to room screen with message to jump to - if (context.mounted) { - context.pop(result); - } - } - }, - ), - ], + data: (identity) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Symbols.notifications), + trailing: const Icon(Symbols.chevron_right), + title: const Text('chatNotifyLevel').tr(), + subtitle: Text( + kNotifyLevelText[identity!.notify].tr(), + ), + onTap: () => showNotifyLevelBottomSheet(identity), ), + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Icons.timer), + trailing: const Icon(Symbols.chevron_right), + title: const Text('chatBreak').tr(), + subtitle: + identity.breakUntil != null && + identity.breakUntil!.isAfter( + DateTime.now(), + ) + ? Text( + DateFormat( + 'yyyy-MM-dd HH:mm', + ).format(identity.breakUntil!), + ) + : const Text('chatBreakNone').tr(), + onTap: () => showChatBreakDialog(), + ), + ListTile( + contentPadding: EdgeInsets.symmetric( + horizontal: 24, + ), + leading: const Icon(Icons.search), + trailing: const Icon(Symbols.chevron_right), + title: const Text('searchMessages').tr(), + subtitle: totalMessages.when( + data: (count) => Text( + 'messagesCount'.tr(args: [count.toString()]), + ), + loading: () => + const CircularProgressIndicator(), + error: (err, stack) => Text( + 'errorGeneric'.tr(args: [err.toString()]), + ), + ), + onTap: () async { + final result = await context.pushNamed( + 'searchMessages', + pathParameters: {'id': id}, + ); + if (result is SearchMessagesResult) { + // Navigate back to room screen with message to jump to + if (context.mounted) { + context.pop(result); + } + } + }, + ), + ], + ), error: (_, _) => const SizedBox.shrink(), loading: () => const SizedBox.shrink(), ), ], ), - ), - ], + ], + ), ), + ], + ), ), ); } @@ -447,97 +482,94 @@ class _ChatRoomActionMenu extends HookConsumerWidget { return PopupMenuButton( icon: Icon(Icons.more_vert, shadows: [iconShadow]), - itemBuilder: - (context) => [ - if (isManagable) - PopupMenuItem( - onTap: () { - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - builder: (context) => EditChatScreen(id: id), - ).then((value) { - if (value != null) { - // Invalidate to refresh room data after edit - ref.invalidate(chatMemberListProvider(id)); - } - }); - }, - child: Row( - children: [ - Icon( - Icons.edit, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const Gap(12), - const Text('editChatRoom').tr(), - ], + itemBuilder: (context) => [ + if (isManagable) + PopupMenuItem( + onTap: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) => EditChatScreen(id: id), + ).then((value) { + if (value != null) { + // Invalidate to refresh room data after edit + ref.invalidate(chatMemberListProvider(id)); + } + }); + }, + child: Row( + children: [ + Icon( + Icons.edit, + color: Theme.of(context).colorScheme.onSecondaryContainer, ), - ), - if (isManagable) - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.delete, color: Colors.red), - const Gap(12), - const Text( - 'deleteChatRoom', - style: TextStyle(color: Colors.red), - ).tr(), - ], + const Gap(12), + const Text('editChatRoom').tr(), + ], + ), + ), + if (isManagable) + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const Gap(12), + const Text( + 'deleteChatRoom', + style: TextStyle(color: Colors.red), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'deleteChatRoomHint'.tr(), + 'deleteChatRoom'.tr(), + isDanger: true, + ).then((confirm) async { + if (confirm) { + final client = ref.watch(apiClientProvider); + await client.delete('/sphere/chat/$id'); + ref.invalidate(chatRoomJoinedProvider); + if (context.mounted) { + context.pop(); + } + } + }); + }, + ) + else + PopupMenuItem( + child: Row( + children: [ + Icon( + Icons.exit_to_app, + color: Theme.of(context).colorScheme.error, ), - onTap: () { - showConfirmAlert( - 'deleteChatRoomHint'.tr(), - 'deleteChatRoom'.tr(), - isDanger: true, - ).then((confirm) async { - if (confirm) { - final client = ref.watch(apiClientProvider); - await client.delete('/sphere/chat/$id'); - ref.invalidate(chatRoomJoinedProvider); - if (context.mounted) { - context.pop(); - } - } - }); - }, - ) - else - PopupMenuItem( - child: Row( - children: [ - Icon( - Icons.exit_to_app, - color: Theme.of(context).colorScheme.error, - ), - const Gap(12), - Text( - 'leaveChatRoom', - style: TextStyle( - color: Theme.of(context).colorScheme.error, - ), - ).tr(), - ], - ), - onTap: () { - showConfirmAlert( - 'leaveChatRoomHint'.tr(), - 'leaveChatRoom'.tr(), - ).then((confirm) async { - if (confirm) { - final client = ref.watch(apiClientProvider); - await client.delete('/sphere/chat/$id/members/me'); - ref.invalidate(chatRoomJoinedProvider); - if (context.mounted) { - context.pop(); - } - } - }); - }, - ), - ], + const Gap(12), + Text( + 'leaveChatRoom', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ).tr(), + ], + ), + onTap: () { + showConfirmAlert( + 'leaveChatRoomHint'.tr(), + 'leaveChatRoom'.tr(), + ).then((confirm) async { + if (confirm) { + final client = ref.watch(apiClientProvider); + await client.delete('/sphere/chat/$id/members/me'); + ref.invalidate(chatRoomJoinedProvider); + if (context.mounted) { + context.pop(); + } + } + }); + }, + ), + ], ); } } @@ -576,11 +608,10 @@ class ChatMemberListNotifier extends AsyncNotifier> ); totalCount = int.parse(response.headers.value('X-Total') ?? '0'); - final members = - response.data - .map((e) => SnChatMember.fromJson(e)) - .cast() - .toList(); + final members = response.data + .map((e) => SnChatMember.fromJson(e)) + .cast() + .toList(); return members; }