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
Signed-off-by: cls_宁波本机 <908705107@qq.com>
261 lines
9.1 KiB
Dart
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);
|
|
}
|