feat: Animate in new events in timeline

This commit is contained in:
krille-chan 2023-12-26 20:08:07 +01:00
parent 8f3f5cdb8b
commit f07aba448d
No known key found for this signature in database
3 changed files with 120 additions and 78 deletions

View file

@ -304,6 +304,13 @@ class ChatController extends State<ChatPageWithRoom> {
Future<void>? loadTimelineFuture;
int? animateInEventIndex;
void onInsert(int i) {
// setState will be called by updateView() anyway
animateInEventIndex = i;
}
Future<void> _getTimeline({
String? eventContextId,
}) async {
@ -317,11 +324,15 @@ class ChatController extends State<ChatPageWithRoom> {
timeline = await room.getTimeline(
onUpdate: updateView,
eventContextId: eventContextId,
onInsert: onInsert,
);
} catch (e, s) {
Logs().w('Unable to load timeline on event ID $eventContextId', e, s);
if (!mounted) return;
timeline = await room.getTimeline(onUpdate: updateView);
timeline = await room.getTimeline(
onUpdate: updateView,
onInsert: onInsert,
);
if (!mounted) return;
if (e is TimeoutException || e is IOException) {
_showScrollUpMaterialBanner(eventContextId!);

View file

@ -27,6 +27,7 @@ class ChatEventList extends StatelessWidget {
final events = controller.timeline!.events
.where((event) => event.isVisibleInGui)
.toList();
final animateInEventIndex = controller.animateInEventIndex;
// create a map of eventId --> index to greatly improve performance of
// ListView's findChildIndexCallback
@ -101,6 +102,8 @@ class ChatEventList extends StatelessWidget {
// The message at this index:
final event = events[i];
final animateIn = animateInEventIndex != null &&
event == controller.timeline!.events[animateInEventIndex];
return AutoScrollTag(
key: ValueKey(event.eventId),
@ -108,6 +111,10 @@ class ChatEventList extends StatelessWidget {
controller: controller.scrollController,
child: Message(
event,
animateIn: animateIn,
resetAnimateIn: () {
controller.animateInEventIndex = null;
},
onSwipe: () => controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onAvatarTab: (Event event) => showAdaptiveBottomSheet(

View file

@ -32,6 +32,8 @@ class Message extends StatelessWidget {
final bool selected;
final Timeline timeline;
final bool highlightMarker;
final bool animateIn;
final void Function()? resetAnimateIn;
const Message(
this.event, {
@ -46,6 +48,8 @@ class Message extends StatelessWidget {
this.selected = false,
required this.timeline,
this.highlightMarker = false,
this.animateIn = false,
this.resetAnimateIn,
super.key,
});
@ -382,90 +386,110 @@ class Message extends StatelessWidget {
}
TapDownDetails? lastTapDownDetails;
final resetAnimateIn = this.resetAnimateIn;
var animateIn = this.animateIn;
return Center(
child: Swipeable(
key: ValueKey(event.eventId),
background: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
return StatefulBuilder(
builder: (context, setState) {
if (animateIn && resetAnimateIn != null) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
animateIn = false;
setState(resetAnimateIn);
});
}
return AnimatedSlide(
offset: Offset(0, animateIn ? 1 : 0),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: Center(
child: Icon(Icons.check_outlined),
),
),
direction: SwipeDirection.endToStart,
onSwipe: (_) => onSwipe(),
child: HoverBuilder(
builder: (context, hovered) => GestureDetector(
onTapDown: (details) {
lastTapDownDetails = details;
},
onTap: () {
if (lastTapDownDetails?.kind == PointerDeviceKind.mouse) return;
onSelect(event);
},
child: Stack(
children: [
Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: container,
child: Swipeable(
key: ValueKey(event.eventId),
background: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Center(
child: Icon(Icons.check_outlined),
),
if (hovered || selected)
Positioned(
left: ownMessage ? 4 : null,
right: ownMessage ? null : 4,
bottom: 4,
child: Material(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.9),
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
shadowColor: Theme.of(context).appBarTheme.shadowColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (hovered) ...[
IconButton(
icon: const Icon(
Icons.reply_outlined,
size: 16,
),
tooltip: L10n.of(context)!.reply,
onPressed: () => onSwipe(),
),
],
IconButton(
icon: Icon(
selected
? Icons.check_circle
: longPressSelect
? Icons.check_circle_outlined
: Icons.menu,
size: 16,
),
tooltip: L10n.of(context)!.select,
onPressed: () => onSelect(event),
),
],
),
direction: SwipeDirection.endToStart,
onSwipe: (_) => onSwipe(),
child: HoverBuilder(
builder: (context, hovered) => GestureDetector(
onTapDown: (details) {
lastTapDownDetails = details;
},
onTap: () {
if (lastTapDownDetails?.kind == PointerDeviceKind.mouse) {
return;
}
onSelect(event);
},
child: Stack(
children: [
Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5,
),
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: container,
),
),
if (hovered || selected)
Positioned(
left: ownMessage ? 4 : null,
right: ownMessage ? null : 4,
bottom: 4,
child: Material(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.9),
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
shadowColor:
Theme.of(context).appBarTheme.shadowColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (hovered) ...[
IconButton(
icon: const Icon(
Icons.reply_outlined,
size: 16,
),
tooltip: L10n.of(context)!.reply,
onPressed: () => onSwipe(),
),
],
IconButton(
icon: Icon(
selected
? Icons.check_circle
: longPressSelect
? Icons.check_circle_outlined
: Icons.menu,
size: 16,
),
tooltip: L10n.of(context)!.select,
onPressed: () => onSelect(event),
),
],
),
),
),
],
),
],
),
),
),
),
),
),
);
},
);
}
}