Files
buzz/mobile/lib/features/channels/local_message_send_transition.dart
klopez4212 aeb741fd31 Polish mobile channel navigation and message sends (#6488)
## Summary
- align iOS back controls and affected titles across channel details,
settings, and pairing
- make channel star and mute actions reflect their state immediately
- animate locally sent channel, thread, and DM messages from behind the
composer with a 300ms ease-out

## Testing
- `just mobile-check`
- full mobile test suite (1,573 tests)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Princess Donut <b238ea756dee4d98afa5883fc7f1de61eeabe65bf700e3a5a5a80db5e42e2c2b@buzz.block.builderlab.xyz>
2026-08-21 19:13:16 +01:00

73 lines
2.2 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// Duration of a local message's entrance transition.
const localMessageSendTransitionDuration = Duration(milliseconds: 300);
/// Default vertical offset, in child heights, for a message entrance.
const localMessageSendTransitionStartOffset = 1.5;
/// Vertical offset, in child heights, used for an avatar entrance.
const localMessageSendTransitionAvatarStartOffset = 2.5;
/// Slides a newly sent local message upward from beneath the composer.
class LocalMessageSendTransition extends HookWidget {
/// Creates a local send transition around [child].
const LocalMessageSendTransition({
super.key,
required this.animate,
this.startOffsetFactor = localMessageSendTransitionStartOffset,
required this.child,
});
/// Whether to play the entrance transition.
final bool animate;
/// Initial vertical offset expressed as a multiple of the child's height.
final double startOffsetFactor;
/// Message content translated by the transition.
final Widget child;
@override
Widget build(BuildContext context) {
final reduceMotion = MediaQuery.disableAnimationsOf(context);
final controller = useAnimationController(
duration: localMessageSendTransitionDuration,
initialValue: animate && !reduceMotion ? 0 : 1,
);
final easedAnimation = useMemoized(
() => CurvedAnimation(parent: controller, curve: Curves.easeOutCubic),
[controller],
);
useEffect(() => easedAnimation.dispose, [easedAnimation]);
useEffect(() {
if (animate && !reduceMotion) {
unawaited(
controller.animateTo(
1,
duration: localMessageSendTransitionDuration,
curve: Curves.linear,
),
);
} else {
controller.value = 1;
}
return null;
}, [animate, controller, reduceMotion]);
return AnimatedBuilder(
animation: easedAnimation,
child: child,
builder: (context, child) => FractionalTranslation(
translation: Offset(0, startOffsetFactor * (1 - easedAnimation.value)),
transformHitTests: false,
child: child,
),
);
}
}