From af044a86bc78eeb6964aba0b89435cf7b4f42abc Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Wed, 13 Nov 2024 22:05:40 +0800
Subject: [PATCH] :sparkles: Quoted (repost) post

---
 lib/screens/post/post_detail.dart |  35 +++--
 lib/types/post.dart               |  14 +-
 lib/types/post.freezed.dart       | 217 +++++++++++++++++++-----------
 lib/types/post.g.dart             |  30 +++--
 lib/widgets/post/post_item.dart   | 188 +++++++++++++++++---------
 5 files changed, 312 insertions(+), 172 deletions(-)

diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart
index 4ded56d..49ca251 100644
--- a/lib/screens/post/post_detail.dart
+++ b/lib/screens/post/post_detail.dart
@@ -47,13 +47,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
         resp.data['body']['attachments']?.cast<String>() ?? [],
       );
       if (!mounted) return;
-      setState(() {
-        _data = SnPost.fromJson(resp.data).copyWith(
-          preload: SnPostPreload(
-            attachments: attachments,
-          ),
-        );
-      });
+      _data = SnPost.fromJson(resp.data).copyWith(
+        preload: SnPostPreload(
+          attachments: attachments,
+        ),
+      );
     } catch (err) {
       context.showErrorDialog(err);
     } finally {
@@ -87,13 +85,19 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
           },
         ),
         flexibleSpace: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
           children: [
-            Text(_data?.body['title'] ?? 'postNoun'.tr())
-                .textStyle(Theme.of(context).textTheme.titleLarge!)
-                .textColor(Colors.white),
-            Text('postDetail')
-                .tr()
-                .textColor(Colors.white.withAlpha((255 * 0.9).round())),
+            if (_data?.body['title'] != null)
+              Text(_data?.body['title'] ?? 'postNoun'.tr())
+                  .textStyle(Theme.of(context).textTheme.titleLarge!)
+                  .textColor(Colors.white),
+            if (_data?.body['title'] != null)
+              Text('postDetail'.tr())
+                  .textColor(Colors.white.withAlpha((255 * 0.9).round()))
+            else
+              Text('postDetail'.tr())
+                  .textStyle(Theme.of(context).textTheme.titleLarge!)
+                  .textColor(Colors.white),
           ],
         ).padding(top: math.max(MediaQuery.of(context).padding.top, 8)),
       ),
@@ -104,7 +108,10 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
           ),
           if (_data != null)
             SliverToBoxAdapter(
-              child: PostItem(data: _data!, showComments: false),
+              child: PostItem(
+                data: _data!,
+                showComments: false,
+              ),
             ),
           const SliverToBoxAdapter(child: Divider(height: 1)),
           if (_data != null)
diff --git a/lib/types/post.dart b/lib/types/post.dart
index 8107f8f..b57dfce 100644
--- a/lib/types/post.dart
+++ b/lib/types/post.dart
@@ -20,13 +20,13 @@ class SnPost with _$SnPost {
     required String? aliasPrefix,
     required List<dynamic> tags,
     required List<dynamic> categories,
-    required dynamic replies,
-    required dynamic replyId,
-    required dynamic repostId,
-    required dynamic replyTo,
-    required dynamic repostTo,
-    required dynamic visibleUsersList,
-    required dynamic invisibleUsersList,
+    required List<SnPost>? replies,
+    required int? replyId,
+    required int? repostId,
+    required SnPost? replyTo,
+    required SnPost? repostTo,
+    required List<int>? visibleUsersList,
+    required List<int>? invisibleUsersList,
     required int visibility,
     required DateTime? editedAt,
     required DateTime? pinnedAt,
diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart
index 2f59d58..507556e 100644
--- a/lib/types/post.freezed.dart
+++ b/lib/types/post.freezed.dart
@@ -31,13 +31,13 @@ mixin _$SnPost {
   String? get aliasPrefix => throw _privateConstructorUsedError;
   List<dynamic> get tags => throw _privateConstructorUsedError;
   List<dynamic> get categories => throw _privateConstructorUsedError;
-  dynamic get replies => throw _privateConstructorUsedError;
-  dynamic get replyId => throw _privateConstructorUsedError;
-  dynamic get repostId => throw _privateConstructorUsedError;
-  dynamic get replyTo => throw _privateConstructorUsedError;
-  dynamic get repostTo => throw _privateConstructorUsedError;
-  dynamic get visibleUsersList => throw _privateConstructorUsedError;
-  dynamic get invisibleUsersList => throw _privateConstructorUsedError;
+  List<SnPost>? get replies => throw _privateConstructorUsedError;
+  int? get replyId => throw _privateConstructorUsedError;
+  int? get repostId => throw _privateConstructorUsedError;
+  SnPost? get replyTo => throw _privateConstructorUsedError;
+  SnPost? get repostTo => throw _privateConstructorUsedError;
+  List<int>? get visibleUsersList => throw _privateConstructorUsedError;
+  List<int>? get invisibleUsersList => throw _privateConstructorUsedError;
   int get visibility => throw _privateConstructorUsedError;
   DateTime? get editedAt => throw _privateConstructorUsedError;
   DateTime? get pinnedAt => throw _privateConstructorUsedError;
@@ -78,13 +78,13 @@ abstract class $SnPostCopyWith<$Res> {
       String? aliasPrefix,
       List<dynamic> tags,
       List<dynamic> categories,
-      dynamic replies,
-      dynamic replyId,
-      dynamic repostId,
-      dynamic replyTo,
-      dynamic repostTo,
-      dynamic visibleUsersList,
-      dynamic invisibleUsersList,
+      List<SnPost>? replies,
+      int? replyId,
+      int? repostId,
+      SnPost? replyTo,
+      SnPost? repostTo,
+      List<int>? visibleUsersList,
+      List<int>? invisibleUsersList,
       int visibility,
       DateTime? editedAt,
       DateTime? pinnedAt,
@@ -99,6 +99,8 @@ abstract class $SnPostCopyWith<$Res> {
       SnMetric metric,
       SnPostPreload? preload});
 
+  $SnPostCopyWith<$Res>? get replyTo;
+  $SnPostCopyWith<$Res>? get repostTo;
   $SnPublisherCopyWith<$Res> get publisher;
   $SnMetricCopyWith<$Res> get metric;
   $SnPostPreloadCopyWith<$Res>? get preload;
@@ -199,31 +201,31 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
       replies: freezed == replies
           ? _value.replies
           : replies // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as List<SnPost>?,
       replyId: freezed == replyId
           ? _value.replyId
           : replyId // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as int?,
       repostId: freezed == repostId
           ? _value.repostId
           : repostId // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as int?,
       replyTo: freezed == replyTo
           ? _value.replyTo
           : replyTo // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as SnPost?,
       repostTo: freezed == repostTo
           ? _value.repostTo
           : repostTo // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as SnPost?,
       visibleUsersList: freezed == visibleUsersList
           ? _value.visibleUsersList
           : visibleUsersList // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as List<int>?,
       invisibleUsersList: freezed == invisibleUsersList
           ? _value.invisibleUsersList
           : invisibleUsersList // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as List<int>?,
       visibility: null == visibility
           ? _value.visibility
           : visibility // ignore: cast_nullable_to_non_nullable
@@ -279,6 +281,34 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
     ) as $Val);
   }
 
+  /// Create a copy of SnPost
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $SnPostCopyWith<$Res>? get replyTo {
+    if (_value.replyTo == null) {
+      return null;
+    }
+
+    return $SnPostCopyWith<$Res>(_value.replyTo!, (value) {
+      return _then(_value.copyWith(replyTo: value) as $Val);
+    });
+  }
+
+  /// Create a copy of SnPost
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $SnPostCopyWith<$Res>? get repostTo {
+    if (_value.repostTo == null) {
+      return null;
+    }
+
+    return $SnPostCopyWith<$Res>(_value.repostTo!, (value) {
+      return _then(_value.copyWith(repostTo: value) as $Val);
+    });
+  }
+
   /// Create a copy of SnPost
   /// with the given fields replaced by the non-null parameter values.
   @override
@@ -333,13 +363,13 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
       String? aliasPrefix,
       List<dynamic> tags,
       List<dynamic> categories,
-      dynamic replies,
-      dynamic replyId,
-      dynamic repostId,
-      dynamic replyTo,
-      dynamic repostTo,
-      dynamic visibleUsersList,
-      dynamic invisibleUsersList,
+      List<SnPost>? replies,
+      int? replyId,
+      int? repostId,
+      SnPost? replyTo,
+      SnPost? repostTo,
+      List<int>? visibleUsersList,
+      List<int>? invisibleUsersList,
       int visibility,
       DateTime? editedAt,
       DateTime? pinnedAt,
@@ -354,6 +384,10 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
       SnMetric metric,
       SnPostPreload? preload});
 
+  @override
+  $SnPostCopyWith<$Res>? get replyTo;
+  @override
+  $SnPostCopyWith<$Res>? get repostTo;
   @override
   $SnPublisherCopyWith<$Res> get publisher;
   @override
@@ -453,33 +487,33 @@ class __$$SnPostImplCopyWithImpl<$Res>
           : categories // ignore: cast_nullable_to_non_nullable
               as List<dynamic>,
       replies: freezed == replies
-          ? _value.replies
+          ? _value._replies
           : replies // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as List<SnPost>?,
       replyId: freezed == replyId
           ? _value.replyId
           : replyId // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as int?,
       repostId: freezed == repostId
           ? _value.repostId
           : repostId // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as int?,
       replyTo: freezed == replyTo
           ? _value.replyTo
           : replyTo // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as SnPost?,
       repostTo: freezed == repostTo
           ? _value.repostTo
           : repostTo // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as SnPost?,
       visibleUsersList: freezed == visibleUsersList
-          ? _value.visibleUsersList
+          ? _value._visibleUsersList
           : visibleUsersList // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as List<int>?,
       invisibleUsersList: freezed == invisibleUsersList
-          ? _value.invisibleUsersList
+          ? _value._invisibleUsersList
           : invisibleUsersList // ignore: cast_nullable_to_non_nullable
-              as dynamic,
+              as List<int>?,
       visibility: null == visibility
           ? _value.visibility
           : visibility // ignore: cast_nullable_to_non_nullable
@@ -551,13 +585,13 @@ class _$SnPostImpl extends _SnPost {
       required this.aliasPrefix,
       required final List<dynamic> tags,
       required final List<dynamic> categories,
-      required this.replies,
+      required final List<SnPost>? replies,
       required this.replyId,
       required this.repostId,
       required this.replyTo,
       required this.repostTo,
-      required this.visibleUsersList,
-      required this.invisibleUsersList,
+      required final List<int>? visibleUsersList,
+      required final List<int>? invisibleUsersList,
       required this.visibility,
       required this.editedAt,
       required this.pinnedAt,
@@ -574,6 +608,9 @@ class _$SnPostImpl extends _SnPost {
       : _body = body,
         _tags = tags,
         _categories = categories,
+        _replies = replies,
+        _visibleUsersList = visibleUsersList,
+        _invisibleUsersList = invisibleUsersList,
         super._();
 
   factory _$SnPostImpl.fromJson(Map<String, dynamic> json) =>
@@ -619,20 +656,46 @@ class _$SnPostImpl extends _SnPost {
     return EqualUnmodifiableListView(_categories);
   }
 
+  final List<SnPost>? _replies;
   @override
-  final dynamic replies;
+  List<SnPost>? get replies {
+    final value = _replies;
+    if (value == null) return null;
+    if (_replies is EqualUnmodifiableListView) return _replies;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(value);
+  }
+
   @override
-  final dynamic replyId;
+  final int? replyId;
   @override
-  final dynamic repostId;
+  final int? repostId;
   @override
-  final dynamic replyTo;
+  final SnPost? replyTo;
   @override
-  final dynamic repostTo;
+  final SnPost? repostTo;
+  final List<int>? _visibleUsersList;
   @override
-  final dynamic visibleUsersList;
+  List<int>? get visibleUsersList {
+    final value = _visibleUsersList;
+    if (value == null) return null;
+    if (_visibleUsersList is EqualUnmodifiableListView)
+      return _visibleUsersList;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(value);
+  }
+
+  final List<int>? _invisibleUsersList;
   @override
-  final dynamic invisibleUsersList;
+  List<int>? get invisibleUsersList {
+    final value = _invisibleUsersList;
+    if (value == null) return null;
+    if (_invisibleUsersList is EqualUnmodifiableListView)
+      return _invisibleUsersList;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(value);
+  }
+
   @override
   final int visibility;
   @override
@@ -687,15 +750,17 @@ class _$SnPostImpl extends _SnPost {
             const DeepCollectionEquality().equals(other._tags, _tags) &&
             const DeepCollectionEquality()
                 .equals(other._categories, _categories) &&
-            const DeepCollectionEquality().equals(other.replies, replies) &&
-            const DeepCollectionEquality().equals(other.replyId, replyId) &&
-            const DeepCollectionEquality().equals(other.repostId, repostId) &&
-            const DeepCollectionEquality().equals(other.replyTo, replyTo) &&
-            const DeepCollectionEquality().equals(other.repostTo, repostTo) &&
+            const DeepCollectionEquality().equals(other._replies, _replies) &&
+            (identical(other.replyId, replyId) || other.replyId == replyId) &&
+            (identical(other.repostId, repostId) ||
+                other.repostId == repostId) &&
+            (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
+            (identical(other.repostTo, repostTo) ||
+                other.repostTo == repostTo) &&
             const DeepCollectionEquality()
-                .equals(other.visibleUsersList, visibleUsersList) &&
+                .equals(other._visibleUsersList, _visibleUsersList) &&
             const DeepCollectionEquality()
-                .equals(other.invisibleUsersList, invisibleUsersList) &&
+                .equals(other._invisibleUsersList, _invisibleUsersList) &&
             (identical(other.visibility, visibility) ||
                 other.visibility == visibility) &&
             (identical(other.editedAt, editedAt) ||
@@ -736,13 +801,13 @@ class _$SnPostImpl extends _SnPost {
         aliasPrefix,
         const DeepCollectionEquality().hash(_tags),
         const DeepCollectionEquality().hash(_categories),
-        const DeepCollectionEquality().hash(replies),
-        const DeepCollectionEquality().hash(replyId),
-        const DeepCollectionEquality().hash(repostId),
-        const DeepCollectionEquality().hash(replyTo),
-        const DeepCollectionEquality().hash(repostTo),
-        const DeepCollectionEquality().hash(visibleUsersList),
-        const DeepCollectionEquality().hash(invisibleUsersList),
+        const DeepCollectionEquality().hash(_replies),
+        replyId,
+        repostId,
+        replyTo,
+        repostTo,
+        const DeepCollectionEquality().hash(_visibleUsersList),
+        const DeepCollectionEquality().hash(_invisibleUsersList),
         visibility,
         editedAt,
         pinnedAt,
@@ -787,13 +852,13 @@ abstract class _SnPost extends SnPost {
       required final String? aliasPrefix,
       required final List<dynamic> tags,
       required final List<dynamic> categories,
-      required final dynamic replies,
-      required final dynamic replyId,
-      required final dynamic repostId,
-      required final dynamic replyTo,
-      required final dynamic repostTo,
-      required final dynamic visibleUsersList,
-      required final dynamic invisibleUsersList,
+      required final List<SnPost>? replies,
+      required final int? replyId,
+      required final int? repostId,
+      required final SnPost? replyTo,
+      required final SnPost? repostTo,
+      required final List<int>? visibleUsersList,
+      required final List<int>? invisibleUsersList,
       required final int visibility,
       required final DateTime? editedAt,
       required final DateTime? pinnedAt,
@@ -834,19 +899,19 @@ abstract class _SnPost extends SnPost {
   @override
   List<dynamic> get categories;
   @override
-  dynamic get replies;
+  List<SnPost>? get replies;
   @override
-  dynamic get replyId;
+  int? get replyId;
   @override
-  dynamic get repostId;
+  int? get repostId;
   @override
-  dynamic get replyTo;
+  SnPost? get replyTo;
   @override
-  dynamic get repostTo;
+  SnPost? get repostTo;
   @override
-  dynamic get visibleUsersList;
+  List<int>? get visibleUsersList;
   @override
-  dynamic get invisibleUsersList;
+  List<int>? get invisibleUsersList;
   @override
   int get visibility;
   @override
diff --git a/lib/types/post.g.dart b/lib/types/post.g.dart
index 2dd8b31..c3e07ab 100644
--- a/lib/types/post.g.dart
+++ b/lib/types/post.g.dart
@@ -20,13 +20,23 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
       aliasPrefix: json['alias_prefix'] as String?,
       tags: json['tags'] as List<dynamic>,
       categories: json['categories'] as List<dynamic>,
-      replies: json['replies'],
-      replyId: json['reply_id'],
-      repostId: json['repost_id'],
-      replyTo: json['reply_to'],
-      repostTo: json['repost_to'],
-      visibleUsersList: json['visible_users_list'],
-      invisibleUsersList: json['invisible_users_list'],
+      replies: (json['replies'] as List<dynamic>?)
+          ?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
+          .toList(),
+      replyId: (json['reply_id'] as num?)?.toInt(),
+      repostId: (json['repost_id'] as num?)?.toInt(),
+      replyTo: json['reply_to'] == null
+          ? null
+          : SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
+      repostTo: json['repost_to'] == null
+          ? null
+          : SnPost.fromJson(json['repost_to'] as Map<String, dynamic>),
+      visibleUsersList: (json['visible_users_list'] as List<dynamic>?)
+          ?.map((e) => (e as num).toInt())
+          .toList(),
+      invisibleUsersList: (json['invisible_users_list'] as List<dynamic>?)
+          ?.map((e) => (e as num).toInt())
+          .toList(),
       visibility: (json['visibility'] as num).toInt(),
       editedAt: json['edited_at'] == null
           ? null
@@ -68,11 +78,11 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
       'alias_prefix': instance.aliasPrefix,
       'tags': instance.tags,
       'categories': instance.categories,
-      'replies': instance.replies,
+      'replies': instance.replies?.map((e) => e.toJson()).toList(),
       'reply_id': instance.replyId,
       'repost_id': instance.repostId,
-      'reply_to': instance.replyTo,
-      'repost_to': instance.repostTo,
+      'reply_to': instance.replyTo?.toJson(),
+      'repost_to': instance.repostTo?.toJson(),
       'visible_users_list': instance.visibleUsersList,
       'invisible_users_list': instance.invisibleUsersList,
       'visibility': instance.visibility,
diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart
index b5eabf7..3455dfe 100644
--- a/lib/widgets/post/post_item.dart
+++ b/lib/widgets/post/post_item.dart
@@ -38,6 +38,11 @@ class PostItem extends StatelessWidget {
       children: [
         _PostContentHeader(data: data).padding(horizontal: 12, vertical: 8),
         _PostContentBody(data: data.body).padding(horizontal: 16, bottom: 6),
+        if (data.repostTo != null)
+          _PostQuoteContent(child: data.repostTo!).padding(
+            horizontal: 8,
+            bottom: 4,
+          ),
         if (data.preload?.attachments?.isNotEmpty ?? true)
           AttachmentList(
             data: data.preload!.attachments!,
@@ -148,7 +153,13 @@ class _PostBottomAction extends StatelessWidget {
 
 class _PostContentHeader extends StatelessWidget {
   final SnPost data;
-  const _PostContentHeader({required this.data});
+  final bool isCompact;
+  final bool showActions;
+  const _PostContentHeader({
+    required this.data,
+    this.isCompact = false,
+    this.showActions = true,
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -157,13 +168,16 @@ class _PostContentHeader extends StatelessWidget {
 
     return Row(
       children: [
-        AccountImage(content: data.publisher.avatar),
-        const Gap(12),
-        Expanded(
-          child: Column(
-            crossAxisAlignment: CrossAxisAlignment.start,
+        AccountImage(
+          content: data.publisher.avatar,
+          radius: isCompact ? 12 : 20,
+        ),
+        Gap(isCompact ? 8 : 12),
+        if (isCompact)
+          Row(
             children: [
               Text(data.publisher.nick).bold(),
+              const Gap(4),
               Row(
                 children: [
                   Text('@${data.publisher.name}').fontSize(13),
@@ -174,86 +188,104 @@ class _PostContentHeader extends StatelessWidget {
                 ],
               ).opacity(0.8),
             ],
+          )
+        else
+          Expanded(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(data.publisher.nick).bold(),
+                Row(
+                  children: [
+                    Text('@${data.publisher.name}').fontSize(13),
+                    const Gap(4),
+                    Text(RelativeTime(context).format(
+                      data.publishedAt ?? data.createdAt,
+                    )).fontSize(13),
+                  ],
+                ).opacity(0.8),
+              ],
+            ),
           ),
-        ),
-        PopupMenuButton(
-          icon: const Icon(Symbols.more_horiz),
-          style: const ButtonStyle(
-            visualDensity: VisualDensity(horizontal: -4, vertical: -4),
-          ),
-          itemBuilder: (BuildContext context) => <PopupMenuEntry>[
-            if (isAuthor)
+        if (showActions)
+          PopupMenuButton(
+            icon: const Icon(Symbols.more_horiz),
+            style: const ButtonStyle(
+              visualDensity: VisualDensity(horizontal: -4, vertical: -4),
+            ),
+            itemBuilder: (BuildContext context) => <PopupMenuEntry>[
+              if (isAuthor)
+                PopupMenuItem(
+                  child: Row(
+                    children: [
+                      const Icon(Symbols.edit),
+                      const Gap(16),
+                      Text('edit').tr(),
+                    ],
+                  ),
+                  onTap: () {
+                    GoRouter.of(context).pushNamed(
+                      'postEditor',
+                      pathParameters: {'mode': data.typePlural},
+                      queryParameters: {'editing': data.id.toString()},
+                    );
+                  },
+                ),
+              if (isAuthor)
+                PopupMenuItem(
+                  child: Row(
+                    children: [
+                      const Icon(Symbols.delete),
+                      const Gap(16),
+                      Text('delete').tr(),
+                    ],
+                  ),
+                ),
+              if (isAuthor) const PopupMenuDivider(),
               PopupMenuItem(
                 child: Row(
                   children: [
-                    const Icon(Symbols.edit),
+                    const Icon(Symbols.reply),
                     const Gap(16),
-                    Text('edit').tr(),
+                    Text('reply').tr(),
                   ],
                 ),
                 onTap: () {
                   GoRouter.of(context).pushNamed(
                     'postEditor',
                     pathParameters: {'mode': data.typePlural},
-                    queryParameters: {'editing': data.id.toString()},
+                    queryParameters: {'replying': data.id.toString()},
                   );
                 },
               ),
-            if (isAuthor)
               PopupMenuItem(
                 child: Row(
                   children: [
-                    const Icon(Symbols.delete),
+                    const Icon(Symbols.forward),
                     const Gap(16),
-                    Text('delete').tr(),
+                    Text('repost').tr(),
+                  ],
+                ),
+                onTap: () {
+                  GoRouter.of(context).pushNamed(
+                    'postEditor',
+                    pathParameters: {'mode': data.typePlural},
+                    queryParameters: {'reposting': data.id.toString()},
+                  );
+                },
+              ),
+              const PopupMenuDivider(),
+              PopupMenuItem(
+                child: Row(
+                  children: [
+                    const Icon(Symbols.flag),
+                    const Gap(16),
+                    Text('report').tr(),
                   ],
                 ),
               ),
-            if (isAuthor) const PopupMenuDivider(),
-            PopupMenuItem(
-              child: Row(
-                children: [
-                  const Icon(Symbols.reply),
-                  const Gap(16),
-                  Text('reply').tr(),
-                ],
-              ),
-              onTap: () {
-                GoRouter.of(context).pushNamed(
-                  'postEditor',
-                  pathParameters: {'mode': data.typePlural},
-                  queryParameters: {'replying': data.id.toString()},
-                );
-              },
-            ),
-            PopupMenuItem(
-              child: Row(
-                children: [
-                  const Icon(Symbols.forward),
-                  const Gap(16),
-                  Text('repost').tr(),
-                ],
-              ),
-              onTap: () {
-                GoRouter.of(context).pushNamed(
-                  'postEditor',
-                  pathParameters: {'mode': data.typePlural},
-                  queryParameters: {'reposting': data.id.toString()},
-                );
-              },
-            ),
-            const PopupMenuDivider(),
-            PopupMenuItem(
-              child: Row(
-                children: [
-                  const Icon(Symbols.flag),
-                  const Gap(16),
-                  Text('report').tr(),
-                ],
-              ),
-            ),
-          ],
-        ),
+            ],
+          ),
       ],
     );
   }
@@ -269,3 +301,29 @@ class _PostContentBody extends StatelessWidget {
     return MarkdownTextContent(content: data['content']);
   }
 }
+
+class _PostQuoteContent extends StatelessWidget {
+  final SnPost child;
+  const _PostQuoteContent({super.key, required this.child});
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        borderRadius: const BorderRadius.all(Radius.circular(8)),
+        border: Border.all(
+          color: Theme.of(context).dividerColor,
+          width: 1,
+        ),
+      ),
+      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+      child: Column(
+        children: [
+          _PostContentHeader(data: child, isCompact: true, showActions: false)
+              .padding(bottom: 4),
+          _PostContentBody(data: child.body),
+        ],
+      ),
+    );
+  }
+}