Compare commits
3 changed files with 49 additions and 128 deletions
|
|
@ -1,5 +1,3 @@
|
||||||
import "dart:async";
|
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:flutter_hooks/flutter_hooks.dart";
|
import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
|
@ -12,7 +10,6 @@ final class ChatScroll({
|
||||||
required final IList<Event> historyItems,
|
required final IList<Event> historyItems,
|
||||||
required final IList<Event> liveItems,
|
required final IList<Event> liveItems,
|
||||||
required final GlobalKey centerKey,
|
required final GlobalKey centerKey,
|
||||||
required final GlobalKey anchorItemKey,
|
|
||||||
required final ListController historyListController,
|
required final ListController historyListController,
|
||||||
required final ListController liveListController,
|
required final ListController liveListController,
|
||||||
required final ScrollController scrollController,
|
required final ScrollController scrollController,
|
||||||
|
|
@ -22,85 +19,30 @@ final class ChatScroll({
|
||||||
required AsyncValue<RoomChat?> controllerData,
|
required AsyncValue<RoomChat?> controllerData,
|
||||||
required Future<void> Function() loadOlder,
|
required Future<void> Function() loadOlder,
|
||||||
required Future<void> Function() markRead,
|
required Future<void> Function() markRead,
|
||||||
required String? contextualEvent,
|
|
||||||
required void Function(String id) setContextualEvent,
|
|
||||||
}) {
|
}) {
|
||||||
final anchorId = useState<String?>(null);
|
final historyListController = useRef(ListController());
|
||||||
|
final liveListController = useRef(ListController());
|
||||||
final anchorItemKey = useMemoized(GlobalKey.new, [anchorId.value]);
|
|
||||||
|
|
||||||
final historyListController = useMemoized(ListController.new, [
|
|
||||||
anchorId.value,
|
|
||||||
]);
|
|
||||||
final liveListController = useMemoized(ListController.new, [
|
|
||||||
anchorId.value,
|
|
||||||
]);
|
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final centerKey = useMemoized(GlobalKey.new);
|
final centerKey = useMemoized(GlobalKey.new);
|
||||||
|
|
||||||
final pendingAnchorTarget = useState<String?>(null);
|
final anchorId = useState<String?>(null);
|
||||||
final anchorMountedCompleter = useRef<Completer<BuildContext>?>(null);
|
|
||||||
|
final anchorIdValue = anchorId.value;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (anchorId.value == null) {
|
if (anchorId.value == null) {
|
||||||
if (controllerData case AsyncData(:final value?)
|
if (controllerData case AsyncData(:final value?)
|
||||||
when value.timeline.isNotEmpty) {
|
when value.timeline.isNotEmpty) {
|
||||||
final hasContextualEvent = value.timeline.any(
|
anchorId.value = value.timeline.last.eventId;
|
||||||
(event) => event.eventId == contextualEvent,
|
|
||||||
);
|
|
||||||
|
|
||||||
anchorId.value = hasContextualEvent
|
|
||||||
? contextualEvent
|
|
||||||
: value.timeline.last.eventId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [controllerData, contextualEvent]);
|
}, [controllerData]);
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
final target = pendingAnchorTarget.value;
|
|
||||||
if (target == null) return null;
|
|
||||||
|
|
||||||
final found =
|
|
||||||
controllerData.value?.timeline.any(
|
|
||||||
(event) => event.eventId == target,
|
|
||||||
) ??
|
|
||||||
false;
|
|
||||||
|
|
||||||
if (found || controllerData is AsyncError) {
|
|
||||||
if (found) {
|
|
||||||
anchorId.value = target;
|
|
||||||
} else {
|
|
||||||
anchorMountedCompleter.value?.completeError(
|
|
||||||
StateError("Failed to load context for $target"),
|
|
||||||
);
|
|
||||||
anchorMountedCompleter.value = null;
|
|
||||||
}
|
|
||||||
pendingAnchorTarget.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [controllerData, pendingAnchorTarget.value]);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
final completer = anchorMountedCompleter.value;
|
|
||||||
if (completer == null) return null;
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final context = anchorItemKey.currentContext;
|
|
||||||
if (context != null && context.mounted) {
|
|
||||||
anchorMountedCompleter.value?.complete(context);
|
|
||||||
anchorMountedCompleter.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [anchorId.value]);
|
|
||||||
|
|
||||||
final ({IList<Event> history, IList<Event> live}) split = useMemoized(() {
|
final ({IList<Event> history, IList<Event> live}) split = useMemoized(() {
|
||||||
final items = controllerData.value?.timeline;
|
final items = controllerData.value?.timeline;
|
||||||
final anchor = anchorId.value;
|
final anchor = anchorIdValue;
|
||||||
|
|
||||||
if (items == null || anchor == null) {
|
if (items == null || anchor == null) {
|
||||||
return (history: const .empty(), live: const .empty());
|
return (history: const .empty(), live: const .empty());
|
||||||
|
|
@ -116,16 +58,14 @@ final class ChatScroll({
|
||||||
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
||||||
live: items.skip(anchorIndex).toIList(),
|
live: items.skip(anchorIndex).toIList(),
|
||||||
);
|
);
|
||||||
}, [controllerData, anchorId.value]);
|
}, [controllerData, anchorIdValue]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() {
|
||||||
() {
|
|
||||||
const topThreshold = 500.0;
|
const topThreshold = 500.0;
|
||||||
const bottomThreshold = 50.0;
|
const bottomThreshold = 50.0;
|
||||||
|
|
||||||
Future<void> checkPosition() async {
|
Future<void> checkPosition() async {
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
if (contextualEvent != null) return;
|
|
||||||
|
|
||||||
final position = scrollController.position;
|
final position = scrollController.position;
|
||||||
|
|
||||||
|
|
@ -140,34 +80,40 @@ final class ChatScroll({
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition());
|
WidgetsBinding.instance.addPostFrameCallback((_) => checkPosition());
|
||||||
|
|
||||||
return () => scrollController.removeListener(checkPosition);
|
return () {
|
||||||
},
|
scrollController.removeListener(checkPosition);
|
||||||
[scrollController, controllerData, loadOlder, markRead, contextualEvent],
|
};
|
||||||
);
|
}, [scrollController, controllerData, loadOlder, markRead]);
|
||||||
|
|
||||||
double? resolveOffset(String itemId) {
|
|
||||||
final historyIndex = split.history.indexWhere(
|
|
||||||
(item) => item.eventId == itemId,
|
|
||||||
);
|
|
||||||
if (historyIndex != -1) {
|
|
||||||
// TODO: Replace SuperSliverView because of the bug that requires this: #94
|
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
|
||||||
return historyListController.getOffsetToReveal(historyIndex, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
final liveIndex = split.live.indexWhere((item) => item.eventId == itemId);
|
|
||||||
if (liveIndex != -1) {
|
|
||||||
// ignore: invalid_use_of_visible_for_testing_member
|
|
||||||
return liveListController.getOffsetToReveal(liveIndex, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> jumpToId(String itemId) async {
|
Future<void> jumpToId(String itemId) async {
|
||||||
if (!scrollController.hasClients) return;
|
if (!scrollController.hasClients) return;
|
||||||
|
|
||||||
final offset = resolveOffset(itemId);
|
final historyIndex = split.history.indexWhere(
|
||||||
|
(item) => item.eventId == itemId,
|
||||||
|
);
|
||||||
|
|
||||||
|
double? offset;
|
||||||
|
if (historyIndex != -1) {
|
||||||
|
// TODO: Replace SuperSliverView because of the bug that requires this: #94
|
||||||
|
// ignore: invalid_use_of_visible_for_testing_member
|
||||||
|
offset = historyListController.value.getOffsetToReveal(
|
||||||
|
historyIndex,
|
||||||
|
0.5,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final liveIndex = split.live.indexWhere(
|
||||||
|
(item) => item.eventId == itemId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (liveIndex != -1) {
|
||||||
|
// ignore: invalid_use_of_visible_for_testing_member
|
||||||
|
offset = liveListController.value.getOffsetToReveal(liveIndex, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset == null) {
|
||||||
|
// not in current timeline
|
||||||
|
}
|
||||||
|
|
||||||
if (offset != null) {
|
if (offset != null) {
|
||||||
await scrollController.animateTo(
|
await scrollController.animateTo(
|
||||||
|
|
@ -175,23 +121,6 @@ final class ChatScroll({
|
||||||
duration: const Duration(milliseconds: 700),
|
duration: const Duration(milliseconds: 700),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final completer = Completer<BuildContext>();
|
|
||||||
anchorMountedCompleter.value = completer;
|
|
||||||
pendingAnchorTarget.value = itemId;
|
|
||||||
setContextualEvent(itemId);
|
|
||||||
|
|
||||||
final context = await completer.future;
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
await Scrollable.ensureVisible(
|
|
||||||
context,
|
|
||||||
alignment: 0.5,
|
|
||||||
duration: const Duration(milliseconds: 700),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,9 +128,8 @@ final class ChatScroll({
|
||||||
historyItems: split.history,
|
historyItems: split.history,
|
||||||
liveItems: split.live,
|
liveItems: split.live,
|
||||||
centerKey: centerKey,
|
centerKey: centerKey,
|
||||||
anchorItemKey: anchorItemKey,
|
historyListController: historyListController.value,
|
||||||
historyListController: historyListController,
|
liveListController: liveListController.value,
|
||||||
liveListController: liveListController,
|
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
jumpToId: jumpToId,
|
jumpToId: jumpToId,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ class const ChatTimeline({
|
||||||
required Future<void> Function(String) jumpToId,
|
required Future<void> Function(String) jumpToId,
|
||||||
required IList<PopupMenuEntry> Function(Event) getEventOptions,
|
required IList<PopupMenuEntry> Function(Event) getEventOptions,
|
||||||
required String? highlightedEvent,
|
required String? highlightedEvent,
|
||||||
required Key key,
|
|
||||||
}) => HighlightWrapper(
|
}) => HighlightWrapper(
|
||||||
EventRenderer(
|
EventRenderer(
|
||||||
event,
|
event,
|
||||||
|
|
@ -36,7 +35,7 @@ class const ChatTimeline({
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
isGrouped: isGrouped(event, previousEvent),
|
isGrouped: isGrouped(event, previousEvent),
|
||||||
),
|
),
|
||||||
key: key,
|
key: ValueKey(event.eventId),
|
||||||
isHighlighted: highlightedEvent == event.eventId,
|
isHighlighted: highlightedEvent == event.eventId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -60,9 +59,6 @@ class const ChatTimeline({
|
||||||
jumpToId: jumpToId,
|
jumpToId: jumpToId,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
highlightedEvent: highlightedEvent,
|
highlightedEvent: highlightedEvent,
|
||||||
key: index == 0
|
|
||||||
? scroll.anchorItemKey
|
|
||||||
: ValueKey(scroll.liveItems[index].eventId),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -76,7 +72,6 @@ class const ChatTimeline({
|
||||||
jumpToId: jumpToId,
|
jumpToId: jumpToId,
|
||||||
getEventOptions: getEventOptions,
|
getEventOptions: getEventOptions,
|
||||||
highlightedEvent: highlightedEvent,
|
highlightedEvent: highlightedEvent,
|
||||||
key: ValueKey(scroll.historyItems[index].eventId),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,6 @@ final class const RoomChat({
|
||||||
final scroll = ChatScroll.use(
|
final scroll = ChatScroll.use(
|
||||||
controllerData: controllerData,
|
controllerData: controllerData,
|
||||||
loadOlder: notifier.loadOlder,
|
loadOlder: notifier.loadOlder,
|
||||||
contextualEvent: contextualEvent.value,
|
|
||||||
setContextualEvent: (id) => contextualEvent.value = id,
|
|
||||||
markRead: () async {
|
markRead: () async {
|
||||||
final room = ref.read(
|
final room = ref.read(
|
||||||
RoomsController.provider.select((rooms) => rooms[roomId]),
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue