parent
47645ea327
commit
7f7bdbf525
2 changed files with 1477 additions and 577 deletions
40
lib/helpers/hooks/chat_composer.dart
Normal file
40
lib/helpers/hooks/chat_composer.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import "package:flutter/services.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/models/relation_type.dart";
|
||||
|
||||
final class ChatComposer({
|
||||
required final ValueNotifier<int?> relatedEvent,
|
||||
required final ValueNotifier<RelationType> relationType,
|
||||
required final FocusNode node,
|
||||
}) {
|
||||
factory use() {
|
||||
final relatedEvent = useState<int?>(null);
|
||||
final relationType = useState(RelationType.reply);
|
||||
final node = useFocusNode(
|
||||
onKeyEvent: (_, event) {
|
||||
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
||||
relatedEvent.value = null;
|
||||
return .handled;
|
||||
}
|
||||
|
||||
return .ignored;
|
||||
},
|
||||
);
|
||||
|
||||
return .new(
|
||||
relatedEvent: relatedEvent,
|
||||
relationType: relationType,
|
||||
node: node,
|
||||
);
|
||||
}
|
||||
|
||||
int? get repliedEventRowId =>
|
||||
relationType.value == .reply ? relatedEvent.value : null;
|
||||
|
||||
void relate(int rowId, RelationType type) {
|
||||
relatedEvent.value = rowId;
|
||||
relationType.value = type;
|
||||
node.requestFocus();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ final class ChatScroll({
|
|||
required final GlobalKey centerKey,
|
||||
required final ScrollController scrollController,
|
||||
required final bool atBottom,
|
||||
required final int? highlightedEvent,
|
||||
|
||||
required final JumpToEvent jumpToEvent,
|
||||
required final Future<void> Function() jumpToBottom,
|
||||
required final GlobalKey Function(int eventRowId) keyFor,
|
||||
|
|
@ -28,6 +30,8 @@ final class ChatScroll({
|
|||
final anchorId = useState<int?>(null);
|
||||
final atBottom = useState(true);
|
||||
final pendingJump = useState<int?>(null);
|
||||
final highlightedEvent = useState<int?>(null);
|
||||
final hookContext = useContext();
|
||||
final pendingJumpDone = useRef<Completer<void>?>(null);
|
||||
void finishPendingJump() {
|
||||
pendingJumpDone.value?.complete();
|
||||
|
|
@ -95,15 +99,14 @@ final class ChatScroll({
|
|||
|
||||
final anchorIndex = items.indexOf(anchor);
|
||||
|
||||
if (anchorIndex == -1) return (history: const .empty(), live: items);
|
||||
|
||||
final historyLength = anchorIndex == -1 ? items.length : anchorIndex + 1;
|
||||
return (
|
||||
history: items.take(anchorIndex).toIList().reversed.toIList(),
|
||||
live: items.skip(anchorIndex).toIList(),
|
||||
history: items.take(historyLength).toIList().reversed.toIList(),
|
||||
live: items.skip(historyLength).toIList(),
|
||||
);
|
||||
}, [controllerData, anchorId.value]);
|
||||
|
||||
final lastRowId = split.live.lastOrNull;
|
||||
final lastRowId = controllerData.value?.timeline.lastOrNull;
|
||||
final previousLastRowId = useRef<int?>(null);
|
||||
|
||||
useEffect(() {
|
||||
|
|
@ -118,7 +121,8 @@ final class ChatScroll({
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!scrollController.hasClients) return;
|
||||
|
||||
scrollController.jumpTo(scrollController.position.minScrollExtent);
|
||||
anchorId.value = lastRowId;
|
||||
scrollController.position.correctPixels(0);
|
||||
});
|
||||
|
||||
return null;
|
||||
|
|
@ -173,37 +177,49 @@ final class ChatScroll({
|
|||
],
|
||||
);
|
||||
|
||||
Future<void> scrollToEvent(int rowId) async {
|
||||
if (!scrollController.hasClients) return;
|
||||
|
||||
final context = keyFor(rowId).currentContext;
|
||||
if (context != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
duration: Durations.extralong1,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
finishPendingJump();
|
||||
final done = pendingJumpDone.value = Completer();
|
||||
pendingJump.value = rowId;
|
||||
if (controllerData.value?.timeline.contains(rowId) ?? false) {
|
||||
anchorId.value = rowId;
|
||||
} else {
|
||||
anchorId.value = null;
|
||||
contextualEvent.value = rowId;
|
||||
}
|
||||
await done.future;
|
||||
}
|
||||
|
||||
return .new(
|
||||
historyRows: split.history,
|
||||
liveRows: split.live,
|
||||
centerKey: centerKey,
|
||||
scrollController: scrollController,
|
||||
atBottom: atBottom.value,
|
||||
highlightedEvent: highlightedEvent.value,
|
||||
keyFor: keyFor,
|
||||
jumpToEvent: (int rowId) async {
|
||||
if (!scrollController.hasClients) return;
|
||||
highlightedEvent.value = rowId;
|
||||
|
||||
final context = keyFor(rowId).currentContext;
|
||||
if (context != null) {
|
||||
await Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
duration: Durations.extralong1,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
finishPendingJump();
|
||||
final done = pendingJumpDone.value = Completer();
|
||||
pendingJump.value = rowId;
|
||||
if (controllerData.value?.timeline.contains(rowId) ?? false) {
|
||||
anchorId.value = rowId;
|
||||
} else {
|
||||
anchorId.value = null;
|
||||
contextualEvent.value = rowId;
|
||||
}
|
||||
await done.future;
|
||||
await scrollToEvent(rowId);
|
||||
await Future.delayed(Durations.extralong1, () {
|
||||
if (hookContext.mounted && highlightedEvent.value == rowId) {
|
||||
highlightedEvent.value = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
jumpToBottom: () async {
|
||||
finishPendingJump();
|
||||
|
|
|
|||
Loading…
Reference in a new issue