🎨 Use feature based folder structure
This commit is contained in:
710
lib/posts/posts_widgets/post/post_item.dart
Normal file
710
lib/posts/posts_widgets/post/post_item.dart
Normal file
@@ -0,0 +1,710 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/posts/posts_models/post.dart';
|
||||
import 'package:island/core/config.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/core/translate.dart';
|
||||
import 'package:island/accounts/accounts_pod.dart';
|
||||
import 'package:island/posts/compose.dart';
|
||||
import 'package:island/core/utils/share_utils.dart';
|
||||
import 'package:island/posts/posts_widgets/post/embed_view_renderer.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_award_sheet.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_pin_sheet.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_reaction_sheet.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_shared.dart';
|
||||
import 'package:island/reports/reports_widgets/safety/abuse_report_helper.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/core/widgets/content/markdown.dart';
|
||||
import 'package:island/core/widgets/content/image.dart';
|
||||
import 'package:island/core/widgets/share/share_sheet.dart';
|
||||
import 'package:island/posts/posts_widgets/compose_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';
|
||||
import 'package:island/core/services/analytics_service.dart';
|
||||
|
||||
const kAvailableStickers = {
|
||||
'angry',
|
||||
'clap',
|
||||
'confuse',
|
||||
'pray',
|
||||
'thumb_up',
|
||||
'party',
|
||||
};
|
||||
|
||||
bool _getReactionImageAvailable(String symbol) {
|
||||
return kAvailableStickers.contains(symbol);
|
||||
}
|
||||
|
||||
Widget _buildReactionIcon(String symbol, double size, {double iconSize = 24}) {
|
||||
if (_getReactionImageAvailable(symbol)) {
|
||||
return Image.asset(
|
||||
'assets/images/stickers/$symbol.png',
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.contain,
|
||||
alignment: Alignment.bottomCenter,
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
kReactionTemplates[symbol]?.icon ?? '',
|
||||
style: TextStyle(fontSize: iconSize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostActionableItem extends HookConsumerWidget {
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isFullPost;
|
||||
final bool isShowReference;
|
||||
final bool isEmbedReply;
|
||||
final bool isEmbedOpenable;
|
||||
final bool isCompact;
|
||||
final bool hideAttachments;
|
||||
final double? borderRadius;
|
||||
final VoidCallback? onRefresh;
|
||||
final Function(SnPost)? onUpdate;
|
||||
final VoidCallback? onOpen;
|
||||
const PostActionableItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.padding,
|
||||
this.isFullPost = false,
|
||||
this.isShowReference = true,
|
||||
this.isEmbedReply = true,
|
||||
this.isEmbedOpenable = false,
|
||||
this.isCompact = false,
|
||||
this.hideAttachments = false,
|
||||
this.borderRadius,
|
||||
this.onRefresh,
|
||||
this.onUpdate,
|
||||
this.onOpen,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userInfoProvider);
|
||||
final isAuthor = useMemoized(
|
||||
() => user.value != null && item.publisher?.accountId == user.value?.id,
|
||||
[user],
|
||||
);
|
||||
|
||||
final config = ref.watch(appSettingsProvider);
|
||||
|
||||
final widgetItem = InkWell(
|
||||
borderRadius: borderRadius != null
|
||||
? BorderRadius.all(Radius.circular(borderRadius!))
|
||||
: null,
|
||||
child: PostItem(
|
||||
key: key,
|
||||
item: item,
|
||||
padding: padding,
|
||||
isFullPost: isFullPost,
|
||||
isShowReference: isShowReference,
|
||||
isEmbedReply: isEmbedReply,
|
||||
isEmbedOpenable: isEmbedOpenable,
|
||||
isTextSelectable: false,
|
||||
isCompact: isCompact,
|
||||
hideAttachments: hideAttachments,
|
||||
onRefresh: onRefresh,
|
||||
onUpdate: onUpdate,
|
||||
onOpen: onOpen,
|
||||
),
|
||||
onTap: () {
|
||||
onOpen?.call();
|
||||
context.pushNamed('postDetail', pathParameters: {'id': item.id});
|
||||
},
|
||||
);
|
||||
|
||||
return ContextMenuWidget(
|
||||
menuProvider: (_) {
|
||||
return Menu(
|
||||
children: [
|
||||
if (isAuthor)
|
||||
MenuAction(
|
||||
title: 'edit'.tr(),
|
||||
image: MenuImage.icon(Symbols.edit),
|
||||
callback: () async {
|
||||
final result = await PostComposeSheet.show(
|
||||
context,
|
||||
originalPost: item,
|
||||
);
|
||||
if (result != null) {
|
||||
onRefresh?.call();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (isAuthor)
|
||||
MenuAction(
|
||||
title: 'delete'.tr(),
|
||||
image: MenuImage.icon(Symbols.delete),
|
||||
callback: () {
|
||||
showConfirmAlert(
|
||||
'deletePostHint'.tr(),
|
||||
'deletePost'.tr(),
|
||||
isDanger: true,
|
||||
).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://solian.app/posts/${item.id}'),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'reply'.tr(),
|
||||
image: MenuImage.icon(Symbols.reply),
|
||||
callback: () async {
|
||||
final result = await PostComposeSheet.show(
|
||||
context,
|
||||
initialState: PostComposeInitialState(replyingTo: item),
|
||||
);
|
||||
if (result != null) {
|
||||
onRefresh?.call();
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'forward'.tr(),
|
||||
image: MenuImage.icon(Symbols.forward),
|
||||
callback: () async {
|
||||
final result = await PostComposeSheet.show(
|
||||
context,
|
||||
initialState: PostComposeInitialState(forwardingTo: item),
|
||||
);
|
||||
if (result != null) {
|
||||
onRefresh?.call();
|
||||
}
|
||||
},
|
||||
),
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'award'.tr(),
|
||||
image: MenuImage.icon(Symbols.star),
|
||||
callback: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostAwardSheet(post: item),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuAction(
|
||||
title: 'share'.tr(),
|
||||
image: MenuImage.icon(Symbols.share),
|
||||
callback: () {
|
||||
showShareSheetLink(
|
||||
context: context,
|
||||
link: 'https://solian.app/posts/${item.id}',
|
||||
title: 'sharePost'.tr(),
|
||||
toSystem: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!kIsWeb)
|
||||
MenuAction(
|
||||
title: 'sharePostPhoto'.tr(),
|
||||
image: MenuImage.icon(Symbols.share_reviews),
|
||||
callback: () {
|
||||
sharePostAsScreenshot(context, ref, item);
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuAction(
|
||||
title: 'abuseReport'.tr(),
|
||||
image: MenuImage.icon(Symbols.flag),
|
||||
callback: () {
|
||||
showAbuseReportSheet(
|
||||
context,
|
||||
resourceIdentifier: 'post:${item.id}',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Material(
|
||||
color: config.cardTransparency < 1
|
||||
? Colors.transparent
|
||||
: Theme.of(context).cardTheme.color,
|
||||
borderRadius: borderRadius != null
|
||||
? BorderRadius.all(Radius.circular(borderRadius!))
|
||||
: null,
|
||||
child: widgetItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostItem extends HookConsumerWidget {
|
||||
final SnPost item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isFullPost;
|
||||
final bool isShowReference;
|
||||
final bool isEmbedReply;
|
||||
final bool isEmbedOpenable;
|
||||
final bool isTextSelectable;
|
||||
final bool isTranslatable;
|
||||
final bool isCompact;
|
||||
final bool hideAttachments;
|
||||
final double? textScale;
|
||||
final VoidCallback? onRefresh;
|
||||
final Function(SnPost)? onUpdate;
|
||||
final VoidCallback? onOpen;
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.padding,
|
||||
this.isFullPost = false,
|
||||
this.isShowReference = true,
|
||||
this.isEmbedReply = true,
|
||||
this.isEmbedOpenable = false,
|
||||
this.isTextSelectable = true,
|
||||
this.isTranslatable = true,
|
||||
this.isCompact = false,
|
||||
this.hideAttachments = false,
|
||||
this.textScale,
|
||||
this.onRefresh,
|
||||
this.onUpdate,
|
||||
this.onOpen,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final renderingPadding =
|
||||
padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8);
|
||||
|
||||
final reacting = useState(false);
|
||||
|
||||
Future<void> 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<String, int>.from(item.reactionsCount);
|
||||
reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta;
|
||||
final reactionsMade = Map<String, bool>.from(item.reactionsMade);
|
||||
reactionsMade[symbol] = delta == 1 ? true : false;
|
||||
onUpdate?.call(
|
||||
item.copyWith(
|
||||
reactionsCount: reactionsCount,
|
||||
reactionsMade: reactionsMade,
|
||||
),
|
||||
);
|
||||
HapticFeedback.heavyImpact();
|
||||
|
||||
AnalyticsService().logPostReacted(
|
||||
item.id,
|
||||
symbol,
|
||||
attitude,
|
||||
isRemoving,
|
||||
);
|
||||
});
|
||||
|
||||
reacting.value = false;
|
||||
}
|
||||
|
||||
final mostReaction = item.reactionsCount.isEmpty
|
||||
? null
|
||||
: item.reactionsCount.entries
|
||||
.sortedBy((e) => e.value)
|
||||
.map((e) => e.key)
|
||||
.last;
|
||||
|
||||
final postLanguage = item.content != null && isTranslatable
|
||||
? ref.watch(detectStringLanguageProvider(item.content!))
|
||||
: null;
|
||||
|
||||
final currentLanguage = isTranslatable ? context.locale.toString() : null;
|
||||
final translatableLanguage = postLanguage != null && isTranslatable
|
||||
? postLanguage.substring(0, 2) != currentLanguage!.substring(0, 2)
|
||||
: false;
|
||||
|
||||
final translating = useState(false);
|
||||
final translatedText = useState<String?>(null);
|
||||
|
||||
Future<void> translate() async {
|
||||
if (!isTranslatable) return;
|
||||
if (translatedText.value != null) {
|
||||
translatedText.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (translating.value) return;
|
||||
if (item.content == null) return;
|
||||
translating.value = true;
|
||||
try {
|
||||
final text = await ref.watch(
|
||||
translateStringProvider(
|
||||
TranslateQuery(
|
||||
text: item.content!,
|
||||
lang: currentLanguage!.substring(0, 2),
|
||||
),
|
||||
).future,
|
||||
);
|
||||
translatedText.value = text;
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
translating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
final translatedWidget = (translatedText.value?.isNotEmpty ?? false)
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider()),
|
||||
const Gap(8),
|
||||
const Text('translated').tr().fontSize(11).opacity(0.75),
|
||||
],
|
||||
),
|
||||
MarkdownTextContent(
|
||||
textStyle: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.bodyMedium!.fontSize! *
|
||||
(textScale ?? 1),
|
||||
),
|
||||
content: translatedText.value!,
|
||||
isSelectable: isTextSelectable,
|
||||
attachments: item.attachments,
|
||||
noMentionChip: item.fediverseUri != null,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null;
|
||||
|
||||
final translatableWidget = (isTranslatable && translatableLanguage)
|
||||
? Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: TextButton.icon(
|
||||
onPressed: translating.value ? null : translate,
|
||||
style: ButtonStyle(
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 2),
|
||||
),
|
||||
visualDensity: const VisualDensity(horizontal: 0, vertical: -4),
|
||||
foregroundColor: WidgetStatePropertyAll(
|
||||
translatedText.value == null ? null : Colors.grey,
|
||||
),
|
||||
),
|
||||
icon: const Icon(Symbols.translate),
|
||||
label: translatedText.value != null
|
||||
? const Text('translated').tr()
|
||||
: translating.value
|
||||
? const Text('translating').tr()
|
||||
: const Text('translate').tr(),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
final translationSection = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [?translatedWidget, ?translatableWidget],
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(renderingPadding.vertical),
|
||||
PostHeader(
|
||||
item: item,
|
||||
isFullPost: isFullPost,
|
||||
isCompact: isCompact,
|
||||
renderingPadding: renderingPadding,
|
||||
trailing: isCompact
|
||||
? null
|
||||
: SizedBox(
|
||||
width: 36,
|
||||
height: 36,
|
||||
child: IconButton(
|
||||
icon: mostReaction == null
|
||||
? const Icon(Symbols.add_reaction)
|
||||
: Badge(
|
||||
label: Center(
|
||||
child: Text(
|
||||
'x${item.reactionsCount[mostReaction]}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
offset: const Offset(4, 20),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.75),
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
child: mostReaction.contains('+')
|
||||
? HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final baseUrl = ref.watch(
|
||||
serverUrlProvider,
|
||||
);
|
||||
final stickerUri =
|
||||
'$baseUrl/sphere/stickers/lookup/$mostReaction/open';
|
||||
return SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: UniversalImage(
|
||||
uri: stickerUri,
|
||||
width: 28,
|
||||
height: 28,
|
||||
fit: BoxFit.contain,
|
||||
).center(),
|
||||
);
|
||||
},
|
||||
)
|
||||
: _buildReactionIcon(mostReaction, 32).padding(
|
||||
bottom:
|
||||
_getReactionImageAvailable(mostReaction)
|
||||
? 2
|
||||
: 0,
|
||||
),
|
||||
),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
(item.reactionsMade[mostReaction] ?? false)
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.5)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return PostReactionSheet(
|
||||
reactionsCount: item.reactionsCount,
|
||||
reactionsMade: item.reactionsMade,
|
||||
onReact: (symbol, attitude) {
|
||||
reactPost(symbol, attitude);
|
||||
},
|
||||
postId: item.id,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -3,
|
||||
vertical: -3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PostBody(
|
||||
item: item,
|
||||
textScale: textScale,
|
||||
isFullPost: isFullPost,
|
||||
isTextSelectable: isTextSelectable,
|
||||
translationSection: translationSection,
|
||||
renderingPadding: renderingPadding,
|
||||
hideAttachments: hideAttachments,
|
||||
),
|
||||
if (item.embedView != null)
|
||||
EmbedViewRenderer(
|
||||
embedView: item.embedView!,
|
||||
maxHeight: 400,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
).padding(horizontal: renderingPadding.horizontal, vertical: 8),
|
||||
if (isShowReference)
|
||||
ReferencedPostWidget(item: item, renderingPadding: renderingPadding),
|
||||
if (item.repliesCount > 0 && isEmbedReply)
|
||||
PostReplyPreview(
|
||||
parent: item,
|
||||
isOpenable: isEmbedOpenable,
|
||||
onOpen: onOpen,
|
||||
).padding(horizontal: renderingPadding.horizontal, top: 8),
|
||||
Gap(renderingPadding.vertical),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostReactionList extends HookConsumerWidget {
|
||||
final String parentId;
|
||||
final Map<String, int> reactions;
|
||||
final Map<String, bool> reactionsMade;
|
||||
final Function(String symbol, int attitude, int delta)? onReact;
|
||||
final EdgeInsets? padding;
|
||||
const PostReactionList({
|
||||
super.key,
|
||||
required this.parentId,
|
||||
required this.reactions,
|
||||
required this.reactionsMade,
|
||||
this.padding,
|
||||
this.onReact,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final submitting = useState(false);
|
||||
|
||||
Future<void> reactPost(String symbol, int attitude) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
submitting.value = true;
|
||||
await client
|
||||
.post(
|
||||
'/sphere/posts/$parentId/reactions',
|
||||
data: {'symbol': symbol, 'attitude': attitude},
|
||||
)
|
||||
.catchError((err) {
|
||||
showErrorAlert(err);
|
||||
return err;
|
||||
})
|
||||
.then((resp) {
|
||||
var isRemoving = resp.statusCode == 204;
|
||||
onReact?.call(symbol, attitude, isRemoving ? -1 : 1);
|
||||
HapticFeedback.heavyImpact();
|
||||
});
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
children: [
|
||||
if (onReact != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
avatar: const Icon(Symbols.add_reaction),
|
||||
label: const Text('react').tr(),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: VisualDensity.minimumDensity,
|
||||
vertical: VisualDensity.minimumDensity,
|
||||
),
|
||||
onPressed: submitting.value
|
||||
? null
|
||||
: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return PostReactionSheet(
|
||||
reactionsCount: reactions,
|
||||
reactionsMade: reactionsMade,
|
||||
onReact: (symbol, attitude) {
|
||||
reactPost(symbol, attitude);
|
||||
},
|
||||
postId: parentId,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
for (final symbol in reactions.keys)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ActionChip(
|
||||
avatar: _buildReactionIcon(symbol, 24),
|
||||
label: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(symbol),
|
||||
Text('x${reactions[symbol]}').bold(),
|
||||
],
|
||||
),
|
||||
onPressed: submitting.value
|
||||
? null
|
||||
: () {
|
||||
reactPost(
|
||||
symbol,
|
||||
kReactionTemplates[symbol]?.attitude ?? 0,
|
||||
);
|
||||
},
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: VisualDensity.minimumDensity,
|
||||
vertical: VisualDensity.minimumDensity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user