From f9b2a96c7c98a07fe152f681c5ab68da00ab5288 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 25 Aug 2025 16:55:06 +0800 Subject: [PATCH] :sparkles: Pin post --- assets/i18n/en-US.json | 19 ++- lib/models/post.dart | 1 + lib/models/post.freezed.dart | 43 +++--- lib/models/post.g.dart | 2 + lib/screens/posts/pub_profile.dart | 7 +- lib/widgets/post/post_item.dart | 40 ++++++ lib/widgets/post/post_list.dart | 42 +++--- lib/widgets/post/post_list.g.dart | 28 +++- lib/widgets/post/post_pin_sheet.dart | 124 +++++++++++++++++ lib/widgets/post/post_shared.dart | 196 ++++++++++++++------------- 10 files changed, 357 insertions(+), 145 deletions(-) create mode 100644 lib/widgets/post/post_pin_sheet.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index b440f98a..5c68f126 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -386,6 +386,7 @@ "postSettings": "Settings", "postPublisherUnselected": "Publisher Unspecified", "postType": "Post Type", + "postTypePost": "Post", "articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.", "postVisibility": "Post Visibility", "postVisibilityPublic": "Public", @@ -926,5 +927,19 @@ "newSecretGenerated": "New Secret Generated", "copySecretHint": "Please copy this secret and store it somewhere safe. You will not be able to see it again.", "expiresIn": "Expires In (seconds)", - "isOidc": "OIDC Compliant" -} \ No newline at end of file + "isOidc": "OIDC Compliant", + "pinPost": "Pin Post", + "unpinPost": "Unpin Post", + "pinnedPost": "Pinned", + "publisherPage": "Publisher Page", + "realmPage": "Realm Page", + "replyPage": "Reply Page", + "pinPostPublisherHint": "Pin this post to your publisher page", + "pinPostRealmHint": "Pin this post to the realm page", + "pinPostRealmDisabledHint": "This post doesn't belong to any realm", + "pinPostReplyHint": "Pin this post to the reply page", + "pinPostReplyDisabledHint": "This post is not a reply", + "pin": "Pin", + "unpinPostHint": "Are you sure you want to unpin this post?", + "all": "All" +} diff --git a/lib/models/post.dart b/lib/models/post.dart index 75cab31c..17b4615e 100644 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -27,6 +27,7 @@ sealed class SnPost with _$SnPost { @Default(0) int upvotes, @Default(0) int downvotes, @Default(0) int repliesCount, + int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, diff --git a/lib/models/post.freezed.dart b/lib/models/post.freezed.dart index b8fad5ee..6ba78a32 100644 --- a/lib/models/post.freezed.dart +++ b/lib/models/post.freezed.dart @@ -15,7 +15,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; String? get slug; 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; String? get realmId; SnRealm? get realm; List get attachments; SnPublisher get publisher; Map get reactionsCount; Map get reactionsMade; List get reactions; List get tags; List get categories; List get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; + String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List get attachments; SnPublisher get publisher; Map get reactionsCount; Map get reactionsMade; List get reactions; List get tags; List get categories; List get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; /// Create a copy of SnPost /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,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.slug, slug) || other.slug == slug)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&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)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); + 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.slug, slug) || other.slug == slug)&&(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.pinMode, pinMode) || other.pinMode == pinMode)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&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)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]); +int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]); @override String toString() { - return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, 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, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; + return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; } @@ -48,7 +48,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, String? slug, 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, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated + String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated }); @@ -65,7 +65,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 = freezed,Object? visibility = null,Object? content = freezed,Object? slug = 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? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { 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 @@ -83,7 +83,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: 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 int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // 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 as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable @@ -242,10 +243,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, 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, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnPost() when $default != null: -return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: +return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: return orElse(); } @@ -263,10 +264,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, 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, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this; switch (_that) { case _SnPost(): -return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);} +return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);} } /// A variant of `when` that fallback to returning `null` /// @@ -280,10 +281,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, 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, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this; switch (_that) { case _SnPost() when $default != null: -return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: +return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: return null; } @@ -295,7 +296,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit @JsonSerializable() class _SnPost implements SnPost { - const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List attachments = const [], required this.publisher, final Map reactionsCount = const {}, final Map reactionsMade = const {}, final List reactions = const [], final List tags = const [], final List categories = const [], final List collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; + const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final Map? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final List attachments = const [], required this.publisher, final Map reactionsCount = const {}, final Map reactionsMade = const {}, final List reactions = const [], final List tags = const [], final List categories = const [], final List collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; factory _SnPost.fromJson(Map json) => _$SnPostFromJson(json); @override final String id; @@ -322,6 +323,7 @@ class _SnPost implements SnPost { @override@JsonKey() final int upvotes; @override@JsonKey() final int downvotes; @override@JsonKey() final int repliesCount; +@override final int? pinMode; @override final String? threadedPostId; @override final SnPost? threadedPost; @override final String? repliedPostId; @@ -398,16 +400,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.slug, slug) || other.slug == slug)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&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)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); + 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.slug, slug) || other.slug == slug)&&(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.pinMode, pinMode) || other.pinMode == pinMode)&&(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)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&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)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]); +int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]); @override String toString() { - return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, 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, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; + return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; } @@ -418,7 +420,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, String? slug, 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, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated + String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List attachments, SnPublisher publisher, Map reactionsCount, Map reactionsMade, List reactions, List tags, List categories, List collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated }); @@ -435,7 +437,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 = freezed,Object? visibility = null,Object? content = freezed,Object? slug = 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? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { 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 @@ -453,7 +455,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: 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 int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // 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 as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/post.g.dart b/lib/models/post.g.dart index 2c780d53..faa3294a 100644 --- a/lib/models/post.g.dart +++ b/lib/models/post.g.dart @@ -29,6 +29,7 @@ _SnPost _$SnPostFromJson(Map json) => _SnPost( upvotes: (json['upvotes'] as num?)?.toInt() ?? 0, downvotes: (json['downvotes'] as num?)?.toInt() ?? 0, repliesCount: (json['replies_count'] as num?)?.toInt() ?? 0, + pinMode: (json['pin_mode'] as num?)?.toInt(), threadedPostId: json['threaded_post_id'] as String?, threadedPost: json['threaded_post'] == null @@ -109,6 +110,7 @@ Map _$SnPostToJson(_SnPost instance) => { 'upvotes': instance.upvotes, 'downvotes': instance.downvotes, 'replies_count': instance.repliesCount, + 'pin_mode': instance.pinMode, 'threaded_post_id': instance.threadedPostId, 'threaded_post': instance.threadedPost?.toJson(), 'replied_post_id': instance.repliedPostId, diff --git a/lib/screens/posts/pub_profile.dart b/lib/screens/posts/pub_profile.dart index 7ecff9dd..dbc77075 100644 --- a/lib/screens/posts/pub_profile.dart +++ b/lib/screens/posts/pub_profile.dart @@ -288,7 +288,11 @@ class PublisherProfileScreen extends HookConsumerWidget { controller: categoryTabController, dividerColor: Colors.transparent, splashBorderRadius: const BorderRadius.all(Radius.circular(8)), - tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')], + tabs: [ + Tab(text: 'all'.tr()), + Tab(text: 'postTypePost'.tr()), + Tab(text: 'postArticle'.tr()), + ], ), ); @@ -345,6 +349,7 @@ class PublisherProfileScreen extends HookConsumerWidget { child: CustomScrollView( slivers: [ SliverGap(16), + SliverPostList(pubName: name, pinned: true), SliverToBoxAdapter( child: publisherCategoryTabWidget(), ), diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 7f79a849..2506cb8c 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -18,6 +18,7 @@ import 'package:island/screens/posts/compose.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/post/post_item_screenshot.dart'; +import 'package:island/widgets/post/post_pin_sheet.dart'; import 'package:island/widgets/post/post_shared.dart'; import 'package:island/widgets/safety/abuse_report_helper.dart'; import 'package:island/widgets/share/share_sheet.dart'; @@ -202,6 +203,45 @@ class PostActionableItem extends HookConsumerWidget { ); }, ), + if (isAuthor && item.pinMode == null) + MenuAction( + title: 'pinPost'.tr(), + image: MenuImage.icon(Symbols.keep), + callback: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => PostPinSheet(post: item), + ).then((value) { + if (value is int) { + onUpdate?.call(item.copyWith(pinMode: value)); + } + }); + }, + ) + else if (isAuthor && item.pinMode != null) + MenuAction( + title: 'unpinPost'.tr(), + image: MenuImage.icon(Symbols.keep_off), + callback: () { + showConfirmAlert('unpinPostHint'.tr(), 'unpinPost'.tr()).then( + (confirm) async { + if (confirm) { + final client = ref.watch(apiClientProvider); + try { + if (context.mounted) showLoadingModal(context); + await client.delete('/sphere/posts/${item.id}/pin'); + onUpdate?.call(item.copyWith(pinMode: null)); + } catch (err) { + showErrorAlert(err); + } finally { + if (context.mounted) hideLoadingModal(context); + } + } + }, + ); + }, + ), MenuSeparator(), MenuAction( title: 'share'.tr(), diff --git a/lib/widgets/post/post_list.dart b/lib/widgets/post/post_list.dart index fe79503f..cc2eba66 100644 --- a/lib/widgets/post/post_list.dart +++ b/lib/widgets/post/post_list.dart @@ -22,6 +22,7 @@ class PostListNotifier extends _$PostListNotifier List? categories, List? tags, bool shuffle = false, + bool pinned = false, }) { return fetch(cursor: null); } @@ -40,6 +41,7 @@ class PostListNotifier extends _$PostListNotifier if (tags != null) 'tags': tags, if (categories != null) 'categories': categories, if (shuffle) 'shuffle': true, + if (pinned) 'pinned': true, }; final response = await client.get( @@ -77,6 +79,7 @@ class SliverPostList extends HookConsumerWidget { final List? categories; final List? tags; final bool shuffle; + final bool pinned; final PostItemType itemType; final Color? backgroundColor; final EdgeInsets? padding; @@ -93,6 +96,7 @@ class SliverPostList extends HookConsumerWidget { this.categories, this.tags, this.shuffle = false, + this.pinned = false, this.itemType = PostItemType.regular, this.backgroundColor, this.padding, @@ -104,33 +108,19 @@ class SliverPostList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final provider = postListNotifierProvider( + pubName: pubName, + realm: realm, + type: type, + categories: categories, + tags: tags, + shuffle: shuffle, + pinned: pinned, + ); return PagingHelperSliverView( - provider: postListNotifierProvider( - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - shuffle: shuffle, - ), - futureRefreshable: - postListNotifierProvider( - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - shuffle: shuffle, - ).future, - notifierRefreshable: - postListNotifierProvider( - pubName: pubName, - realm: realm, - type: type, - categories: categories, - tags: tags, - shuffle: shuffle, - ).notifier, + provider: provider, + futureRefreshable: provider.future, + notifierRefreshable: provider.notifier, contentBuilder: (data, widgetCount, endItemView) => SliverList.builder( itemCount: widgetCount, diff --git a/lib/widgets/post/post_list.g.dart b/lib/widgets/post/post_list.g.dart index 89050911..132a0ca0 100644 --- a/lib/widgets/post/post_list.g.dart +++ b/lib/widgets/post/post_list.g.dart @@ -6,7 +6,7 @@ part of 'post_list.dart'; // RiverpodGenerator // ************************************************************************** -String _$postListNotifierHash() => r'faa0b939fae56367ff120ce63d9deb17b1995c9c'; +String _$postListNotifierHash() => r'7be076e6cee1c52c258d0fad2cd9fe9ac5e100ac'; /// Copied from Dart SDK class _SystemHash { @@ -37,6 +37,7 @@ abstract class _$PostListNotifier late final List? categories; late final List? tags; late final bool shuffle; + late final bool pinned; FutureOr> build({ String? pubName, @@ -45,6 +46,7 @@ abstract class _$PostListNotifier List? categories, List? tags, bool shuffle = false, + bool pinned = false, }); } @@ -66,6 +68,7 @@ class PostListNotifierFamily List? categories, List? tags, bool shuffle = false, + bool pinned = false, }) { return PostListNotifierProvider( pubName: pubName, @@ -74,6 +77,7 @@ class PostListNotifierFamily categories: categories, tags: tags, shuffle: shuffle, + pinned: pinned, ); } @@ -88,6 +92,7 @@ class PostListNotifierFamily categories: provider.categories, tags: provider.tags, shuffle: provider.shuffle, + pinned: provider.pinned, ); } @@ -121,6 +126,7 @@ class PostListNotifierProvider List? categories, List? tags, bool shuffle = false, + bool pinned = false, }) : this._internal( () => PostListNotifier() @@ -129,7 +135,8 @@ class PostListNotifierProvider ..type = type ..categories = categories ..tags = tags - ..shuffle = shuffle, + ..shuffle = shuffle + ..pinned = pinned, from: postListNotifierProvider, name: r'postListNotifierProvider', debugGetCreateSourceHash: @@ -145,6 +152,7 @@ class PostListNotifierProvider categories: categories, tags: tags, shuffle: shuffle, + pinned: pinned, ); PostListNotifierProvider._internal( @@ -160,6 +168,7 @@ class PostListNotifierProvider required this.categories, required this.tags, required this.shuffle, + required this.pinned, }) : super.internal(); final String? pubName; @@ -168,6 +177,7 @@ class PostListNotifierProvider final List? categories; final List? tags; final bool shuffle; + final bool pinned; @override FutureOr> runNotifierBuild( @@ -180,6 +190,7 @@ class PostListNotifierProvider categories: categories, tags: tags, shuffle: shuffle, + pinned: pinned, ); } @@ -195,7 +206,8 @@ class PostListNotifierProvider ..type = type ..categories = categories ..tags = tags - ..shuffle = shuffle, + ..shuffle = shuffle + ..pinned = pinned, from: from, name: null, dependencies: null, @@ -207,6 +219,7 @@ class PostListNotifierProvider categories: categories, tags: tags, shuffle: shuffle, + pinned: pinned, ), ); } @@ -228,7 +241,8 @@ class PostListNotifierProvider other.type == type && other.categories == categories && other.tags == tags && - other.shuffle == shuffle; + other.shuffle == shuffle && + other.pinned == pinned; } @override @@ -240,6 +254,7 @@ class PostListNotifierProvider hash = _SystemHash.combine(hash, categories.hashCode); hash = _SystemHash.combine(hash, tags.hashCode); hash = _SystemHash.combine(hash, shuffle.hashCode); + hash = _SystemHash.combine(hash, pinned.hashCode); return _SystemHash.finish(hash); } @@ -266,6 +281,9 @@ mixin PostListNotifierRef /// The parameter `shuffle` of this provider. bool get shuffle; + + /// The parameter `pinned` of this provider. + bool get pinned; } class _PostListNotifierProviderElement @@ -290,6 +308,8 @@ class _PostListNotifierProviderElement List? get tags => (origin as PostListNotifierProvider).tags; @override bool get shuffle => (origin as PostListNotifierProvider).shuffle; + @override + bool get pinned => (origin as PostListNotifierProvider).pinned; } // ignore_for_file: type=lint diff --git a/lib/widgets/post/post_pin_sheet.dart b/lib/widgets/post/post_pin_sheet.dart new file mode 100644 index 00000000..6e68182a --- /dev/null +++ b/lib/widgets/post/post_pin_sheet.dart @@ -0,0 +1,124 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/post.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class PostPinSheet extends HookConsumerWidget { + final SnPost post; + const PostPinSheet({super.key, required this.post}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mode = useState(0); + + Future pinPost() async { + try { + showLoadingModal(context); + final client = ref.watch(apiClientProvider); + await client.post( + '/sphere/posts/${post.id}/pin', + data: {'mode': mode.value}, + ); + + if (context.mounted) Navigator.of(context).pop(mode.value); + } catch (e) { + showErrorAlert(e); + } finally { + if (context.mounted) hideLoadingModal(context); + } + } + + return SheetScaffold( + titleText: 'pinPost'.tr(), + heightFactor: 0.6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Publisher page pin option (always available) + ListTile( + leading: Radio( + value: 0, + groupValue: mode.value, + onChanged: (value) { + mode.value = value!; + }, + ), + title: Text('publisherPage'.tr()), + subtitle: Text('pinPostPublisherHint'.tr()), + onTap: () { + mode.value = 0; + }, + ), + + // Realm page pin option (show always, but disabled when not available) + ListTile( + leading: Radio( + value: 1, + groupValue: mode.value, + onChanged: + post.realmId != null && post.realmId!.isNotEmpty + ? (value) { + mode.value = value!; + } + : null, + ), + title: Text('realmPage'.tr()), + subtitle: + post.realmId != null && post.realmId!.isNotEmpty + ? Text('pinPostRealmHint'.tr()) + : Text('pinPostRealmDisabledHint'.tr()), + onTap: + post.realmId != null && post.realmId!.isNotEmpty + ? () { + mode.value = 1; + } + : null, + enabled: post.realmId != null && post.realmId!.isNotEmpty, + ), + + // Reply page pin option (show always, but disabled when not available) + ListTile( + leading: Radio( + value: 2, + groupValue: mode.value, + onChanged: + post.repliedPostId != null && post.repliedPostId!.isNotEmpty + ? (value) { + mode.value = value!; + } + : null, + ), + title: Text('replyPage'.tr()), + subtitle: + post.repliedPostId != null && post.repliedPostId!.isNotEmpty + ? Text('pinPostReplyHint'.tr()) + : Text('pinPostReplyDisabledHint'.tr()), + onTap: + post.repliedPostId != null && post.repliedPostId!.isNotEmpty + ? () { + mode.value = 2; + } + : null, + enabled: + post.repliedPostId != null && post.repliedPostId!.isNotEmpty, + ), + + const SizedBox(height: 16), + + // Pin button + FilledButton.icon( + onPressed: pinPost, + icon: const Icon(Symbols.keep), + label: Text('pin'.tr()), + ).padding(horizontal: 24), + ], + ), + ); + } +} diff --git a/lib/widgets/post/post_shared.dart b/lib/widgets/post/post_shared.dart index 8124b91c..7a2a1f45 100644 --- a/lib/widgets/post/post_shared.dart +++ b/lib/widgets/post/post_shared.dart @@ -545,107 +545,119 @@ class PostHeader extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 12, + return Column( children: [ - GestureDetector( - onTap: - isInteractive - ? () { - context.pushNamed( - 'publisherProfile', - pathParameters: {'name': item.publisher.name}, - ); - } - : null, - child: ProfilePictureWidget( - file: item.publisher.picture, - radius: 16, - borderRadius: item.publisher.type == 0 ? null : 6, - ), - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + if (item.pinMode != null) + Row( + spacing: 4, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 4, + const Icon(Symbols.keep, size: 15, fill: 1), + Text('pinnedPost').tr().fontSize(13), + ], + ).opacity(0.8).padding(horizontal: 8, bottom: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12, + children: [ + GestureDetector( + onTap: + isInteractive + ? () { + context.pushNamed( + 'publisherProfile', + pathParameters: {'name': item.publisher.name}, + ); + } + : null, + child: ProfilePictureWidget( + file: item.publisher.picture, + radius: 16, + borderRadius: item.publisher.type == 0 ? null : 6, + ), + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(item.publisher.nick).bold(), - if (item.publisher.verification != null) - VerificationMark(mark: item.publisher.verification!), - if (item.realm == null) - Text('@${item.publisher.name}').fontSize(11) - else - ...([ - const Icon(Symbols.arrow_right, size: 14), - Flexible( - child: InkWell( - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 5, - children: [ - Flexible( - child: Text( - item.realm!.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ProfilePictureWidget( - file: item.realm!.picture, - fallbackIcon: Symbols.group, - radius: 9, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 4, + children: [ + Text(item.publisher.nick).bold(), + if (item.publisher.verification != null) + VerificationMark(mark: item.publisher.verification!), + if (item.realm == null) + Text('@${item.publisher.name}').fontSize(11) + else + ...([ + const Icon(Symbols.arrow_right, size: 14), + Flexible( + child: InkWell( + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 5, + children: [ + Flexible( + child: Text( + item.realm!.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ProfilePictureWidget( + file: item.realm!.picture, + fallbackIcon: Symbols.group, + radius: 9, + ), + ], ), + onTap: () { + GoRouter.of(context).pushNamed( + 'realmDetail', + pathParameters: {'slug': item.realm!.slug}, + ); + }, + ), + ), + ]), + ], + ), + Row( + spacing: 6, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + !isFullPost && isRelativeTime + ? (item.publishedAt ?? item.createdAt)! + .formatRelative(context) + : (item.publishedAt ?? item.createdAt)! + .formatSystem(), + ).fontSize(10), + if (item.editedAt != null) + Text( + 'editedAt'.tr( + args: [ + !isFullPost && isRelativeTime + ? item.editedAt!.formatRelative(context) + : item.editedAt!.formatSystem(), ], ), - onTap: () { - GoRouter.of(context).pushNamed( - 'realmDetail', - pathParameters: {'slug': item.realm!.slug}, - ); - }, - ), - ), - ]), + ).fontSize(10), + if (item.visibility != 0) + Text( + PostVisibilityHelpers.getVisibilityText( + item.visibility, + ).tr(), + ).fontSize(10), + ], + ), ], ), - Row( - spacing: 6, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - !isFullPost && isRelativeTime - ? (item.publishedAt ?? item.createdAt)!.formatRelative( - context, - ) - : (item.publishedAt ?? item.createdAt)!.formatSystem(), - ).fontSize(10), - if (item.editedAt != null) - Text( - 'editedAt'.tr( - args: [ - !isFullPost && isRelativeTime - ? item.editedAt!.formatRelative(context) - : item.editedAt!.formatSystem(), - ], - ), - ).fontSize(10), - if (item.visibility != 0) - Text( - PostVisibilityHelpers.getVisibilityText( - item.visibility, - ).tr(), - ).fontSize(10), - ], - ), - ], - ), + ), + if (trailing != null) trailing!, + ], ), - if (trailing != null) trailing!, ], ).padding(horizontal: renderingPadding.horizontal, bottom: 4); }