From 7c92dee0972212009f6cc744d36fc2032be18429 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 30 Jul 2025 22:16:36 +0800 Subject: [PATCH] :sparkles: Edited the post item styles --- lib/screens/explore.dart | 35 +- lib/screens/posts/compose.dart | 6 +- lib/screens/posts/post_detail.dart | 1 - lib/widgets/account/status.dart | 1 + lib/widgets/account/status_creation.dart | 2 +- lib/widgets/chat/message_item.dart | 3 +- .../content/cloud_file_collection.dart | 21 +- lib/widgets/content/cloud_files.dart | 5 +- lib/widgets/post/post_item.dart | 754 ++++++------------ lib/widgets/post/post_item_creator.dart | 22 +- lib/widgets/post/post_replies.dart | 2 +- 11 files changed, 271 insertions(+), 581 deletions(-) diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 8a2654f..47e71f9 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -399,19 +399,9 @@ class _ActivityListView extends HookConsumerWidget { switch (item.type) { case 'posts.new': case 'posts.new.replies': - final isReply = item.type == 'posts.new.replies'; - itemWidget = PostItem( - backgroundColor: - isWideScreen(context) ? Colors.transparent : null, + itemWidget = PostActionableItem( + borderRadius: 8, item: SnPost.fromJson(item.data!), - padding: - isReply - ? const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, - ) - : null, onRefresh: () { activitiesNotifier.forceRefresh(); }, @@ -422,21 +412,10 @@ class _ActivityListView extends HookConsumerWidget { ); }, ); - if (isReply) { - itemWidget = Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - const Icon(Symbols.reply), - const Gap(8), - Text('Replying your post'), - ], - ).padding(horizontal: 20, vertical: 8), - itemWidget, - ], - ); - } + itemWidget = Card( + margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: itemWidget, + ); break; case 'discovery': itemWidget = _DiscoveryActivityItem(data: item.data!); @@ -445,7 +424,7 @@ class _ActivityListView extends HookConsumerWidget { itemWidget = const Placeholder(); } - return Column(children: [itemWidget, const Divider(height: 1)]); + return itemWidget; }, ), SliverGap(getTabbedPadding(context).bottom), diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index c9cced5..1728df3 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -470,7 +470,9 @@ class PostComposeScreen extends HookConsumerWidget { color: colorScheme.primary, ), IconButton( - onPressed: () => ComposeLogic.addAttachmentById(ref, state, context), + onPressed: + () => + ComposeLogic.addAttachmentById(ref, state, context), icon: const Icon(Symbols.attach_file), color: colorScheme.primary, ), @@ -655,7 +657,7 @@ class PostComposeScreen extends HookConsumerWidget { child: SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.all(16), - child: PostItem(item: post, isOpenable: false), + child: PostItem(item: post), ), ), ], diff --git a/lib/screens/posts/post_detail.dart b/lib/screens/posts/post_detail.dart index 0c200ff..da9dd50 100644 --- a/lib/screens/posts/post_detail.dart +++ b/lib/screens/posts/post_detail.dart @@ -71,7 +71,6 @@ class PostDetailScreen extends HookConsumerWidget { children: [ PostItem( item: post!, - isOpenable: false, isFullPost: true, backgroundColor: isWide ? Colors.transparent : null, onUpdate: (newItem) { diff --git a/lib/widgets/account/status.dart b/lib/widgets/account/status.dart index b09e135..e070aa3 100644 --- a/lib/widgets/account/status.dart +++ b/lib/widgets/account/status.dart @@ -86,6 +86,7 @@ class AccountStatusCreationWidget extends HookConsumerWidget { onTap: () { showModalBottomSheet( context: context, + useRootNavigator: true, builder: (context) => AccountStatusCreationSheet( initialStatus: diff --git a/lib/widgets/account/status_creation.dart b/lib/widgets/account/status_creation.dart index 3d1b2bd..3f5120a 100644 --- a/lib/widgets/account/status_creation.dart +++ b/lib/widgets/account/status_creation.dart @@ -49,7 +49,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget { final user = ref.watch(userInfoProvider); final apiClient = ref.read(apiClientProvider); await apiClient.request( - '/accounts/me/statuses', + '/id/accounts/me/statuses', data: { 'attitude': attitude.value, 'is_invisible': isInvisible.value, diff --git a/lib/widgets/chat/message_item.dart b/lib/widgets/chat/message_item.dart index 8db2271..b2231e5 100644 --- a/lib/widgets/chat/message_item.dart +++ b/lib/widgets/chat/message_item.dart @@ -228,7 +228,8 @@ class MessageItem extends HookConsumerWidget { return CloudFileList( files: remoteMessage.attachments, maxWidth: constraints.maxWidth, - ).padding(vertical: 4); + padding: EdgeInsets.symmetric(vertical: 4), + ); }, ), if (remoteMessage.meta['embeds'] != null) diff --git a/lib/widgets/content/cloud_file_collection.dart b/lib/widgets/content/cloud_file_collection.dart index 47ce324..e4a8f72 100644 --- a/lib/widgets/content/cloud_file_collection.dart +++ b/lib/widgets/content/cloud_file_collection.dart @@ -27,6 +27,7 @@ class CloudFileList extends HookConsumerWidget { final double? minWidth; final bool disableZoomIn; final bool disableConstraint; + final EdgeInsets? padding; const CloudFileList({ super.key, required this.files, @@ -35,6 +36,7 @@ class CloudFileList extends HookConsumerWidget { this.minWidth, this.disableZoomIn = false, this.disableConstraint = false, + this.padding, }); double calculateAspectRatio() { @@ -60,7 +62,8 @@ class CloudFileList extends HookConsumerWidget { if (files.isEmpty) return const SizedBox.shrink(); if (files.length == 1) { final isImage = files.first.mimeType?.startsWith('image') ?? false; - return ConstrainedBox( + return Container( + padding: padding, constraints: BoxConstraints( maxHeight: disableConstraint ? double.infinity : maxHeight, minWidth: minWidth ?? 0, @@ -75,7 +78,7 @@ class CloudFileList extends HookConsumerWidget { child: AspectRatio( aspectRatio: calculateAspectRatio(), child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), + borderRadius: const BorderRadius.all(Radius.circular(8)), child: _CloudFileListEntry( file: files.first, heroTag: heroTags.first, @@ -95,7 +98,7 @@ class CloudFileList extends HookConsumerWidget { ), ), ), - ).padding(horizontal: 3); + ); } return ConstrainedBox( @@ -105,7 +108,7 @@ class CloudFileList extends HookConsumerWidget { child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: files.length, - padding: EdgeInsets.symmetric(horizontal: 3), + padding: padding, itemBuilder: (context, index) { return AspectRatio( aspectRatio: @@ -133,6 +136,7 @@ class CloudFileList extends HookConsumerWidget { item: files[index], heroTag: heroTags[index], ), + rootNavigator: true, ); } }, @@ -184,7 +188,7 @@ class CloudFileZoomIn extends HookConsumerWidget { final filePath = '${tempDir.path}/${item.id}.${extension(item.name)}'; await client.download( - '/files/${item.id}', + '/drive/files/${item.id}', filePath, queryParameters: {'original': true}, ); @@ -334,7 +338,6 @@ class CloudFileZoomIn extends HookConsumerWidget { imageProvider: CloudImageWidget.provider( fileId: item.id, serverUrl: serverUrl, - original: true, ), // Apply rotation transformation customSize: MediaQuery.of(context).size, @@ -475,7 +478,6 @@ class _CloudFileListEntry extends StatelessWidget { final bool isImage; final bool disableZoomIn; final VoidCallback? onTap; - final BoxFit fit; const _CloudFileListEntry({ required this.file, @@ -483,7 +485,6 @@ class _CloudFileListEntry extends StatelessWidget { required this.isImage, required this.disableZoomIn, this.onTap, - this.fit = BoxFit.contain, }); @override @@ -506,10 +507,10 @@ class _CloudFileListEntry extends StatelessWidget { item: file, heroTag: heroTag, noBlurhash: true, - fit: fit, + fit: BoxFit.contain, ) else - CloudFileWidget(item: file, heroTag: heroTag, fit: fit), + CloudFileWidget(item: file, heroTag: heroTag, fit: BoxFit.contain), ], ); diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index 73055e3..4557db7 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -92,7 +92,10 @@ class CloudImageWidget extends ConsumerWidget { required String serverUrl, bool original = false, }) { - final uri = '$serverUrl/drive/files/$fileId?original=$original'; + final uri = + original + ? '$serverUrl/drive/files/$fileId?original=true' + : '$serverUrl/drive/files/$fileId'; return CachedNetworkImageProvider(uri); } } diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 75423a3..52d0e2f 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -5,36 +6,79 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'dart:math' as math; -import 'package:island/models/embed.dart'; import 'package:island/models/post.dart'; -import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; -import 'package:island/screens/posts/compose.dart'; -import 'package:island/services/responsive.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/alert.dart'; -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/embed/link.dart'; import 'package:island/widgets/content/markdown.dart'; -import 'package:island/widgets/safety/abuse_report_helper.dart'; -import 'package:island/widgets/post/post_replies_sheet.dart'; -import 'package:island/widgets/share/share_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'; -class PostItem extends HookConsumerWidget { +class PostActionableItem extends HookConsumerWidget { final Color? backgroundColor; final SnPost item; final EdgeInsets? padding; - final bool isOpenable; final bool isFullPost; - final bool showReferencePost; + final bool isShowReference; + final double? borderRadius; + final Function? onRefresh; + final Function(SnPost)? onUpdate; + const PostActionableItem({ + super.key, + required this.item, + this.backgroundColor, + this.padding, + this.isFullPost = false, + this.isShowReference = true, + this.borderRadius, + this.onRefresh, + this.onUpdate, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userInfoProvider); + final isAuthor = useMemoized( + () => user.value != null && user.value?.id == item.publisher.accountId, + [user], + ); + + final widgetItem = InkWell( + borderRadius: + borderRadius != null + ? BorderRadius.all(Radius.circular(borderRadius!)) + : null, + child: PostItem( + key: key, + item: item, + backgroundColor: backgroundColor, + padding: padding, + isFullPost: isFullPost, + isShowReference: isShowReference, + isTextSelectable: false, + onRefresh: onRefresh, + onUpdate: onUpdate, + ), + onTap: () { + context.pushNamed('postDetail', pathParameters: {'id': item.id}); + }, + ); + + return widgetItem; + } +} + +class PostItem extends HookConsumerWidget { + final SnPost item; + final Color? backgroundColor; + final EdgeInsets? padding; + final bool isFullPost; + final bool isShowReference; + final bool isTextSelectable; final Function? onRefresh; final Function(SnPost)? onUpdate; const PostItem({ @@ -42,9 +86,9 @@ class PostItem extends HookConsumerWidget { required this.item, this.backgroundColor, this.padding, - this.isOpenable = true, this.isFullPost = false, - this.showReferencePost = true, + this.isShowReference = true, + this.isTextSelectable = true, this.onRefresh, this.onUpdate, }); @@ -52,559 +96,213 @@ class PostItem extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final renderingPadding = - padding ?? EdgeInsets.symmetric(horizontal: 12, vertical: 16); + padding ?? EdgeInsets.symmetric(horizontal: 8, vertical: 8); - final user = ref.watch(userInfoProvider); - final isAuthor = useMemoized( - () => user.value != null && user.value?.id == item.publisher.accountId, - [user], - ); + final reacting = useState(false); - final hasBackground = - ref.watch(backgroundImageFileProvider).valueOrNull != null; + Future reactPost(String symbol, int attitude) async { + final client = ref.watch(apiClientProvider); + reacting.value = true; + await client + .post( + '/sphere/posts/${item.id}/reactions', + data: {'symbol': symbol, 'attitude': attitude}, + ) + .catchError((err) { + showErrorAlert(err); + return err; + }) + .then((resp) { + final isRemoving = resp.statusCode == 204; + final delta = isRemoving ? -1 : 1; + final reactionsCount = Map.from(item.reactionsCount); + reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta; + onUpdate?.call(item.copyWith(reactionsCount: reactionsCount)); + HapticFeedback.heavyImpact(); + }); + reacting.value = false; + } - Widget child; - if (item.type == 1 && isFullPost) { - child = Padding( - padding: renderingPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + final mostReaction = + item.reactionsCount.isEmpty + ? null + : item.reactionsCount.entries + .sortedBy((e) => e.value) + .map((e) => e.key) + .first; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Gap(renderingPadding.horizontal), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12, children: [ GestureDetector( + child: ProfilePictureWidget( + file: item.publisher.picture, + radius: 16, + ), onTap: () { context.pushNamed( 'publisherProfile', pathParameters: {'name': item.publisher.name}, ); }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfilePictureWidget(file: item.publisher.picture), - const Gap(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(item.publisher.nick).bold(), - if (item.publisher.verification != null) - VerificationMark( - mark: item.publisher.verification!, - ).padding(left: 4), - ], - ), + Row( + spacing: 4, + children: [ + Text(item.publisher.nick).bold(), + if (item.publisher.verification != null) + VerificationMark(mark: item.publisher.verification!), + Text('@${item.publisher.name}').fontSize(11), + ], ), Text( isFullPost - ? item.publishedAt?.formatSystem() ?? '' - : item.publishedAt?.formatRelative(context) ?? '', - ).fontSize(11), + ? (item.publishedAt ?? item.createdAt)!.formatSystem() + : (item.publishedAt ?? item.createdAt)!.formatRelative( + context, + ), + ).fontSize(10), ], ), ), - if (item.visibility != 0) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getVisibilityIcon(item.visibility), - size: 14, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 4), - Text( - _getVisibilityText(item.visibility).tr(), - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ], - ).padding(top: 10, bottom: 2), - const Gap(16), - _ArticlePostDisplay(item: item, isFullPost: isFullPost), - if (item.tags.isNotEmpty || item.categories.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (item.tags.isNotEmpty) - Wrap( - children: [ - for (final tag in item.tags) - InkWell( - child: Row( - spacing: 4, - children: [ - const Icon(Symbols.label, size: 13), - Text(tag.name ?? '#${tag.slug}').fontSize(13), - ], - ), - onTap: () {}, - ), - ], - ), - if (item.categories.isNotEmpty) - Wrap( - children: [ - for (final category in item.categories) - InkWell( - child: Row( - spacing: 4, - children: [ - const Icon(Symbols.category, size: 13), - Text( - category.name ?? '#${category.slug}', - ).fontSize(13), - ], - ), - onTap: () {}, - ), - ], - ), - ], - ), - if ((item.repliedPost != null || item.forwardedPost != null) && - showReferencePost) - _buildReferencePost(context, item), - if (item.attachments.isNotEmpty && item.type != 1) - CloudFileList( - disableConstraint: isFullPost, - files: item.attachments, - maxWidth: math.min( - MediaQuery.of(context).size.width, - kWideScreenWidth, - ), - ), - if (item.meta?['embeds'] != null) - ...((item.meta!['embeds'] as List) - .where((embed) => embed['Type'] == 'link') - .map( - (embedData) => EmbedLinkWidget( - link: SnEmbedLink.fromJson( - embedData as Map, + IconButton( + icon: + mostReaction == null + ? const Icon(Symbols.add_reaction) + : Badge( + label: Text( + 'x${item.reactionsCount[mostReaction]}', + style: TextStyle(fontSize: 11), + ), + offset: Offset(4, 20), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.75), + textColor: Theme.of(context).colorScheme.onPrimary, + child: Text( + kReactionTemplates[mostReaction]!.icon, + style: TextStyle(fontSize: 20), + ), ), - maxWidth: math.min( - MediaQuery.of(context).size.width, - kWideScreenWidth, - ), - margin: EdgeInsets.only(top: 8), - ), - )), - const Gap(8), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(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, - useRootNavigator: true, - builder: (context) => PostRepliesSheet(post: item), - ); - } - }, - ), - ), - 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), - ); - }, - ), - ), - ], - ), - ], - ), - ); - } else { - child = Padding( - padding: renderingPadding, - child: Column( - spacing: 8, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - GestureDetector( - child: ProfilePictureWidget(file: item.publisher.picture), - onTap: () { - context.pushNamed( - 'publisherProfile', - pathParameters: {'name': item.publisher.name}, + onPressed: () { + showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (BuildContext context) { + return _PostReactionSheet( + reactionsCount: item.reactionsCount, + onReact: (symbol, attitude) { + reactPost(symbol, attitude); + }, ); }, - ), - Expanded( - child: GestureDetector( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text(item.publisher.nick).bold(), - if (item.publisher.verification != null) - VerificationMark( - mark: item.publisher.verification!, - ).padding(left: 4), - Spacer(), - Text( - isFullPost - ? item.publishedAt?.formatSystem() ?? '' - : item.publishedAt?.formatRelative(context) ?? - '', - ).fontSize(11).alignment(Alignment.bottomRight), - const Gap(4), - ], - ), - // Add visibility indicator if not public (visibility != 0) - if (item.visibility != 0) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getVisibilityIcon(item.visibility), - size: 14, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 4), - Text( - _getVisibilityText(item.visibility).tr(), - style: TextStyle( - fontSize: 12, - color: - Theme.of(context).colorScheme.secondary, - ), - ), - ], - ).padding(top: 2, bottom: 2), - if (item.type == 1) - _ArticlePostDisplay( - item: item, - isFullPost: isFullPost, - ) - else ...[ - if (item.title?.isNotEmpty ?? false) - Text( - item.title!, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold), - ), - if (item.description?.isNotEmpty ?? false) - Text( - item.description!, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith( - color: - Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ).padding(bottom: 8), - if (item.content?.isNotEmpty ?? false) - MarkdownTextContent( - content: item.content!, - linesMargin: - item.type == 0 - ? EdgeInsets.only(bottom: 8) - : null, - attachments: item.attachments, - ), - ], - // Render tags and categories if they exist - if (item.tags.isNotEmpty || item.categories.isNotEmpty) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (item.tags.isNotEmpty) - Wrap( - children: [ - for (final tag in item.tags) - InkWell( - child: Row( - spacing: 4, - children: [ - const Icon(Symbols.label, size: 13), - Text( - tag.name ?? '#${tag.slug}', - ).fontSize(13), - ], - ), - onTap: () {}, - ), - ], - ), - if (item.categories.isNotEmpty) - Wrap( - children: [ - for (final category in item.categories) - InkWell( - child: Row( - spacing: 4, - children: [ - const Icon( - Symbols.category, - size: 13, - ), - Text( - category.name ?? - '#${category.slug}', - ).fontSize(13), - ], - ), - onTap: () {}, - ), - ], - ), - ], - ), - // Show truncation hint if post is truncated - if (item.isTruncated && !isFullPost && item.type != 1) - _PostTruncateHint().padding( - bottom: - (item.attachments.isNotEmpty || - item.repliedPost != null || - item.forwardedPost != null) - ? 8 - : null, - ), - if ((item.repliedPost != null || - item.forwardedPost != null) && - showReferencePost) - _buildReferencePost(context, item), - if (item.attachments.isNotEmpty && item.type != 1) - CloudFileList( - files: item.attachments, - disableConstraint: isFullPost, - maxWidth: math.min( - MediaQuery.of(context).size.width * 0.85, - kWideScreenWidth - 160, - ), - ), - // Render embed links - if (item.meta?['embeds'] != null) - ...((item.meta!['embeds'] as List) - .where((embed) => embed['Type'] == 'link') - .map( - (embedData) => EmbedLinkWidget( - link: SnEmbedLink.fromJson( - embedData as Map, - ), - maxWidth: math.min( - MediaQuery.of(context).size.width * 0.85, - kWideScreenWidth - 160, - ), - margin: EdgeInsets.only(top: 8), - ), - )), - ], - ), - onTap: () { - if (isOpenable) { - context.pushNamed( - 'postDetail', - pathParameters: {'id': item.id}, - ); - } - }, - ), - ), - ], + ); + }, + padding: EdgeInsets.zero, + visualDensity: VisualDensity(horizontal: -3, vertical: -3), ), - Row( + ], + ).padding(horizontal: renderingPadding.horizontal, bottom: 4), + if (!isFullPost && item.type == 1) + Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: const BorderRadius.all(Radius.circular(16)), + ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + margin: EdgeInsets.only( + left: renderingPadding.horizontal, + right: renderingPadding.horizontal, + top: 4, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Replies count button - Padding( - padding: const EdgeInsets.only(left: 52, 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, - useRootNavigator: true, - builder: (context) => PostRepliesSheet(post: item), - ); - } - }, + Align( + alignment: Alignment.centerLeft, + child: Badge( + label: Text('postArticle').tr(), + backgroundColor: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary, ), ), - // 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), - ); - }, + const Gap(4), + if (item.title != null) + Text( + item.title!, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), + if (item.description != null) + Text( + item.description!, + style: Theme.of(context).textTheme.bodyMedium, + ) + else + MarkdownTextContent(content: '${item.content!}...'), ], ), - ], - ), - ); - } - - return ContextMenuWidget( - menuProvider: (_) { - return Menu( - children: [ - if (isAuthor) - MenuAction( - title: 'edit'.tr(), - image: MenuImage.icon(Symbols.edit), - callback: () { - context - .pushNamed('postEdit', pathParameters: {'id': item.id}) - .then((value) { - if (value != null) { - onRefresh?.call(); - } - }); - }, - ), - if (isAuthor) - MenuAction( - title: 'delete'.tr(), - image: MenuImage.icon(Symbols.delete), - callback: () { - showConfirmAlert( - 'deletePostHint'.tr(), - 'deletePost'.tr(), - ).then((confirm) { - if (confirm) { - final client = ref.watch(apiClientProvider); - client - .delete('/sphere/posts/${item.id}') - .catchError((err) { - showErrorAlert(err); - return err; - }) - .then((_) { - onRefresh?.call(); - }); - } - }); - }, - ), - if (isAuthor) MenuSeparator(), - MenuAction( - title: 'copyLink'.tr(), - image: MenuImage.icon(Symbols.link), - callback: () { - Clipboard.setData( - ClipboardData(text: 'https://solsynth.dev/posts/${item.id}'), - ); - }, + ) + else if (item.content?.isNotEmpty ?? false) + Padding( + padding: EdgeInsets.only( + left: renderingPadding.horizontal, + right: renderingPadding.horizontal, ), - MenuAction( - title: 'reply'.tr(), - image: MenuImage.icon(Symbols.reply), - callback: () { - context.pushNamed( - 'postCompose', - extra: PostComposeInitialState(replyingTo: item), - ); - }, + child: MarkdownTextContent( + content: item.content!, + isSelectable: isTextSelectable, ), - MenuAction( - title: 'forward'.tr(), - image: MenuImage.icon(Symbols.forward), - callback: () { - context.pushNamed( - 'postCompose', - extra: PostComposeInitialState(forwardingTo: item), - ); - }, + ), + if (item.attachments.isNotEmpty) + CloudFileList( + files: item.attachments, + padding: EdgeInsets.symmetric( + horizontal: renderingPadding.horizontal, + vertical: 4, ), - MenuSeparator(), - MenuAction( - title: 'share'.tr(), - image: MenuImage.icon(Symbols.share), - callback: () { - showShareSheetLink( - context: context, - link: '${ref.read(serverUrlProvider)}/posts/${item.id}', - title: 'sharePost'.tr(), - toSystem: true, - ); - }, - ), - MenuAction( - title: 'abuseReport'.tr(), - image: MenuImage.icon(Symbols.flag), - callback: () { - showAbuseReportSheet( - context, - resourceIdentifier: 'post/${item.id}', - ); - }, - ), - ], - ); - }, - child: Material( - color: hasBackground ? Colors.transparent : backgroundColor, - child: child, - ), + ), + if (isShowReference) + _buildReferencePost(context, item, renderingPadding), + Gap(renderingPadding.vertical), + ], ); } } -Widget _buildReferencePost(BuildContext context, SnPost item) { +Widget _buildReferencePost( + BuildContext context, + SnPost item, + EdgeInsets renderingPadding, +) { final referencePost = item.repliedPost ?? item.forwardedPost; if (referencePost == null) return const SizedBox.shrink(); final isReply = item.repliedPost != null; return Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(12), + padding: EdgeInsets.symmetric( + horizontal: renderingPadding.horizontal, + vertical: 8, + ), + margin: EdgeInsets.only( + top: 8, + left: renderingPadding.vertical, + right: renderingPadding.vertical, + ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), borderRadius: BorderRadius.circular(12), diff --git a/lib/widgets/post/post_item_creator.dart b/lib/widgets/post/post_item_creator.dart index 20fc196..198c4f0 100644 --- a/lib/widgets/post/post_item_creator.dart +++ b/lib/widgets/post/post_item_creator.dart @@ -45,11 +45,13 @@ class PostItemCreator extends HookConsumerWidget { title: 'edit'.tr(), image: MenuImage.icon(Symbols.edit), callback: () { - context.pushNamed('postEdit', pathParameters: {'id': item.id}).then((value) { - if (value != null) { - onRefresh?.call(); - } - }); + context + .pushNamed('postEdit', pathParameters: {'id': item.id}) + .then((value) { + if (value != null) { + onRefresh?.call(); + } + }); }, ), MenuAction( @@ -80,7 +82,10 @@ class PostItemCreator extends HookConsumerWidget { image: MenuImage.icon(Symbols.link), callback: () { // Copy post link to clipboard - context.pushNamed('postDetail', pathParameters: {'id': item.id}); + context.pushNamed( + 'postDetail', + pathParameters: {'id': item.id}, + ); }, ), ], @@ -198,7 +203,8 @@ class PostItemCreator extends HookConsumerWidget { files: item.attachments, maxWidth: MediaQuery.of(context).size.width * 0.85, minWidth: MediaQuery.of(context).size.width * 0.9, - ).padding(top: 8), + padding: EdgeInsets.only(top: 8), + ), // Reference post indicator if (item.repliedPost != null || item.forwardedPost != null) @@ -211,7 +217,7 @@ class PostItemCreator extends HookConsumerWidget { size: 16, color: Theme.of(context).colorScheme.secondary, ), - const SizedBox(width: 4), + const Gap(4), Text( item.repliedPost != null ? 'repliedTo'.tr() diff --git a/lib/widgets/post/post_replies.dart b/lib/widgets/post/post_replies.dart index 1e01636..4b83972 100644 --- a/lib/widgets/post/post_replies.dart +++ b/lib/widgets/post/post_replies.dart @@ -99,7 +99,7 @@ class PostRepliesList extends HookConsumerWidget { item: data.items[index], backgroundColor: backgroundColor ?? (isWide ? Colors.transparent : null), - showReferencePost: false, + isShowReference: false, ), const Divider(height: 1), ],