From 4b6a5c28de001931a432124b8261ee81a4e103f1 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 4 May 2025 16:05:18 +0800 Subject: [PATCH] :sparkles: Direct messages --- assets/i18n/en-US.json | 3 +- lib/database/message_repository.dart | 2 +- lib/models/chat.dart | 12 +-- lib/models/chat.freezed.dart | 127 +++++++++++++++------------ lib/models/chat.g.dart | 42 +++++---- lib/screens/chat/chat.dart | 120 ++++++++++++++++++++++--- lib/screens/chat/chat.g.dart | 21 ++--- lib/screens/chat/room.dart | 26 ++++-- lib/screens/chat/room_detail.dart | 25 +++++- lib/widgets/content/cloud_files.dart | 5 ++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 12 files changed, 278 insertions(+), 114 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index ef85ad8..368732b 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -92,5 +92,6 @@ "forward": "Forward", "edited": "Edited", "addVideo": "Add video", - "addPhoto": "Add photo" + "addPhoto": "Add photo", + "createDirectMessage": "New direct message" } diff --git a/lib/database/message_repository.dart b/lib/database/message_repository.dart index b8296e5..321bae9 100644 --- a/lib/database/message_repository.dart +++ b/lib/database/message_repository.dart @@ -8,7 +8,7 @@ import 'package:island/widgets/alert.dart'; import 'package:uuid/uuid.dart'; class MessageRepository { - final SnChat room; + final SnChatRoom room; final SnChatMember identity; final Dio _apiClient; final AppDatabase _database; diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 37da82b..893501c 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -7,8 +7,8 @@ part 'chat.freezed.dart'; part 'chat.g.dart'; @freezed -abstract class SnChat with _$SnChat { - const factory SnChat({ +abstract class SnChatRoom with _$SnChatRoom { + const factory SnChatRoom({ required int id, required String name, required String description, @@ -23,9 +23,11 @@ abstract class SnChat with _$SnChat { required DateTime createdAt, required DateTime updatedAt, required DateTime? deletedAt, - }) = _SnChat; + required List? members, + }) = _SnChatRoom; - factory SnChat.fromJson(Map json) => _$SnChatFromJson(json); + factory SnChatRoom.fromJson(Map json) => + _$SnChatRoomFromJson(json); } @freezed @@ -79,7 +81,7 @@ abstract class SnChatMember with _$SnChatMember { required DateTime? deletedAt, required String id, required int chatRoomId, - required SnChat? chatRoom, + required SnChatRoom? chatRoom, required int accountId, required SnAccount account, required String? nick, diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index ce0355c..0e7e5c4 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -14,42 +14,42 @@ part of 'chat.dart'; T _$identity(T value) => value; /// @nodoc -mixin _$SnChat { +mixin _$SnChatRoom { - int get id; String get name; String get description; int get type; bool get isPublic; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; int? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; -/// Create a copy of SnChat + int get id; String get name; String get description; int get type; bool get isPublic; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; int? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List? get members; +/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -$SnChatCopyWith get copyWith => _$SnChatCopyWithImpl(this as SnChat, _$identity); +$SnChatRoomCopyWith get copyWith => _$SnChatRoomCopyWithImpl(this as SnChatRoom, _$identity); - /// Serializes this SnChat to a JSON map. + /// Serializes this SnChatRoom to a JSON map. Map toJson(); @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnChat&&(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.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(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)); + 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.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(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)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,pictureId,picture,backgroundId,background,realmId,realm,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,pictureId,picture,backgroundId,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(members)); @override String toString() { - return 'SnChat(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; } } /// @nodoc -abstract mixin class $SnChatCopyWith<$Res> { - factory $SnChatCopyWith(SnChat value, $Res Function(SnChat) _then) = _$SnChatCopyWithImpl; +abstract mixin class $SnChatRoomCopyWith<$Res> { + factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl; @useResult $Res call({ - int id, String name, String description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, int? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + int id, String name, String description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, int? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members }); @@ -57,16 +57,16 @@ $SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get backgrou } /// @nodoc -class _$SnChatCopyWithImpl<$Res> - implements $SnChatCopyWith<$Res> { - _$SnChatCopyWithImpl(this._self, this._then); +class _$SnChatRoomCopyWithImpl<$Res> + implements $SnChatRoomCopyWith<$Res> { + _$SnChatRoomCopyWithImpl(this._self, this._then); - final SnChat _self; - final $Res Function(SnChat) _then; + final SnChatRoom _self; + final $Res Function(SnChatRoom) _then; -/// Create a copy of SnChat +/// 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 = null,Object? description = null,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable @@ -82,10 +82,11 @@ as int?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_t as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable 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?, +as DateTime?,members: freezed == members ? _self.members : members // ignore: cast_nullable_to_non_nullable +as List?, )); } -/// Create a copy of SnChat +/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -97,7 +98,7 @@ $SnCloudFileCopyWith<$Res>? get picture { return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) { return _then(_self.copyWith(picture: value)); }); -}/// Create a copy of SnChat +}/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -109,7 +110,7 @@ $SnCloudFileCopyWith<$Res>? get background { return $SnCloudFileCopyWith<$Res>(_self.background!, (value) { return _then(_self.copyWith(background: value)); }); -}/// Create a copy of SnChat +}/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -128,9 +129,9 @@ $SnRealmCopyWith<$Res>? get realm { /// @nodoc @JsonSerializable() -class _SnChat implements SnChat { - const _SnChat({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt}); - factory _SnChat.fromJson(Map json) => _$SnChatFromJson(json); +class _SnChatRoom implements SnChatRoom { + const _SnChatRoom({required this.id, required this.name, required this.description, required this.type, required this.isPublic, required this.pictureId, required this.picture, required this.backgroundId, required this.background, required this.realmId, required this.realm, required this.createdAt, required this.updatedAt, required this.deletedAt, required final List? members}): _members = members; + factory _SnChatRoom.fromJson(Map json) => _$SnChatRoomFromJson(json); @override final int id; @override final String name; @@ -146,41 +147,50 @@ class _SnChat implements SnChat { @override final DateTime createdAt; @override final DateTime updatedAt; @override final DateTime? deletedAt; + final List? _members; +@override List? get members { + final value = _members; + if (value == null) return null; + if (_members is EqualUnmodifiableListView) return _members; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} -/// Create a copy of SnChat + +/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -_$SnChatCopyWith<_SnChat> get copyWith => __$SnChatCopyWithImpl<_SnChat>(this, _$identity); +_$SnChatRoomCopyWith<_SnChatRoom> get copyWith => __$SnChatRoomCopyWithImpl<_SnChatRoom>(this, _$identity); @override Map toJson() { - return _$SnChatToJson(this, ); + return _$SnChatRoomToJson(this, ); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnChat&&(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.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(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)); + 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.pictureId, pictureId) || other.pictureId == pictureId)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.backgroundId, backgroundId) || other.backgroundId == backgroundId)&&(identical(other.background, background) || other.background == background)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(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)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,pictureId,picture,backgroundId,background,realmId,realm,createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,name,description,type,isPublic,pictureId,picture,backgroundId,background,realmId,realm,createdAt,updatedAt,deletedAt,const DeepCollectionEquality().hash(_members)); @override String toString() { - return 'SnChat(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnChatRoom(id: $id, name: $name, description: $description, type: $type, isPublic: $isPublic, pictureId: $pictureId, picture: $picture, backgroundId: $backgroundId, background: $background, realmId: $realmId, realm: $realm, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, members: $members)'; } } /// @nodoc -abstract mixin class _$SnChatCopyWith<$Res> implements $SnChatCopyWith<$Res> { - factory _$SnChatCopyWith(_SnChat value, $Res Function(_SnChat) _then) = __$SnChatCopyWithImpl; +abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$Res> { + factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl; @override @useResult $Res call({ - int id, String name, String description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, int? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + int id, String name, String description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, int? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members }); @@ -188,17 +198,17 @@ $Res call({ } /// @nodoc -class __$SnChatCopyWithImpl<$Res> - implements _$SnChatCopyWith<$Res> { - __$SnChatCopyWithImpl(this._self, this._then); +class __$SnChatRoomCopyWithImpl<$Res> + implements _$SnChatRoomCopyWith<$Res> { + __$SnChatRoomCopyWithImpl(this._self, this._then); - final _SnChat _self; - final $Res Function(_SnChat) _then; + final _SnChatRoom _self; + final $Res Function(_SnChatRoom) _then; -/// Create a copy of SnChat +/// 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 = null,Object? description = null,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { - return _then(_SnChat( +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { + return _then(_SnChatRoom( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable @@ -213,11 +223,12 @@ as int?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_t as SnRealm?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable 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?, +as DateTime?,members: freezed == members ? _self._members : members // ignore: cast_nullable_to_non_nullable +as List?, )); } -/// Create a copy of SnChat +/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -229,7 +240,7 @@ $SnCloudFileCopyWith<$Res>? get picture { return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) { return _then(_self.copyWith(picture: value)); }); -}/// Create a copy of SnChat +}/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -241,7 +252,7 @@ $SnCloudFileCopyWith<$Res>? get background { return $SnCloudFileCopyWith<$Res>(_self.background!, (value) { return _then(_self.copyWith(background: value)); }); -}/// Create a copy of SnChat +}/// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') @@ -655,7 +666,7 @@ $SnChatMemberCopyWith<$Res> get sender { /// @nodoc mixin _$SnChatMember { - DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; int get chatRoomId; SnChat? get chatRoom; int get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot; + DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; int get chatRoomId; SnChatRoom? get chatRoom; int get accountId; SnAccount get account; String? get nick; int get role; int get notify; DateTime? get joinedAt; bool get isBot; /// Create a copy of SnChatMember /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -688,11 +699,11 @@ abstract mixin class $SnChatMemberCopyWith<$Res> { factory $SnChatMemberCopyWith(SnChatMember value, $Res Function(SnChatMember) _then) = _$SnChatMemberCopyWithImpl; @useResult $Res call({ - DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, SnChat? chatRoom, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot + DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, SnChatRoom? chatRoom, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot }); -$SnChatCopyWith<$Res>? get chatRoom;$SnAccountCopyWith<$Res> get account; +$SnChatRoomCopyWith<$Res>? get chatRoom;$SnAccountCopyWith<$Res> get account; } /// @nodoc @@ -713,7 +724,7 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable as int,chatRoom: freezed == chatRoom ? _self.chatRoom : chatRoom // ignore: cast_nullable_to_non_nullable -as SnChat?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as SnChatRoom?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as int,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable @@ -727,12 +738,12 @@ as bool, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$SnChatCopyWith<$Res>? get chatRoom { +$SnChatRoomCopyWith<$Res>? get chatRoom { if (_self.chatRoom == null) { return null; } - return $SnChatCopyWith<$Res>(_self.chatRoom!, (value) { + return $SnChatRoomCopyWith<$Res>(_self.chatRoom!, (value) { return _then(_self.copyWith(chatRoom: value)); }); }/// Create a copy of SnChatMember @@ -760,7 +771,7 @@ class _SnChatMember implements SnChatMember { @override final DateTime? deletedAt; @override final String id; @override final int chatRoomId; -@override final SnChat? chatRoom; +@override final SnChatRoom? chatRoom; @override final int accountId; @override final SnAccount account; @override final String? nick; @@ -802,11 +813,11 @@ abstract mixin class _$SnChatMemberCopyWith<$Res> implements $SnChatMemberCopyWi factory _$SnChatMemberCopyWith(_SnChatMember value, $Res Function(_SnChatMember) _then) = __$SnChatMemberCopyWithImpl; @override @useResult $Res call({ - DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, SnChat? chatRoom, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot + DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, String id, int chatRoomId, SnChatRoom? chatRoom, int accountId, SnAccount account, String? nick, int role, int notify, DateTime? joinedAt, bool isBot }); -@override $SnChatCopyWith<$Res>? get chatRoom;@override $SnAccountCopyWith<$Res> get account; +@override $SnChatRoomCopyWith<$Res>? get chatRoom;@override $SnAccountCopyWith<$Res> get account; } /// @nodoc @@ -827,7 +838,7 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign as DateTime?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,chatRoomId: null == chatRoomId ? _self.chatRoomId : chatRoomId // ignore: cast_nullable_to_non_nullable as int,chatRoom: freezed == chatRoom ? _self.chatRoom : chatRoom // ignore: cast_nullable_to_non_nullable -as SnChat?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as SnChatRoom?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable as int,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable as SnAccount,nick: freezed == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable as String?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable @@ -842,12 +853,12 @@ as bool, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$SnChatCopyWith<$Res>? get chatRoom { +$SnChatRoomCopyWith<$Res>? get chatRoom { if (_self.chatRoom == null) { return null; } - return $SnChatCopyWith<$Res>(_self.chatRoom!, (value) { + return $SnChatRoomCopyWith<$Res>(_self.chatRoom!, (value) { return _then(_self.copyWith(chatRoom: value)); }); }/// Create a copy of SnChatMember diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index 55b72d1..ccceb27 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -6,7 +6,7 @@ part of 'chat.dart'; // JsonSerializableGenerator // ************************************************************************** -_SnChat _$SnChatFromJson(Map json) => _SnChat( +_SnChatRoom _$SnChatRoomFromJson(Map json) => _SnChatRoom( id: (json['id'] as num).toInt(), name: json['name'] as String, description: json['description'] as String, @@ -33,24 +33,30 @@ _SnChat _$SnChatFromJson(Map json) => _SnChat( json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + members: + (json['members'] as List?) + ?.map((e) => SnChatMember.fromJson(e as Map)) + .toList(), ); -Map _$SnChatToJson(_SnChat instance) => { - 'id': instance.id, - 'name': instance.name, - 'description': instance.description, - 'type': instance.type, - 'is_public': instance.isPublic, - 'picture_id': instance.pictureId, - 'picture': instance.picture?.toJson(), - 'background_id': instance.backgroundId, - 'background': instance.background?.toJson(), - 'realm_id': instance.realmId, - 'realm': instance.realm?.toJson(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'deleted_at': instance.deletedAt?.toIso8601String(), -}; +Map _$SnChatRoomToJson(_SnChatRoom instance) => + { + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + 'type': instance.type, + 'is_public': instance.isPublic, + 'picture_id': instance.pictureId, + 'picture': instance.picture?.toJson(), + 'background_id': instance.backgroundId, + 'background': instance.background?.toJson(), + 'realm_id': instance.realmId, + 'realm': instance.realm?.toJson(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'members': instance.members?.map((e) => e.toJson()).toList(), + }; _SnChatMessage _$SnChatMessageFromJson(Map json) => _SnChatMessage( @@ -152,7 +158,7 @@ _SnChatMember _$SnChatMemberFromJson(Map json) => chatRoom: json['chat_room'] == null ? null - : SnChat.fromJson(json['chat_room'] as Map), + : SnChatRoom.fromJson(json['chat_room'] as Map), accountId: (json['account_id'] as num).toInt(), account: SnAccount.fromJson(json['account'] as Map), nick: json['nick'] as String?, diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index cd3d402..b976f8d 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:auto_route/auto_route.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -12,6 +15,7 @@ import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/route.gr.dart'; import 'package:island/services/file.dart'; +import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -23,10 +27,13 @@ import 'package:styled_widget/styled_widget.dart'; part 'chat.g.dart'; @riverpod -Future> chatroomsJoined(Ref ref) async { +Future> chatroomsJoined(Ref ref) async { final client = ref.watch(apiClientProvider); final resp = await client.get('/chat'); - return resp.data.map((e) => SnChat.fromJson(e)).cast().toList(); + return resp.data + .map((e) => SnChatRoom.fromJson(e)) + .cast() + .toList(); } @RoutePage() @@ -37,6 +44,23 @@ class ChatListScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final chats = ref.watch(chatroomsJoinedProvider); + final fabKey = useMemoized(() => GlobalKey(), []); + + Future createDirectMessage() async { + final result = await showCupertinoModalBottomSheet( + context: context, + builder: (context) => AccountPickerSheet(), + ); + if (result == null) return; + final client = ref.read(apiClientProvider); + try { + await client.post('/chat/direct', data: {'related_user_id': result.id}); + ref.refresh(chatroomsJoinedProvider.future); + } catch (err) { + showErrorAlert(err); + } + } + return AppScaffold( appBar: AppBar( title: Text('chat').tr(), @@ -53,12 +77,66 @@ class ChatListScreen extends HookConsumerWidget { const Gap(8), ], ), - floatingActionButton: FloatingActionButton( - heroTag: Key("chat-page-fab"), - onPressed: () { - context.pushRoute(NewChatRoute()); - }, - child: const Icon(Symbols.add), + floatingActionButtonLocation: ExpandableFab.location, + floatingActionButton: ExpandableFab( + key: fabKey, + distance: 75, + type: ExpandableFabType.up, + childrenAnimation: ExpandableFabAnimation.none, + overlayStyle: ExpandableFabOverlayStyle( + color: Theme.of( + context, + ).colorScheme.surface.withAlpha((255 * 0.5).round()), + ), + openButtonBuilder: RotateFloatingActionButtonBuilder( + child: const Icon(Symbols.add, size: 28), + fabSize: ExpandableFabSize.regular, + foregroundColor: + Theme.of(context).floatingActionButtonTheme.foregroundColor, + backgroundColor: + Theme.of(context).floatingActionButtonTheme.backgroundColor, + ), + closeButtonBuilder: DefaultFloatingActionButtonBuilder( + heroTag: Key("chat-page-fab"), + child: const Icon(Symbols.close, size: 28), + fabSize: ExpandableFabSize.regular, + foregroundColor: + Theme.of(context).floatingActionButtonTheme.foregroundColor, + backgroundColor: + Theme.of(context).floatingActionButtonTheme.backgroundColor, + ), + children: [ + Row( + children: [ + Text('createChatRoom').tr(), + const Gap(20), + FloatingActionButton( + heroTag: null, + tooltip: 'createChatRoom'.tr(), + onPressed: () { + context.pushRoute(NewChatRoute()).then((value) { + if (value != null) { + ref.refresh(chatroomsJoinedProvider.future); + } + }); + }, + child: const Icon(Symbols.chat_add_on), + ), + ], + ), + Row( + children: [ + Text('createDirectMessage').tr(), + const Gap(20), + FloatingActionButton( + heroTag: null, + tooltip: 'createDirectMessage'.tr(), + onPressed: createDirectMessage, + child: const Icon(Symbols.communication), + ), + ], + ), + ], ), body: chats.when( data: @@ -72,6 +150,18 @@ class ChatListScreen extends HookConsumerWidget { itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; + if (item.type == 1) { + return ListTile( + leading: ProfilePictureWidget( + fileId: item.members!.first.account.profile.pictureId, + ), + title: Text(item.members!.first.account.nick), + subtitle: Text("An direct message"), + onTap: () { + context.pushRoute(ChatRoomRoute(id: item.id)); + }, + ); + } return ListTile( leading: item.pictureId == null @@ -89,18 +179,24 @@ class ChatListScreen extends HookConsumerWidget { ), ), loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center(child: Text('Error: $error')), + error: + (error, stack) => GestureDetector( + child: Center(child: Text('Error: $error')), + onTap: () { + ref.invalidate(chatroomsJoinedProvider); + }, + ), ), ); } } @riverpod -Future chatroom(Ref ref, int? identifier) async { +Future chatroom(Ref ref, int? identifier) async { if (identifier == null) return null; final client = ref.watch(apiClientProvider); final resp = await client.get('/chat/$identifier'); - return SnChat.fromJson(resp.data); + return SnChatRoom.fromJson(resp.data); } @riverpod @@ -208,7 +304,7 @@ class EditChatScreen extends HookConsumerWidget { options: Options(method: id == null ? 'POST' : 'PATCH'), ); if (context.mounted) { - context.maybePop(SnChat.fromJson(resp.data)); + context.maybePop(SnChatRoom.fromJson(resp.data)); } } catch (err) { showErrorAlert(err); diff --git a/lib/screens/chat/chat.g.dart b/lib/screens/chat/chat.g.dart index fc0d647..7b20b7b 100644 --- a/lib/screens/chat/chat.g.dart +++ b/lib/screens/chat/chat.g.dart @@ -6,12 +6,12 @@ part of 'chat.dart'; // RiverpodGenerator // ************************************************************************** -String _$chatroomsJoinedHash() => r'3a2db4159663c54dfd7bc40519e2faa6df69b41f'; +String _$chatroomsJoinedHash() => r'0c93fd3cb8fe5c87626836ced4f244bfa7598582'; /// See also [chatroomsJoined]. @ProviderFor(chatroomsJoined) final chatroomsJoinedProvider = - AutoDisposeFutureProvider>.internal( + AutoDisposeFutureProvider>.internal( chatroomsJoined, name: r'chatroomsJoinedProvider', debugGetCreateSourceHash: @@ -24,8 +24,8 @@ final chatroomsJoinedProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef>; -String _$chatroomHash() => r'27bd4cb49326bb2f2eac7d7db9db7f610e21afb2'; +typedef ChatroomsJoinedRef = AutoDisposeFutureProviderRef>; +String _$chatroomHash() => r'3a945a61ea434f860fbeae9d40778fbfceddc5db'; /// Copied from Dart SDK class _SystemHash { @@ -53,7 +53,7 @@ class _SystemHash { const chatroomProvider = ChatroomFamily(); /// See also [chatroom]. -class ChatroomFamily extends Family> { +class ChatroomFamily extends Family> { /// See also [chatroom]. const ChatroomFamily(); @@ -83,7 +83,7 @@ class ChatroomFamily extends Family> { } /// See also [chatroom]. -class ChatroomProvider extends AutoDisposeFutureProvider { +class ChatroomProvider extends AutoDisposeFutureProvider { /// See also [chatroom]. ChatroomProvider(int? identifier) : this._internal( @@ -113,7 +113,7 @@ class ChatroomProvider extends AutoDisposeFutureProvider { @override Override overrideWith( - FutureOr Function(ChatroomRef provider) create, + FutureOr Function(ChatroomRef provider) create, ) { return ProviderOverride( origin: this, @@ -130,7 +130,7 @@ class ChatroomProvider extends AutoDisposeFutureProvider { } @override - AutoDisposeFutureProviderElement createElement() { + AutoDisposeFutureProviderElement createElement() { return _ChatroomProviderElement(this); } @@ -150,12 +150,13 @@ class ChatroomProvider extends AutoDisposeFutureProvider { @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -mixin ChatroomRef on AutoDisposeFutureProviderRef { +mixin ChatroomRef on AutoDisposeFutureProviderRef { /// The parameter `identifier` of this provider. int? get identifier; } -class _ChatroomProviderElement extends AutoDisposeFutureProviderElement +class _ChatroomProviderElement + extends AutoDisposeFutureProviderElement with ChatroomRef { _ChatroomProviderElement(super.provider); diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 2fc27b7..7efa46c 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -443,19 +443,28 @@ class ChatRoomScreen extends HookConsumerWidget { height: 26, width: 26, child: - room?.pictureId != null + room!.type == 1 ? ProfilePictureWidget( - fileId: room?.pictureId, + fileId: + room.members!.first.account.profile.pictureId, + ) + : room.pictureId != null + ? ProfilePictureWidget( + fileId: room.pictureId, fallbackIcon: Symbols.chat, ) : CircleAvatar( child: Text( - room?.name[0].toUpperCase() ?? '', + room.name[0].toUpperCase(), style: const TextStyle(fontSize: 12), ), ), ), - Text(room?.name ?? 'unknown'.tr()).fontSize(19), + Text( + room!.type == 1 + ? room.members!.first.account.nick + : room.name, + ).fontSize(19), ], ), loading: () => const Text('Loading...'), @@ -613,7 +622,7 @@ class ChatRoomScreen extends HookConsumerWidget { class _ChatInput extends StatelessWidget { final TextEditingController messageController; - final SnChat chatRoom; + final SnChatRoom chatRoom; final VoidCallback onSend; final VoidCallback onClear; final Function(bool isPhoto) onPickFile; @@ -744,7 +753,12 @@ class _ChatInput extends StatelessWidget { child: TextField( controller: messageController, decoration: InputDecoration( - hintText: 'chatMessageHint'.tr(args: [chatRoom.name]), + hintText: + chatRoom.type == 1 + ? 'chatDirectMessageHint'.tr( + args: [chatRoom.members!.first.account.nick], + ) + : 'chatMessageHint'.tr(args: [chatRoom.name]), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric( diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index bc49ef9..ca13643 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -55,9 +55,26 @@ class ChatDetailScreen extends HookConsumerWidget { leading: PageBackButton(shadows: [iconShadow]), flexibleSpace: FlexibleSpaceBar( background: - currentRoom?.backgroundId != null + currentRoom!.type == 1 && + currentRoom + .members! + .first + .account + .profile + .backgroundId != + null ? CloudImageWidget( - fileId: currentRoom!.backgroundId!, + fileId: + currentRoom + .members! + .first + .account + .profile + .backgroundId!, + ) + : currentRoom.backgroundId != null + ? CloudImageWidget( + fileId: currentRoom.backgroundId!, fit: BoxFit.cover, ) : Container( @@ -65,7 +82,9 @@ class ChatDetailScreen extends HookConsumerWidget { Theme.of(context).appBarTheme.backgroundColor, ), title: Text( - currentRoom?.name ?? 'unknown'.tr(), + currentRoom.type == 1 + ? currentRoom.members!.first.account.nick + : currentRoom.name, ).textColor(Theme.of(context).appBarTheme.foregroundColor), ), actions: [ diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index 1124e3f..b0e4701 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -70,11 +70,13 @@ class ProfilePictureWidget extends ConsumerWidget { final String? fileId; final double radius; final IconData? fallbackIcon; + final Color? fallbackColor; const ProfilePictureWidget({ super.key, required this.fileId, this.radius = 20, this.fallbackIcon, + this.fallbackColor, }); @override @@ -93,6 +95,9 @@ class ProfilePictureWidget extends ConsumerWidget { ? Icon( fallbackIcon ?? Symbols.account_circle, size: radius, + color: + fallbackColor ?? + Theme.of(context).colorScheme.onPrimaryContainer, ).center() : CachedNetworkImage(imageUrl: uri, fit: BoxFit.cover), ), diff --git a/pubspec.lock b/pubspec.lock index 5bd3629..556d41d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -622,6 +622,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flutter_expandable_fab: + dependency: "direct main" + description: + name: flutter_expandable_fab + sha256: "9de10aad89ebff35956d8eb4ceb0d8749835dc1184d3ab17b721eb06c778c519" + url: "https://pub.dev" + source: hosted + version: "2.5.0" flutter_highlight: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0afe0b7..07c8795 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -89,6 +89,7 @@ dependencies: drift_flutter: ^0.2.4 path: ^1.9.1 collection: ^1.19.1 + flutter_expandable_fab: ^2.5.0 dev_dependencies: flutter_test: