timeline fixups
This commit is contained in:
parent
c658034d3b
commit
ac31fbb74d
2 changed files with 46 additions and 7 deletions
|
|
@ -9,6 +9,14 @@ import "package:nexus/models/room_chat.dart";
|
||||||
|
|
||||||
typedef JumpToEvent = Future<void> Function(int rowId);
|
typedef JumpToEvent = Future<void> Function(int rowId);
|
||||||
|
|
||||||
|
/// Identifies the row for the event with [rowId], so it can be found to
|
||||||
|
/// scroll to it, and keeps its element when rows are added before it.
|
||||||
|
final class EventRowKey extends GlobalKey {
|
||||||
|
EventRowKey(this.rowId) : super.constructor();
|
||||||
|
|
||||||
|
final int rowId;
|
||||||
|
}
|
||||||
|
|
||||||
final class ChatScroll({
|
final class ChatScroll({
|
||||||
required final IList<int> historyRows,
|
required final IList<int> historyRows,
|
||||||
required final IList<int> liveRows,
|
required final IList<int> liveRows,
|
||||||
|
|
@ -20,7 +28,7 @@ final class ChatScroll({
|
||||||
|
|
||||||
required final JumpToEvent jumpToEvent,
|
required final JumpToEvent jumpToEvent,
|
||||||
required final Future<void> Function() jumpToBottom,
|
required final Future<void> Function() jumpToBottom,
|
||||||
required final GlobalKey Function(int eventRowId) keyFor,
|
required final EventRowKey Function(int eventRowId) keyFor,
|
||||||
}) {
|
}) {
|
||||||
factory use({
|
factory use({
|
||||||
required AsyncValue<RoomChat?> controllerData,
|
required AsyncValue<RoomChat?> controllerData,
|
||||||
|
|
@ -42,9 +50,9 @@ final class ChatScroll({
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final centerKey = useMemoized(GlobalKey.new);
|
final centerKey = useMemoized(GlobalKey.new);
|
||||||
|
|
||||||
final itemKeys = useMemoized(() => <int, GlobalKey>{}, []);
|
final itemKeys = useMemoized(() => <int, EventRowKey>{}, []);
|
||||||
GlobalKey keyFor(int eventRowId) =>
|
EventRowKey keyFor(int eventRowId) =>
|
||||||
itemKeys.putIfAbsent(eventRowId, GlobalKey.new);
|
itemKeys.putIfAbsent(eventRowId, () => EventRowKey(eventRowId));
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (anchorId.value == null) {
|
if (anchorId.value == null) {
|
||||||
|
|
@ -117,6 +125,22 @@ final class ChatScroll({
|
||||||
final lastRowId = controllerData.value?.timeline.lastOrNull;
|
final lastRowId = controllerData.value?.timeline.lastOrNull;
|
||||||
final previousLastRowId = useRef<int?>(null);
|
final previousLastRowId = useRef<int?>(null);
|
||||||
|
|
||||||
|
void updateAtBottom() {
|
||||||
|
if (!hookContext.mounted || !scrollController.hasClients) return;
|
||||||
|
|
||||||
|
final position = scrollController.position;
|
||||||
|
final isAtBottom = position.extentBefore <= 0;
|
||||||
|
if (isAtBottom != atBottom.value) {
|
||||||
|
// TODO: remove, debugging replies not showing
|
||||||
|
debugPrint(
|
||||||
|
"[timeline] atBottom: $isAtBottom pixels=${position.pixels} "
|
||||||
|
"min=${position.minScrollExtent} anchor=${anchorId.value} "
|
||||||
|
"live=${split.live.length}",
|
||||||
|
);
|
||||||
|
atBottom.value = isAtBottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final previous = previousLastRowId.value;
|
final previous = previousLastRowId.value;
|
||||||
previousLastRowId.value = lastRowId;
|
previousLastRowId.value = lastRowId;
|
||||||
|
|
@ -141,6 +165,10 @@ final class ChatScroll({
|
||||||
|
|
||||||
anchorId.value = lastRowId;
|
anchorId.value = lastRowId;
|
||||||
scrollController.position.correctPixels(0);
|
scrollController.position.correctPixels(0);
|
||||||
|
|
||||||
|
// Other callbacks this frame still see the new event below the old
|
||||||
|
// anchor, so they think it isn't at the bottom until the next frame
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => updateAtBottom());
|
||||||
});
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -154,9 +182,7 @@ final class ChatScroll({
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
|
|
||||||
final position = scrollController.position;
|
final position = scrollController.position;
|
||||||
|
updateAtBottom();
|
||||||
final isAtBottom = position.extentBefore <= 0;
|
|
||||||
if (isAtBottom != atBottom.value) atBottom.value = isAtBottom;
|
|
||||||
|
|
||||||
// contextualEvent updates immediately when jumping, but
|
// contextualEvent updates immediately when jumping, but
|
||||||
// controllerData is only for the historical timeline once it loads
|
// controllerData is only for the historical timeline once it loads
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,16 @@ class const ChatTimeline({
|
||||||
isHighlighted: scroll.highlightedEvent == eventRowId,
|
isHighlighted: scroll.highlightedEvent == eventRowId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Where the row with [key] now is in [rows], so its element moves with it
|
||||||
|
/// when rows are added before it, rather than being rebuilt by index.
|
||||||
|
int? indexOf(IList<int> rows, Key key) => switch (key) {
|
||||||
|
EventRowKey(:final rowId) => switch (rows.indexOf(rowId)) {
|
||||||
|
-1 => null,
|
||||||
|
final index => index,
|
||||||
|
},
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => LayoutBuilder(
|
Widget build(BuildContext context) => LayoutBuilder(
|
||||||
builder: (context, constraints) => TweenAnimationBuilder(
|
builder: (context, constraints) => TweenAnimationBuilder(
|
||||||
|
|
@ -66,6 +76,7 @@ class const ChatTimeline({
|
||||||
|
|
||||||
SuperSliverList.builder(
|
SuperSliverList.builder(
|
||||||
itemCount: scroll.liveRows.length,
|
itemCount: scroll.liveRows.length,
|
||||||
|
findChildIndexCallback: (key) => indexOf(scroll.liveRows, key),
|
||||||
itemBuilder: (_, index) => eventRow(
|
itemBuilder: (_, index) => eventRow(
|
||||||
scroll.liveRows[index],
|
scroll.liveRows[index],
|
||||||
index > 0
|
index > 0
|
||||||
|
|
@ -78,6 +89,8 @@ class const ChatTimeline({
|
||||||
SuperSliverList.builder(
|
SuperSliverList.builder(
|
||||||
key: scroll.centerKey,
|
key: scroll.centerKey,
|
||||||
itemCount: scroll.historyRows.length,
|
itemCount: scroll.historyRows.length,
|
||||||
|
findChildIndexCallback: (key) =>
|
||||||
|
indexOf(scroll.historyRows, key),
|
||||||
itemBuilder: (_, index) => eventRow(
|
itemBuilder: (_, index) => eventRow(
|
||||||
scroll.historyRows[index],
|
scroll.historyRows[index],
|
||||||
scroll.historyRows.getOrNull(index + 1),
|
scroll.historyRows.getOrNull(index + 1),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue