diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 9e79500..04e668c 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -752,5 +752,6 @@ "screenAccountBadges": "Badges", "accountBadges": "Badges", "accountBadgesDescription": "View and manage your badges.", - "badgeActivated": "Activated badge {}." + "badgeActivated": "Activated badge {}.", + "viewDetailedAttachment": "Details" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 7d34dc4..a5881b8 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -750,5 +750,6 @@ "screenAccountBadges": "徽章", "accountBadges": "徽章", "accountBadgesDescription": "查看并管理你的徽章。", - "badgeActivated": "已佩戴徽章 {}。" + "badgeActivated": "已佩戴徽章 {}。", + "viewDetailedAttachment": "查看附件详情" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index b4b0c3d..a322e9d 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -750,5 +750,6 @@ "screenAccountBadges": "徽章", "accountBadges": "徽章", "accountBadgesDescription": "查看並管理你的徽章。", - "badgeActivated": "已佩戴徽章 {}。" + "badgeActivated": "已佩戴徽章 {}。", + "viewDetailedAttachment": "查看附件詳情" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index eaa1247..dbc2327 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -750,5 +750,6 @@ "screenAccountBadges": "徽章", "accountBadges": "徽章", "accountBadgesDescription": "查看並管理你的徽章。", - "badgeActivated": "已佩戴徽章 {}。" + "badgeActivated": "已佩戴徽章 {}。", + "viewDetailedAttachment": "查看附件詳情" } diff --git a/lib/widgets/account/account_popover.dart b/lib/widgets/account/account_popover.dart index af6e98c..5eec0de 100644 --- a/lib/widgets/account/account_popover.dart +++ b/lib/widgets/account/account_popover.dart @@ -72,34 +72,36 @@ class AccountPopoverCard extends StatelessWidget { const Gap(8) ], ).padding(horizontal: 16), - const Gap(16), - Wrap( - children: data.badges - .map( - (ele) => Tooltip( - richMessage: TextSpan( - children: [ - TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), - if (ele.metadata['title'] != null) - TextSpan( - text: '\n${ele.metadata['title']}', - style: const TextStyle(fontWeight: FontWeight.bold), + if (data.badges.isNotEmpty) const Gap(12), + if (data.badges.isNotEmpty) + Wrap( + spacing: 4, + children: data.badges + .map( + (ele) => Tooltip( + richMessage: TextSpan( + children: [ + TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), + if (ele.metadata['title'] != null) + TextSpan( + text: '\n${ele.metadata['title']}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: '\n'), + TextSpan( + text: DateFormat.yMEd().format(ele.createdAt), + ), + ], + ), + child: Icon( + kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, + color: kBadgesMeta[ele.type]?.$3, + fill: 1, ), - TextSpan(text: '\n'), - TextSpan( - text: DateFormat.yMEd().format(ele.createdAt), ), - ], - ), - child: Icon( - kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, - color: kBadgesMeta[ele.type]?.$3, - fill: 1, - ), - ), - ) - .toList(), - ).padding(horizontal: 24), + ) + .toList(), + ).padding(horizontal: 24), const Gap(8), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -127,33 +129,33 @@ class AccountPopoverCard extends StatelessWidget { final SnAccountStatusInfo? status = snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null; return Row( - children: [ - Icon( - Symbols.circle, - fill: 1, - size: 16, - color: (status?.isOnline ?? false) ? Colors.green : Colors.grey, - ).padding(all: 4), - const Gap(8), + children: [ + Icon( + Symbols.circle, + fill: 1, + size: 16, + color: (status?.isOnline ?? false) ? Colors.green : Colors.grey, + ).padding(all: 4), + const Gap(8), + Text( + status != null + ? status.isOnline + ? 'accountStatusOnline'.tr() + : 'accountStatusOffline'.tr() + : 'loading'.tr(), + ), + if (status != null && !status.isOnline && status.lastSeenAt != null) Text( - status != null - ? status.isOnline - ? 'accountStatusOnline'.tr() - : 'accountStatusOffline'.tr() - : 'loading'.tr(), - ), - if (status != null && !status.isOnline && status.lastSeenAt != null) - Text( - 'accountStatusLastSeen'.tr(args: [ - status.lastSeenAt != null - ? RelativeTime(context).format( - status.lastSeenAt!.toLocal(), - ) - : 'unknown', - ]), - ).padding(left: 6).opacity(0.75), - ], - ).padding(horizontal: 24); + 'accountStatusLastSeen'.tr(args: [ + status.lastSeenAt != null + ? RelativeTime(context).format( + status.lastSeenAt!.toLocal(), + ) + : 'unknown', + ]), + ).padding(left: 6).opacity(0.75), + ], + ).padding(horizontal: 24); }, ), // Bottom padding diff --git a/lib/widgets/attachment/attachment_zoom.dart b/lib/widgets/attachment/attachment_zoom.dart index 8be522d..3eee61d 100644 --- a/lib/widgets/attachment/attachment_zoom.dart +++ b/lib/widgets/attachment/attachment_zoom.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math' show max; import 'package:dio/dio.dart'; import 'package:dismissible_page/dismissible_page.dart'; @@ -43,6 +44,9 @@ class AttachmentZoomView extends StatefulWidget { class _AttachmentZoomViewState extends State { late final PageController _pageController = PageController(initialPage: widget.initialIndex ?? 0); + bool _showOverlay = true; + bool _dismissable = true; + void _updatePage() { setState(() { if (_isCompletedDownload) { @@ -146,7 +150,7 @@ class _AttachmentZoomViewState extends State { onDismissed: () { Navigator.of(context).pop(); }, - direction: DismissiblePageDismissDirection.none, + direction: _dismissable ? DismissiblePageDismissDirection.multi : DismissiblePageDismissDirection.none, backgroundColor: Colors.transparent, isFullScreen: true, child: GestureDetector( @@ -163,6 +167,9 @@ class _AttachmentZoomViewState extends State { child: PhotoView( key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), backgroundDecoration: BoxDecoration(color: Colors.transparent), + scaleStateChangedCallback: (scaleState) { + setState(() => _dismissable = scaleState == PhotoViewScaleState.initial); + }, imageProvider: UniversalImage.provider( sn.getAttachmentUrl(widget.data.first.rid), ), @@ -172,7 +179,10 @@ class _AttachmentZoomViewState extends State { return PhotoViewGallery.builder( pageController: _pageController, - scrollPhysics: const BouncingScrollPhysics(), + enableRotation: true, + scaleStateChangedCallback: (scaleState) { + setState(() => _dismissable = scaleState == PhotoViewScaleState.initial); + }, builder: (context, idx) { final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); return PhotoViewGalleryPageOptions( @@ -197,6 +207,27 @@ class _AttachmentZoomViewState extends State { backgroundDecoration: BoxDecoration(color: Colors.transparent), ); }), + Positioned( + top: max(MediaQuery.of(context).padding.top, 8), + left: 14, + child: IgnorePointer( + ignoring: !_showOverlay, + child: IconButton( + constraints: const BoxConstraints(), + icon: const Icon(Icons.close), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.surface.withOpacity(0.5), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + .opacity(_showOverlay ? 1 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.easeInOut), + ), + ), Align( alignment: Alignment.bottomCenter, child: IgnorePointer( @@ -214,7 +245,9 @@ class _AttachmentZoomViewState extends State { ), ), ), - ), + ) + .opacity(_showOverlay ? 1 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.easeInOut), Positioned( left: 16, right: 16, @@ -318,16 +351,6 @@ class _AttachmentZoomViewState extends State { ]), style: metaTextStyle, ).padding(right: 2), - if (item.metadata['exif']?['ISO'] != null) - Text( - 'ISO${item.metadata['exif']?['ISO']}', - style: metaTextStyle, - ).padding(right: 2), - if (item.metadata['exif']?['Aperture'] != null) - Text( - 'f/${item.metadata['exif']?['Aperture']}', - style: metaTextStyle, - ).padding(right: 2), if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null) Text( @@ -344,29 +367,44 @@ class _AttachmentZoomViewState extends State { '${item.metadata['width']}x${item.metadata['height']}', style: metaTextStyle, ), - if (item.metadata['ratio'] != null) - Text( - (item.metadata['ratio'] as num).toStringAsFixed(2), - style: metaTextStyle, - ), - Text( - item.mimetype, - style: metaTextStyle, - ), ], ), ), + const Gap(4), + InkWell( + onTap: () { + _showDetail = true; + showModalBottomSheet( + context: context, + builder: (context) => _AttachmentZoomDetailPopup( + data: widget.data + .elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), + ), + ).then((_) { + _showDetail = false; + }); + }, + child: Text( + 'viewDetailedAttachment'.tr(), + style: metaTextStyle.copyWith(decoration: TextDecoration.underline), + ), + ), ], ); }), - ), + ) + .opacity(_showOverlay ? 1 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.easeInOut), ), ], ), ), + onTap: () { + setState(() => _showOverlay = !_showOverlay); + }, onVerticalDragUpdate: (details) { if (_showDetail) return; - if (details.delta.dy <= -40) { + if (details.delta.dy <= -20) { _showDetail = true; showModalBottomSheet( context: context, @@ -378,9 +416,6 @@ class _AttachmentZoomViewState extends State { }); } }, - onTap: () { - Navigator.of(context).pop(); - }, ), ); } @@ -480,14 +515,14 @@ class _AttachmentZoomDetailPopup extends StatelessWidget { ), tableGap, ...(data.metadata['exif']?.keys.map((k) => TableRow( - children: [ - TableCell(child: Text(k).padding(right: 16)), - TableCell(child: Text(data.metadata['exif'][k].toString())), - ], - )) ?? + children: [ + TableCell(child: Text(k).padding(right: 16)), + TableCell(child: Text(data.metadata['exif'][k].toString())), + ], + )) ?? []), ], - ).padding(horizontal: 20, vertical: 8), + ).padding(horizontal: 20, vertical: 8, bottom: MediaQuery.of(context).padding.bottom), ), ), ], diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index 87402db..3652b5f 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -28,8 +28,7 @@ class ChatMessageInput extends StatefulWidget { final ChatMessageController controller; final SnChannelMember? otherMember; - const ChatMessageInput( - {super.key, required this.controller, this.otherMember}); + const ChatMessageInput({super.key, required this.controller, this.otherMember}); @override State createState() => ChatMessageInputState(); @@ -46,20 +45,12 @@ class ChatMessageInputState extends State { final HotKey _pasteHotKey = HotKey( key: PhysicalKeyboardKey.keyV, - modifiers: [ - (!kIsWeb && Platform.isMacOS) - ? HotKeyModifier.meta - : HotKeyModifier.control - ], + modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control], scope: HotKeyScope.inapp, ); final HotKey _newLineHotKey = HotKey( key: PhysicalKeyboardKey.enter, - modifiers: [ - (!kIsWeb && Platform.isMacOS) - ? HotKeyModifier.meta - : HotKeyModifier.control - ], + modifiers: [(!kIsWeb && Platform.isMacOS) ? HotKeyModifier.meta : HotKeyModifier.control], scope: HotKeyScope.inapp, ); @@ -109,8 +100,7 @@ class ChatMessageInputState extends State { void setEdit(SnChatMessage? value) { _contentController.text = value?.body['text'] ?? ''; _attachments.clear(); - _attachments.addAll( - value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []); + _attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []); setState(() => _editingMessage = value); } @@ -149,9 +139,7 @@ class ChatMessageInputState extends State { media.name, 'messaging', null, - mimetype: media.raw != null && media.type == SnMediaType.image - ? 'image/png' - : null, + mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null, ); final item = await attach.chunkedUploadParts( @@ -183,10 +171,7 @@ class ChatMessageInputState extends State { widget.controller.sendMessage( _editingMessage != null ? 'messages.edit' : 'messages.new', _contentController.text, - attachments: _attachments - .where((e) => e.attachment != null) - .map((e) => e.attachment!.rid) - .toList(), + attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), relatedId: _editingMessage?.id, quoteId: _replyingMessage?.id, editingMessage: _editingMessage, @@ -247,15 +232,12 @@ class ChatMessageInputState extends State { TweenAnimationBuilder( tween: Tween(begin: 0, end: _progress), duration: Duration(milliseconds: 300), - builder: (context, value, _) => - LinearProgressIndicator(value: value, minHeight: 2), + builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), ) else if (_isBusy) const LinearProgressIndicator(value: null, minHeight: 2), Padding( - padding: _attachments.isNotEmpty - ? const EdgeInsets.only(top: 8) - : EdgeInsets.zero, + padding: _attachments.isNotEmpty ? const EdgeInsets.only(top: 8) : EdgeInsets.zero, child: PostMediaPendingList( attachments: _attachments, isBusy: _isBusy, @@ -267,8 +249,9 @@ class ChatMessageInputState extends State { }, onUpdateBusy: (state) => setState(() => _isBusy = state), ), - ).height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true).animate( - const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), + ) + .height(_attachments.isNotEmpty ? 80 + 8 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: _replyingMessage != null @@ -289,8 +272,7 @@ class ChatMessageInputState extends State { const Gap(8), Expanded( child: Text( - _replyingMessage?.body['text'] ?? - '${_replyingMessage?.sender.nick}', + _replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}', maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -307,8 +289,9 @@ class ChatMessageInputState extends State { ).padding(vertical: 8), ) : const SizedBox.shrink(), - ).height(_replyingMessage != null ? 38 : 0, animate: true).animate( - const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), + ) + .height(_replyingMessage != null ? 38 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: _editingMessage != null @@ -329,8 +312,7 @@ class ChatMessageInputState extends State { const Gap(8), Expanded( child: Text( - _editingMessage?.body['text'] ?? - '${_editingMessage?.sender.nick}', + _editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}', maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -348,8 +330,9 @@ class ChatMessageInputState extends State { ).padding(vertical: 8), ) : const SizedBox.shrink(), - ).height(_editingMessage != null ? 38 : 0, animate: true).animate( - const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), + ) + .height(_editingMessage != null ? 38 : 0, animate: true) + .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), Container( padding: EdgeInsets.symmetric(horizontal: 16), constraints: BoxConstraints(minHeight: 56, maxHeight: 240), @@ -366,14 +349,11 @@ class ChatMessageInputState extends State { ? 'fieldChatMessageDirect'.tr(args: [ '@${ud.getAccountFromCache(widget.otherMember?.accountId)?.name}', ]) - : 'fieldChatMessage'.tr(args: [ - widget.controller.channel?.name ?? 'loading'.tr() - ]), + : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]), border: InputBorder.none, ), textInputAction: TextInputAction.send, - onTapOutside: (_) => - FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onSubmitted: (_) { if (_isBusy) return; _sendMessage(); @@ -388,8 +368,7 @@ class ChatMessageInputState extends State { Symbols.mood, color: Theme.of(context).colorScheme.primary, ), - visualDensity: - const VisualDensity(horizontal: -4, vertical: -4), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: () { @@ -409,8 +388,7 @@ class ChatMessageInputState extends State { Symbols.send, color: Theme.of(context).colorScheme.primary, ), - visualDensity: - const VisualDensity(horizontal: -4, vertical: -4), + visualDensity: const VisualDensity(horizontal: -4, vertical: -4), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), @@ -427,8 +405,7 @@ class _StickerPicker extends StatelessWidget { final Function? onDismiss; final Function(String)? onInsert; - const _StickerPicker( - {this.onDismiss, required this.originalText, this.onInsert}); + const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert}); @override Widget build(BuildContext context) { @@ -439,8 +416,9 @@ class _StickerPicker extends StatelessWidget { }, child: Container( constraints: BoxConstraints( - maxWidth: min(360, MediaQuery.of(context).size.width), - maxHeight: 240), + maxWidth: min(360, MediaQuery.of(context).size.width - 40), + maxHeight: 240, + ), child: Material( elevation: 8, borderRadius: const BorderRadius.all(Radius.circular(8)), @@ -453,10 +431,8 @@ class _StickerPicker extends StatelessWidget { return [ Container( margin: EdgeInsets.only(bottom: 8), - padding: - EdgeInsets.symmetric(horizontal: 8, vertical: 4), - color: - Theme.of(context).colorScheme.surfaceContainerHigh, + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + color: Theme.of(context).colorScheme.surfaceContainerHigh, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -468,8 +444,7 @@ class _StickerPicker extends StatelessWidget { ), GridView.builder( physics: const NeverScrollableScrollPhysics(), - padding: - const EdgeInsets.only(left: 8, right: 8, bottom: 8), + padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), shrinkWrap: true, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 48, @@ -492,8 +467,7 @@ class _StickerPicker extends StatelessWidget { richMessage: TextSpan( children: [ TextSpan( - text: - ':${element.pack.prefix}${element.alias}:\n', + text: ':${element.pack.prefix}${element.alias}:\n', style: GoogleFonts.robotoMono()), TextSpan(text: element.name).bold(), ], @@ -502,15 +476,11 @@ class _StickerPicker extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8)), - color: Theme.of(context) - .colorScheme - .surfaceContainerHigh, + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: Theme.of(context).colorScheme.surfaceContainerHigh, ), child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(8)), + borderRadius: const BorderRadius.all(Radius.circular(8)), child: UniversalImage( sn.getAttachmentUrl(element.attachment.rid), width: 48, diff --git a/lib/widgets/post/publisher_popover.dart b/lib/widgets/post/publisher_popover.dart index 81cefec..d29decd 100644 --- a/lib/widgets/post/publisher_popover.dart +++ b/lib/widgets/post/publisher_popover.dart @@ -6,17 +6,24 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/sn_network.dart'; +import 'package:surface/providers/user_directory.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/universal_image.dart'; +import '../../screens/account/profile_page.dart' show kBadgesMeta; + class PublisherPopoverCard extends StatelessWidget { final SnPublisher data; + const PublisherPopoverCard({super.key, required this.data}); @override Widget build(BuildContext context) { final sn = context.read(); + final ud = context.read(); + + final user = data.type == 0 ? ud.getAccountFromCache(data.accountId) : null; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -41,6 +48,7 @@ class PublisherPopoverCard extends StatelessWidget { AccountImage( content: data.avatar, radius: 20, + borderRadius: data.type == 1 ? 8 : 20, ), Gap(16), Expanded( @@ -68,6 +76,36 @@ class PublisherPopoverCard extends StatelessWidget { const Gap(8) ], ).padding(horizontal: 16), + if (user != null && user.badges.isNotEmpty) const Gap(16), + if (user != null && user.badges.isNotEmpty) + Wrap( + spacing: 4, + children: user.badges + .map( + (ele) => Tooltip( + richMessage: TextSpan( + children: [ + TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), + if (ele.metadata['title'] != null) + TextSpan( + text: '\n${ele.metadata['title']}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan(text: '\n'), + TextSpan( + text: DateFormat.yMEd().format(ele.createdAt), + ), + ], + ), + child: Icon( + kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, + color: kBadgesMeta[ele.type]?.$3, + fill: 1, + ), + ), + ) + .toList(), + ).padding(horizontal: 24), const Gap(16), Row( children: [ @@ -108,10 +146,7 @@ class PublisherPopoverCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('publisherTotalDownvote') - .tr() - .fontSize(13) - .opacity(0.75), + Text('publisherTotalDownvote').tr().fontSize(13).opacity(0.75), Text(data.totalDownvote.toString()), ], ),