diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 071e367..20596e5 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -100,6 +100,11 @@ "permissionModerator": "Moderator", "permissionMember": "Member", "reply": "Reply", + "repliesCount": { + "zero": "No reply", + "one": "{} reply", + "other": "{} replies" + }, "forward": "Forward", "repliedTo": "Replied to", "forwarded": "Forwarded", diff --git a/lib/models/post.dart b/lib/models/post.dart index 98bfd38..e213b41 100644 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -22,6 +22,7 @@ sealed class SnPost with _$SnPost { required int viewsTotal, required int upvotes, required int downvotes, + required int repliesCount, required String? threadedPostId, required SnPost? threadedPost, required String? repliedPostId, diff --git a/lib/models/post.freezed.dart b/lib/models/post.freezed.dart index bcf5b04..51ee8e7 100644 --- a/lib/models/post.freezed.dart +++ b/lib/models/post.freezed.dart @@ -16,7 +16,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$SnPost { - String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime get publishedAt; int get visibility; String? get content; int get type; Map? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List get attachments; SnPublisher get publisher; Map get reactionsCount; List get reactions; List get tags; List get categories; List get collections; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime get publishedAt; int get visibility; String? get content; int get type; Map? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List get attachments; SnPublisher get publisher; Map get reactionsCount; List get reactions; List get tags; List get categories; List get collections; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnPost /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -29,16 +29,16 @@ $SnPostCopyWith get copyWith => _$SnPostCopyWithImpl(this as SnP @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(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 SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt]); +int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res> { factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; @useResult $Res call({ - String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List attachments, SnPublisher publisher, Map reactionsCount, List reactions, List tags, List categories, List collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List attachments, SnPublisher publisher, Map reactionsCount, List reactions, List tags, List categories, List collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -66,7 +66,7 @@ class _$SnPostCopyWithImpl<$Res> /// Create a copy of SnPost /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable @@ -82,6 +82,7 @@ as Map?,viewsUnique: null == viewsUnique ? _self.viewsUnique : as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable +as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable @@ -154,7 +155,7 @@ $SnPublisherCopyWith<$Res> get publisher { @JsonSerializable() class _SnPost implements SnPost { - const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final Map? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final List attachments, required this.publisher, final Map reactionsCount = const {}, required final List reactions, required final List tags, required final List categories, required final List collections, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; + const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final Map? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.repliesCount, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final List attachments, required this.publisher, final Map reactionsCount = const {}, required final List reactions, required final List tags, required final List categories, required final List collections, required this.createdAt, required this.updatedAt, required this.deletedAt}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; factory _SnPost.fromJson(Map json) => _$SnPostFromJson(json); @override final String id; @@ -179,6 +180,7 @@ class _SnPost implements SnPost { @override final int viewsTotal; @override final int upvotes; @override final int downvotes; +@override final int repliesCount; @override final String? threadedPostId; @override final SnPost? threadedPost; @override final String? repliedPostId; @@ -245,16 +247,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(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 _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt]); +int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt]); @override String toString() { - return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -265,7 +267,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; @override @useResult $Res call({ - String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List attachments, SnPublisher publisher, Map reactionsCount, List reactions, List tags, List categories, List collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List attachments, SnPublisher publisher, Map reactionsCount, List reactions, List tags, List categories, List collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -282,7 +284,7 @@ class __$SnPostCopyWithImpl<$Res> /// Create a copy of SnPost /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnPost( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable @@ -298,6 +300,7 @@ as Map?,viewsUnique: null == viewsUnique ? _self.viewsUnique : as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: cast_nullable_to_non_nullable as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable +as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/post.g.dart b/lib/models/post.g.dart index 6f4f24f..f195c61 100644 --- a/lib/models/post.g.dart +++ b/lib/models/post.g.dart @@ -24,6 +24,7 @@ _SnPost _$SnPostFromJson(Map json) => _SnPost( viewsTotal: (json['views_total'] as num).toInt(), upvotes: (json['upvotes'] as num).toInt(), downvotes: (json['downvotes'] as num).toInt(), + repliesCount: (json['replies_count'] as num).toInt(), threadedPostId: json['threaded_post_id'] as String?, threadedPost: json['threaded_post'] == null @@ -76,6 +77,7 @@ Map _$SnPostToJson(_SnPost instance) => { 'views_total': instance.viewsTotal, 'upvotes': instance.upvotes, 'downvotes': instance.downvotes, + 'replies_count': instance.repliesCount, 'threaded_post_id': instance.threadedPostId, 'threaded_post': instance.threadedPost?.toJson(), 'replied_post_id': instance.repliedPostId, diff --git a/lib/route.gr.dart b/lib/route.gr.dart index fb1931c..9b877dc 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -182,17 +182,10 @@ class ArticleComposeRoute extends _i29.PageRouteInfo { ArticleComposeRoute({ _i30.Key? key, _i32.SnPost? originalPost, - _i32.SnPost? repliedPost, - _i32.SnPost? forwardedPost, List<_i29.PageRouteInfo>? children, }) : super( ArticleComposeRoute.name, - args: ArticleComposeRouteArgs( - key: key, - originalPost: originalPost, - repliedPost: repliedPost, - forwardedPost: forwardedPost, - ), + args: ArticleComposeRouteArgs(key: key, originalPost: originalPost), initialChildren: children, ); @@ -213,42 +206,26 @@ class ArticleComposeRoute extends _i29.PageRouteInfo { } class ArticleComposeRouteArgs { - const ArticleComposeRouteArgs({ - this.key, - this.originalPost, - this.repliedPost, - this.forwardedPost, - }); + const ArticleComposeRouteArgs({this.key, this.originalPost}); final _i30.Key? key; final _i32.SnPost? originalPost; - final _i32.SnPost? repliedPost; - - final _i32.SnPost? forwardedPost; - @override String toString() { - return 'ArticleComposeRouteArgs{key: $key, originalPost: $originalPost, repliedPost: $repliedPost, forwardedPost: $forwardedPost}'; + return 'ArticleComposeRouteArgs{key: $key, originalPost: $originalPost}'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! ArticleComposeRouteArgs) return false; - return key == other.key && - originalPost == other.originalPost && - repliedPost == other.repliedPost && - forwardedPost == other.forwardedPost; + return key == other.key && originalPost == other.originalPost; } @override - int get hashCode => - key.hashCode ^ - originalPost.hashCode ^ - repliedPost.hashCode ^ - forwardedPost.hashCode; + int get hashCode => key.hashCode ^ originalPost.hashCode; } /// generated route for diff --git a/lib/widgets/content/paging_helper_ext.dart b/lib/widgets/content/paging_helper_ext.dart deleted file mode 100644 index 2fb44dd..0000000 --- a/lib/widgets/content/paging_helper_ext.dart +++ /dev/null @@ -1,236 +0,0 @@ -// ignore_for_file: implementation_imports, invalid_use_of_internal_member - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:riverpod_paging_utils/src/paging_data.dart'; -import 'package:riverpod_paging_utils/src/paging_helper_view_theme.dart'; -import 'package:riverpod_paging_utils/src/paging_notifier_mixin.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -/// A generic widget for pagination. -/// -/// Main features: -/// 1. Displays the widget created by [contentBuilder] when data is available. -/// 2. Shows a CircularProgressIndicator while loading the first page. -/// 3. Displays an error widget when there is an error on the first page. -/// 4. Shows error messages using a SnackBar. -/// 5. Loads the next page when the last item is displayed. -/// 6. Supports pull-to-refresh functionality. -/// -/// You can customize the appearance of the loading view, error view, and endItemView using [PagingHelperViewTheme]. -final class PagingHelperSliverView, I> - extends ConsumerWidget { - const PagingHelperSliverView({ - required this.provider, - required this.futureRefreshable, - required this.notifierRefreshable, - required this.contentBuilder, - this.showSecondPageError = true, - super.key, - }); - - final ProviderListenable> provider; - final Refreshable> futureRefreshable; - final Refreshable> notifierRefreshable; - - /// Specifies a function that returns a widget to display when data is available. - /// endItemView is a widget to detect when the last displayed item is visible. - /// If endItemView is non-null, it is displayed at the end of the list. - final Widget Function(D data, int widgetCount, Widget endItemView) - contentBuilder; - - final bool showSecondPageError; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context).extension(); - - final loadingBuilder = - theme?.loadingViewBuilder ?? - (context) => SliverFillRemaining( - child: const Center(child: CircularProgressIndicator()), - ); - final errorBuilder = - theme?.errorViewBuilder ?? - (context, e, st, onPressed) => SliverFillRemaining( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: onPressed, - icon: const Icon(Icons.refresh), - ), - Text(e.toString()), - ], - ), - ), - ); - - return ref - .watch(provider) - .whenIgnorableError( - data: ( - data, { - required hasError, - required isLoading, - required error, - }) { - final content = contentBuilder( - data, - // Add 1 to the length to include the endItemView - data.items.length + 1, - switch ((data.hasMore, hasError, isLoading)) { - // Display a widget to detect when the last element is reached - // if there are more pages and no errors - (true, false, _) => _EndVDLoadingItemView( - onScrollEnd: - () async => ref.read(notifierRefreshable).loadNext(), - ), - (true, true, false) when showSecondPageError => - _EndErrorItemView( - error: error, - onRetryButtonPressed: - () async => ref.read(notifierRefreshable).loadNext(), - ), - (true, true, true) => const _EndLoadingItemView(), - _ => const SizedBox.shrink(), - }, - ); - - return content; - }, - // Loading state for the first page - loading: () => loadingBuilder(context), - // Error state for the first page - error: - (e, st) => errorBuilder( - context, - e, - st, - () => ref.read(notifierRefreshable).forceRefresh(), - ), - // Prioritize data for errors on the second page and beyond - skipErrorOnHasValue: true, - ); - } -} - -final class _EndLoadingItemView extends StatelessWidget { - const _EndLoadingItemView(); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).extension(); - final childBuilder = - theme?.endLoadingViewBuilder ?? - (context) => const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ); - - return childBuilder(context); - } -} - -final class _EndVDLoadingItemView extends StatelessWidget { - const _EndVDLoadingItemView({required this.onScrollEnd}); - final VoidCallback onScrollEnd; - - @override - Widget build(BuildContext context) { - return VisibilityDetector( - key: key ?? const Key('EndItem'), - onVisibilityChanged: (info) { - if (info.visibleFraction > 0.1) { - onScrollEnd(); - } - }, - child: const _EndLoadingItemView(), - ); - } -} - -final class _EndErrorItemView extends StatelessWidget { - const _EndErrorItemView({ - required this.error, - required this.onRetryButtonPressed, - }); - final Object? error; - final VoidCallback onRetryButtonPressed; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).extension(); - final childBuilder = - theme?.endErrorViewBuilder ?? - (context, e, onPressed) => Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - IconButton( - onPressed: onPressed, - icon: const Icon(Icons.refresh), - ), - Text(error.toString()), - ], - ), - ), - ); - - return childBuilder(context, error, onRetryButtonPressed); - } -} - -extension _AsyncValueX on AsyncValue { - /// Extends the [when] method to handle async data states more effectively, - /// especially when maintaining data integrity despite errors. - /// - /// Use `skipErrorOnHasValue` to retain and display existing data - /// even if subsequent fetch attempts result in errors, - /// ideal for maintaining a seamless user experience. - R whenIgnorableError({ - required R Function( - T data, { - required bool hasError, - required bool isLoading, - required Object? error, - }) - data, - required R Function(Object error, StackTrace stackTrace) error, - required R Function() loading, - bool skipLoadingOnReload = false, - bool skipLoadingOnRefresh = true, - bool skipError = false, - bool skipErrorOnHasValue = false, - }) { - if (skipErrorOnHasValue) { - if (hasValue && hasError) { - return data( - requireValue, - hasError: true, - isLoading: isLoading, - error: this.error, - ); - } - } - - return when( - skipLoadingOnReload: skipLoadingOnReload, - skipLoadingOnRefresh: skipLoadingOnRefresh, - skipError: skipError, - data: - (d) => data( - d, - hasError: hasError, - isLoading: isLoading, - error: this.error, - ), - error: error, - loading: loading, - ); - } -} diff --git a/lib/widgets/post/compose_shared.dart b/lib/widgets/post/compose_shared.dart index c01fb04..7e50179 100644 --- a/lib/widgets/post/compose_shared.dart +++ b/lib/widgets/post/compose_shared.dart @@ -213,7 +213,7 @@ class ComposeLogic { // Prepare API request final client = ref.watch(apiClientProvider); final isNewPost = originalPost == null; - final endpoint = isNewPost ? '/posts' : '/posts/${originalPost!.id}'; + final endpoint = isNewPost ? '/posts' : '/posts/${originalPost.id}'; // Create request payload final payload = { diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 2a91475..8193d57 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -19,6 +19,7 @@ import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/markdown.dart'; +import 'package:island/widgets/post/post_replies_sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:super_context_menu/super_context_menu.dart'; @@ -235,18 +236,52 @@ class PostItem extends HookConsumerWidget { ), ], ), - PostReactionList( - parentId: item.id, - reactions: item.reactionsCount, - padding: EdgeInsets.only(left: 48), - onReact: (symbol, attitude, delta) { - final reactionsCount = Map.from( - item.reactionsCount, - ); - reactionsCount[symbol] = - (reactionsCount[symbol] ?? 0) + delta; - onUpdate?.call(item.copyWith(reactionsCount: reactionsCount)); - }, + Row( + children: [ + // Replies count button + Padding( + padding: const EdgeInsets.only(left: 48, right: 12), + child: ActionChip( + avatar: Icon(Symbols.reply, size: 16), + label: Text( + (item.repliesCount > 0) + ? 'repliesCount'.plural(item.repliesCount) + : 'reply'.tr(), + ), + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + onPressed: () { + if (isOpenable) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => PostRepliesSheet(post: item), + ); + } + }, + ), + ), + // Reactions list + Expanded( + child: PostReactionList( + parentId: item.id, + reactions: item.reactionsCount, + padding: EdgeInsets.zero, + onReact: (symbol, attitude, delta) { + final reactionsCount = Map.from( + item.reactionsCount, + ); + reactionsCount[symbol] = + (reactionsCount[symbol] ?? 0) + delta; + onUpdate?.call( + item.copyWith(reactionsCount: reactionsCount), + ); + }, + ), + ), + ], ), ], ), diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index 11282eb..6f48fa7 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; -import 'package:island/widgets/content/paging_helper_ext.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/post/post_item_creator.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/widgets/post/post_replies.dart b/lib/widgets/post/post_replies.dart index 6b97619..0b100ba 100644 --- a/lib/widgets/post/post_replies.dart +++ b/lib/widgets/post/post_replies.dart @@ -3,7 +3,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/post.dart'; import 'package:island/pods/network.dart'; import 'package:island/services/responsive.dart'; -import 'package:island/widgets/content/paging_helper_ext.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -57,7 +56,8 @@ class PostRepliesNotifier extends _$PostRepliesNotifier class PostRepliesList extends HookConsumerWidget { final String postId; - const PostRepliesList({super.key, required this.postId}); + final Color? backgroundColor; + const PostRepliesList({super.key, required this.postId, this.backgroundColor}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -93,7 +93,7 @@ class PostRepliesList extends HookConsumerWidget { children: [ PostItem( item: data.items[index], - backgroundColor: isWide ? Colors.transparent : null, + backgroundColor: backgroundColor ?? (isWide ? Colors.transparent : null), showReferencePost: false, ), const Divider(height: 1), diff --git a/lib/widgets/post/post_replies_sheet.dart b/lib/widgets/post/post_replies_sheet.dart new file mode 100644 index 0000000..1410c33 --- /dev/null +++ b/lib/widgets/post/post_replies_sheet.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/post.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/post/post_replies.dart'; +import 'package:island/widgets/post/post_quick_reply.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class PostRepliesSheet extends HookConsumerWidget { + final SnPost post; + + const PostRepliesSheet({super.key, required this.post}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SheetScaffold( + titleText: 'repliesCount'.plural(post.repliesCount), + child: Column( + children: [ + // Replies list + Expanded( + child: CustomScrollView( + slivers: [PostRepliesList( + postId: post.id.toString(), + backgroundColor: Colors.transparent, + )], + ), + ), + // Quick reply section + Material( + elevation: 2, + child: PostQuickReply( + parent: post, + onPosted: () { + ref.invalidate(postRepliesNotifierProvider(post.id)); + }, + ).padding( + bottom: MediaQuery.of(context).padding.bottom + 16, + top: 16, + horizontal: 16, + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 8fcaca5..0f01142 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "7cf79af8eb6023bee797a77b067fb6e63ac5650f3789546e023958098feb776e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.2" build_config: dependency: transitive description: @@ -173,26 +173,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: "7a507e6026abe52074836d51a945bfad456daa7493eb7a6cac565e490e7d5b54" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "1ce1e5063b564f26c27bda54c82a3d38339df69ec58f90e0017f447de77e4839" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.5.2" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "564230f3fd9363df7870058fef11ec5502ee620aec3b1ee8106b943be5c63a76" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.1.0" built_collection: dependency: transitive description: @@ -453,18 +453,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53" + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "11.5.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "7.0.3" dio: dependency: "direct main" description: @@ -895,10 +895,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -1217,10 +1217,10 @@ packages: dependency: transitive description: name: lean_builder - sha256: ac129cd2173aa4e53e1327bcee2233d738d68ee446f3c797135633deafe6ca8a + sha256: dca2165cfe681c69ae903a0880cab90ee93d730777605a0f44c9dd08cec7e1b9 url: "https://pub.dev" source: hosted - version: "0.1.0-alpha.12" + version: "0.1.0-alpha.13" lint: dependency: transitive description: @@ -1769,10 +1769,10 @@ packages: dependency: "direct main" description: name: riverpod_paging_utils - sha256: "18f59960807835b1d3cb993e825442d7b09928d0f55ad50bda65c002b5893bdc" + sha256: a3eb7cc87d53d90dac9bf0b0d695ecdc049aae5dd6debd7d2d62ab3682cf5841 url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.8.1" rxdart: dependency: transitive description: @@ -2509,4 +2509,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.4" + flutter: ">=3.29.0"