refactor: Migrate to scrollabel list

pull/1248/head
Krille 7 months ago
parent 8dd43d8a7f
commit 57db6c88cd
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652

@ -16,7 +16,7 @@ import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:record/record.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/html.dart' as html;
@ -29,6 +29,7 @@ import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/utils/error_reporter.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/app_lock.dart';
@ -104,7 +105,13 @@ class ChatController extends State<ChatPageWithRoom>
String get roomId => widget.room.id;
final AutoScrollController scrollController = AutoScrollController();
final ItemScrollController itemScrollController = ItemScrollController();
final ScrollOffsetController scrollOffsetController =
ScrollOffsetController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
final ScrollOffsetListener scrollOffsetListener =
ScrollOffsetListener.create();
FocusNode inputFocus = FocusNode();
StreamSubscription<html.Event>? onFocusSub;
@ -230,21 +237,20 @@ class ChatController extends State<ChatPageWithRoom>
setReadMarker(eventId: mostRecentEventId);
}
void _updateScrollController() {
void _updateScrollController(double offset) {
if (!mounted) {
return;
}
if (!scrollController.hasClients) return;
if (timeline?.allowNewEvent == false ||
scrollController.position.pixels > 0 && _scrolledUp == false) {
offset > 2 && _scrolledUp == false) {
setState(() => _scrolledUp = true);
} else if (scrollController.position.pixels <= 0 && _scrolledUp == true) {
} else if (offset <= 2 && _scrolledUp == true) {
setState(() => _scrolledUp = false);
setReadMarker();
}
if (scrollController.position.pixels == 0 ||
scrollController.position.pixels == 64) {
if (offset == 0 || offset == 64) {
requestFuture();
}
}
@ -259,7 +265,7 @@ class ChatController extends State<ChatPageWithRoom>
@override
void initState() {
scrollController.addListener(_updateScrollController);
scrollOffsetListener.changes.listen(_updateScrollController);
inputFocus.addListener(_inputFocusListener);
_loadDraft();
@ -292,7 +298,7 @@ class ChatController extends State<ChatPageWithRoom>
if (timeline?.events.any((event) => event.eventId == fullyRead) ??
false) {
Logs().v('Scroll up to visible event', fullyRead);
setReadMarker();
scrollToEventId(fullyRead);
return;
}
if (!mounted) return;
@ -901,7 +907,9 @@ class ChatController extends State<ChatPageWithRoom>
}
void scrollToEventId(String eventId) async {
final eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId);
final events =
timeline!.events.where((event) => event.isVisibleInGui).toList();
final eventIndex = events.indexWhere((e) => e.eventId == eventId);
if (eventIndex == -1) {
setState(() {
timeline = null;
@ -920,11 +928,11 @@ class ChatController extends State<ChatPageWithRoom>
setState(() {
scrollToEventIdMarker = eventId;
});
await scrollController.scrollToIndex(
eventIndex,
preferPosition: AutoScrollPosition.middle,
await itemScrollController.scrollTo(
index: eventIndex + 1,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
_updateScrollController();
}
void scrollDown() async {
@ -939,7 +947,11 @@ class ChatController extends State<ChatPageWithRoom>
});
await loadTimelineFuture;
}
scrollController.jumpTo(0);
itemScrollController.scrollTo(
index: 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
}
void onEmojiSelected(_, Emoji? emoji) {

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
@ -12,7 +12,6 @@ import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
class ChatEventList extends StatelessWidget {
final ChatController controller;
@ -41,119 +40,108 @@ class ChatEventList extends StatelessWidget {
controller.room.client.applicationAccountConfig.wallpaperUrl != null;
return SelectionArea(
child: ListView.custom(
child: ScrollablePositionedList.builder(
padding: EdgeInsets.only(
top: 16,
bottom: 8,
left: horizontalPadding,
right: horizontalPadding,
),
itemCount: events.length + 2,
reverse: true,
controller: controller.scrollController,
keyboardDismissBehavior: PlatformInfos.isIOS
? ScrollViewKeyboardDismissBehavior.onDrag
: ScrollViewKeyboardDismissBehavior.manual,
childrenDelegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
// Footer to display typing indicator and read receipts:
if (i == 0) {
if (controller.timeline!.isRequestingFuture) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
}
if (controller.timeline!.canRequestFuture) {
return Center(
child: IconButton(
onPressed: controller.requestFuture,
icon: const Icon(Icons.refresh_outlined),
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SeenByRow(controller),
TypingIndicators(controller),
],
itemScrollController: controller.itemScrollController,
scrollOffsetController: controller.scrollOffsetController,
itemPositionsListener: controller.itemPositionsListener,
scrollOffsetListener: controller.scrollOffsetListener,
itemBuilder: (BuildContext context, int i) {
// Footer to display typing indicator and read receipts:
if (i == 0) {
if (controller.timeline!.isRequestingFuture) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
}
if (controller.timeline!.canRequestFuture) {
return Center(
child: IconButton(
onPressed: controller.requestFuture,
icon: const Icon(Icons.refresh_outlined),
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SeenByRow(controller),
TypingIndicators(controller),
],
);
}
// Request history button or progress indicator:
if (i == events.length + 1) {
if (controller.timeline!.isRequestingHistory) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
}
if (controller.timeline!.canRequestHistory) {
return Builder(
builder: (context) {
WidgetsBinding.instance
.addPostFrameCallback(controller.requestHistory);
return Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
);
},
);
}
return const SizedBox.shrink();
// Request history button or progress indicator:
if (i == events.length + 1) {
if (controller.timeline!.isRequestingHistory) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
}
if (controller.timeline!.canRequestHistory) {
return Builder(
builder: (context) {
WidgetsBinding.instance
.addPostFrameCallback(controller.requestHistory);
return Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
);
},
);
}
i--;
return const SizedBox.shrink();
}
i--;
// The message at this index:
final event = events[i];
final animateIn = animateInEventIndex != null &&
controller.timeline!.events.length > animateInEventIndex &&
event == controller.timeline!.events[animateInEventIndex];
// The message at this index:
final event = events[i];
final animateIn = animateInEventIndex != null &&
controller.timeline!.events.length > animateInEventIndex &&
event == controller.timeline!.events[animateInEventIndex];
return AutoScrollTag(
key: ValueKey(event.eventId),
index: i,
controller: controller.scrollController,
child: Message(
event,
animateIn: animateIn,
resetAnimateIn: () {
controller.animateInEventIndex = null;
},
onSwipe: () => controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: event.senderFromMemoryOrFallback,
outerContext: context,
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',
),
),
highlightMarker:
controller.scrollToEventIdMarker == event.eventId,
onSelect: controller.onSelectMessage,
scrollToEventId: (String eventId) =>
controller.scrollToEventId(eventId),
longPressSelect: controller.selectedEvents.isNotEmpty,
selected: controller.selectedEvents
.any((e) => e.eventId == event.eventId),
timeline: controller.timeline!,
displayReadMarker:
controller.readMarkerEventId == event.eventId &&
controller.timeline?.allowNewEvent == false,
nextEvent: i + 1 < events.length ? events[i + 1] : null,
previousEvent: i > 0 ? events[i - 1] : null,
avatarPresenceBackgroundColor:
hasWallpaper ? Colors.transparent : null,
return Message(
event,
animateIn: animateIn,
resetAnimateIn: () {
controller.animateInEventIndex = null;
},
onSwipe: () => controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo,
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: event.senderFromMemoryOrFallback,
outerContext: context,
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',
),
);
},
childCount: events.length + 2,
findChildIndexCallback: (key) =>
controller.findChildIndexCallback(key, thisEventsKeyMap),
),
),
highlightMarker: controller.scrollToEventIdMarker == event.eventId,
onSelect: controller.onSelectMessage,
scrollToEventId: (String eventId) =>
controller.scrollToEventId(eventId),
longPressSelect: controller.selectedEvents.isNotEmpty,
selected: controller.selectedEvents
.any((e) => e.eventId == event.eventId),
timeline: controller.timeline!,
displayReadMarker: controller.readMarkerEventId == event.eventId &&
controller.timeline?.allowNewEvent == false,
nextEvent: i + 1 < events.length ? events[i + 1] : null,
previousEvent: i > 0 ? events[i - 1] : null,
avatarPresenceBackgroundColor:
hasWallpaper ? Colors.transparent : null,
);
},
),
);
}

@ -1694,14 +1694,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
scroll_to_index:
scrollable_positioned_list:
dependency: "direct main"
description:
name: scroll_to_index
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
name: scrollable_positioned_list
sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "0.3.8"
sdp_transform:
dependency: transitive
description:

@ -77,7 +77,7 @@ dependencies:
qr_code_scanner: ^1.0.1
receive_sharing_intent: 1.4.5 # Update needs more work
record: ^5.1.2
scroll_to_index: ^3.0.1
scrollable_positioned_list: ^0.3.8
share_plus: ^9.0.0
shared_preferences: ^2.2.0 # Pinned because https://github.com/flutter/flutter/issues/118401
slugify: ^2.0.0
@ -160,4 +160,4 @@ dependency_overrides:
git:
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
ref: null-safety
win32: 5.5.0
win32: 5.5.0

Loading…
Cancel
Save