Files
buzz/mobile/lib/features/channels/message_actions/reaction_popover.dart
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

261 lines
9.1 KiB
Dart

part of '../message_actions.dart';
const _reactionTrayMaxWidth = (52.0 * 6) + (Grid.twelve * 5) + (Grid.xxs * 2);
const _reactionTrayMaxHeight = 52.0 + (Grid.xxs * 2);
const _reactionTraySpringAllowance = 16.0;
const _reactionPopoverDuration = Duration(milliseconds: 320);
final _reactionSpringCurve = _SpringEaseOutCurve(
duration: const Duration(milliseconds: 260),
bounce: 0.22,
);
/// Presents the frequently-used reaction tray over a long-pressed message.
///
/// The conversation is frosted around [anchorRect] so the selected message
/// remains visually connected to the tray.
void _showMessageReactionPopover({
required BuildContext context,
required WidgetRef ref,
required TimelineMessage message,
required Rect anchorRect,
required EdgeInsets spotlightPadding,
}) {
unawaited(HapticFeedback.mediumImpact());
final reduceMotion = MediaQuery.disableAnimationsOf(context);
showGeneralDialog<void>(
context: context,
barrierDismissible: true,
barrierLabel: 'Dismiss reaction picker',
barrierColor: Colors.transparent,
transitionDuration: reduceMotion ? Duration.zero : _reactionPopoverDuration,
transitionBuilder: (context, animation, secondaryAnimation, child) => child,
pageBuilder: (dialogContext, animation, secondaryAnimation) =>
_MessageReactionPopover(
anchorRect: anchorRect,
spotlightPadding: spotlightPadding,
animation: animation,
message: message,
pageContext: context,
pageRef: ref,
),
);
}
class _MessageReactionPopover extends StatelessWidget {
final Rect anchorRect;
final EdgeInsets spotlightPadding;
final Animation<double> animation;
final TimelineMessage message;
final BuildContext pageContext;
final WidgetRef pageRef;
const _MessageReactionPopover({
required this.anchorRect,
required this.spotlightPadding,
required this.animation,
required this.message,
required this.pageContext,
required this.pageRef,
});
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
return LayoutBuilder(
builder: (context, constraints) {
final safeTop = mediaQuery.padding.top + mediaQuery.viewInsets.top;
final safeBottom =
constraints.maxHeight -
mediaQuery.padding.bottom -
mediaQuery.viewInsets.bottom;
final availableAbove = anchorRect.top - Grid.xxs - safeTop;
final availableBelow = safeBottom - anchorRect.bottom - Grid.xxs;
final showAbove =
availableAbove >= _reactionTrayMaxHeight ||
(availableBelow < _reactionTrayMaxHeight &&
availableAbove >= availableBelow);
final proposedTop = showAbove
? anchorRect.top - _reactionTrayMaxHeight - Grid.xxs
: anchorRect.bottom + Grid.xxs;
final maxTop = math.max(safeTop, safeBottom - _reactionTrayMaxHeight);
final top = proposedTop.clamp(safeTop, maxTop).toDouble();
final trayScaleAlignment = showAbove
? Alignment.bottomLeft
: Alignment.topLeft;
final trayWidth = math.min(
_reactionTrayMaxWidth,
constraints.maxWidth - (Grid.xxs * 2),
);
final maxLeft = constraints.maxWidth - trayWidth - Grid.xxs;
final left = (anchorRect.center.dx - (trayWidth / 2))
.clamp(Grid.xxs, maxLeft)
.toDouble();
return Stack(
children: [
Positioned.fill(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
final blurProgress = const Interval(
0,
0.30,
curve: Curves.easeOutCubic,
).transform(animation.value);
final sigma = 20 * blurProgress;
return ClipPath(
key: const ValueKey('reaction-popover-background'),
clipper: _OutsideAnchorClipper(
anchorRect,
spotlightPadding,
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: ColoredBox(
color: context.colors.inverseSurface.withValues(
alpha: 0.10 * blurProgress,
),
),
),
);
},
),
),
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.of(context).pop(),
),
),
Positioned(
top: top,
left: left,
width: trayWidth + _reactionTraySpringAllowance,
height: _reactionTrayMaxHeight,
child: AnimatedBuilder(
animation: animation,
child: SizedBox(
width: trayWidth,
height: _reactionTrayMaxHeight,
child: Padding(
padding: const EdgeInsets.all(Grid.xxs),
child: _QuickReactionRow(
message: message,
sheetContext: context,
pageContext: pageContext,
pageRef: pageRef,
presentationAnimation: animation,
),
),
),
builder: (context, child) {
final appearance = const Interval(
0.04,
0.23,
curve: Curves.easeOutCubic,
).transform(animation.value);
final expansion = const Interval(
0.16,
0.92,
).transform(animation.value);
final springExpansion = _reactionSpringCurve.transform(
expansion,
);
final width = lerpDouble(
_reactionTrayMaxHeight,
trayWidth,
springExpansion,
)!;
return Opacity(
opacity: appearance,
child: Transform.scale(
alignment: trayScaleAlignment,
scale: lerpDouble(0.95, 1, appearance)!,
child: Align(
alignment: Alignment.centerLeft,
child: SizedBox(
key: const ValueKey('reaction-popover-tray'),
width: width,
height: _reactionTrayMaxHeight,
child: Material(
color: context.colors.surface,
surfaceTintColor: Colors.transparent,
elevation: 8,
shadowColor: Colors.black.withValues(alpha: 0.2),
shape: const StadiumBorder(),
clipBehavior: Clip.antiAlias,
child: OverflowBox(
alignment: Alignment.centerLeft,
minWidth: trayWidth,
maxWidth: trayWidth,
minHeight: _reactionTrayMaxHeight,
maxHeight: _reactionTrayMaxHeight,
child: child,
),
),
),
),
),
);
},
),
),
],
);
},
);
}
}
class _OutsideAnchorClipper extends CustomClipper<Path> {
final Rect anchorRect;
final EdgeInsets spotlightPadding;
const _OutsideAnchorClipper(this.anchorRect, this.spotlightPadding);
@override
Path getClip(Size size) {
final spotlightRect = Rect.fromLTRB(
anchorRect.left - spotlightPadding.left,
anchorRect.top - spotlightPadding.top,
anchorRect.right + spotlightPadding.right,
anchorRect.bottom + spotlightPadding.bottom,
);
return Path()
..fillType = PathFillType.evenOdd
..addRect(Offset.zero & size)
..addRRect(
RRect.fromRectAndRadius(spotlightRect, const Radius.circular(Radii.lg)),
);
}
@override
bool shouldReclip(_OutsideAnchorClipper oldClipper) =>
oldClipper.anchorRect != anchorRect ||
oldClipper.spotlightPadding != spotlightPadding;
}
class _SpringEaseOutCurve extends Curve {
final double _durationSeconds;
final SpringSimulation _simulation;
_SpringEaseOutCurve({required Duration duration, required double bounce})
: _durationSeconds =
duration.inMicroseconds / Duration.microsecondsPerSecond,
_simulation = SpringSimulation(
SpringDescription.withDurationAndBounce(
duration: duration,
bounce: bounce,
),
0,
1,
0,
snapToEnd: true,
);
@override
double transformInternal(double t) => _simulation.x(t * _durationSeconds);
}