mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-10 09:05:10 +00:00
feat: Animate in new events in timeline
This commit is contained in:
parent
8f3f5cdb8b
commit
f07aba448d
3 changed files with 120 additions and 78 deletions
|
@ -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!);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue