mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
## Summary - Add a lifted-message long-press popover with the reaction tray and grouped actions. - Use a native action surface - Preserve taps and scrolling, exclude attached reactions from the lifted preview, and align the composition to the bottom safe area. <img width="1080" height="2424" alt="Screenshot_20260814-160732" src="https://github.com/user-attachments/assets/a9d5fc91-4ee3-427c-8199-1c3be2b4e98a" /> ## Testing - `bin/just mobile-check` - `flutter test test/features/channels/message_actions_test.dart` - `flutter test` (1,364 tests) - Manual interaction review on iPhone and Pixel 10 --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Kenny Lopez <klopez4212@gmail.com> Signed-off-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
140 lines
4.8 KiB
Dart
140 lines
4.8 KiB
Dart
part of '../message_actions.dart';
|
|
|
|
class _QuickReactionRow extends ConsumerWidget {
|
|
final TimelineMessage message;
|
|
|
|
/// The sheet's context, popped before the reaction fires.
|
|
final BuildContext sheetContext;
|
|
|
|
/// The long-pressed message's page context — survives the sheet pop, so the
|
|
/// picker opened from "+" isn't torn down with the sheet.
|
|
final BuildContext pageContext;
|
|
|
|
/// The long-pressed message's page ref. The picker callback outlives this
|
|
/// bottom sheet, so it must not read through the sheet's disposed ref.
|
|
final WidgetRef pageRef;
|
|
|
|
/// Drives the staged glyph reveal when this row is shown in the popover.
|
|
/// The bottom sheet leaves this null and retains its existing static row.
|
|
final Animation<double>? presentationAnimation;
|
|
|
|
final Object? popResult;
|
|
|
|
/// Selects exactly one popover result and side effect. Bottom sheets leave
|
|
/// this null and retain their existing local dismissal behavior.
|
|
final void Function(Object? result, VoidCallback effect)? onSelected;
|
|
|
|
const _QuickReactionRow({
|
|
required this.message,
|
|
required this.sheetContext,
|
|
required this.pageContext,
|
|
required this.pageRef,
|
|
this.presentationAnimation,
|
|
this.popResult,
|
|
this.onSelected,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final customEmoji = ref.watch(customEmojiListProvider);
|
|
final emoji = quickReactionEmoji(
|
|
ref.watch(recentEmojiProvider),
|
|
customShortcodes: {
|
|
for (final entry in customEmoji) entry.shortcode.toLowerCase(),
|
|
},
|
|
);
|
|
final customByShortcode = {
|
|
for (final entry in customEmoji) entry.shortcode.toLowerCase(): entry,
|
|
};
|
|
|
|
void react(String value) {
|
|
// The generic picker is also used for composing and statuses. Record
|
|
// recency here, at the reaction call site, so only reactions drive the
|
|
// quick-reaction row.
|
|
pageRef.read(recentEmojiProvider.notifier).record(value);
|
|
// The sheet is on its way out, so the burst can't come from this tile —
|
|
// hand it to the pill that's about to appear in the timeline.
|
|
armReactionBurst(pageRef, message, value);
|
|
pageRef.read(channelActionsProvider).addReaction(message.id, value);
|
|
}
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
const desiredCircleSize = 52.0;
|
|
const minimumCircleSize = 44.0;
|
|
final itemCount = emoji.length + 1;
|
|
final gapCount = itemCount - 1;
|
|
final circleSize =
|
|
((constraints.maxWidth - (Grid.twelve * gapCount)) / itemCount)
|
|
.clamp(minimumCircleSize, desiredCircleSize)
|
|
.toDouble();
|
|
final gap =
|
|
((constraints.maxWidth - (circleSize * itemCount)) / gapCount)
|
|
.clamp(0.0, Grid.twelve)
|
|
.toDouble();
|
|
final circles = <Widget>[
|
|
for (var index = 0; index < emoji.length; index++)
|
|
_ReactionItemReveal(
|
|
key: ValueKey('quick-reaction-${emoji[index]}'),
|
|
animation: presentationAnimation,
|
|
index: index,
|
|
child: _QuickReactionCircle(
|
|
size: circleSize,
|
|
onTap: () {
|
|
void effect() => react(emoji[index]);
|
|
|
|
final select = onSelected;
|
|
if (select != null) {
|
|
select(popResult, effect);
|
|
} else {
|
|
Navigator.of(sheetContext).pop(popResult);
|
|
effect();
|
|
}
|
|
},
|
|
child: _QuickReactionGlyph(
|
|
value: emoji[index],
|
|
customByShortcode: customByShortcode,
|
|
),
|
|
),
|
|
),
|
|
_ReactionItemReveal(
|
|
key: const ValueKey('quick-reaction-more'),
|
|
animation: presentationAnimation,
|
|
index: emoji.length,
|
|
child: _QuickReactionCircle(
|
|
size: circleSize,
|
|
onTap: () {
|
|
void effect() =>
|
|
showEmojiPicker(context: pageContext, onSelect: react);
|
|
|
|
final select = onSelected;
|
|
if (select != null) {
|
|
select(popResult, effect);
|
|
} else {
|
|
Navigator.of(sheetContext).pop(popResult);
|
|
effect();
|
|
}
|
|
},
|
|
child: Icon(
|
|
LucideIcons.plus,
|
|
size: 24,
|
|
color: context.colors.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
];
|
|
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
for (var index = 0; index < circles.length; index++) ...[
|
|
circles[index],
|
|
if (index < circles.length - 1) SizedBox(width: gap),
|
|
],
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|