import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:material_ui/material_ui.dart"; import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:measure_size/measure_size.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/member_list_opened.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/room_chat.dart"; import "package:nexus/helpers/hooks/chat_scroll.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/composer/composer.dart"; import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/pinned_events_drawer.dart"; import "package:nexus/widgets/member_list.dart"; import "package:nexus/widgets/room_appbar.dart"; import "package:nexus/main.dart"; import "package:nexus/widgets/room_chat/chat_timeline.dart"; import "package:nexus/helpers/extensions/build_event_options.dart"; import "package:nexus/controllers/jump_request.dart"; final class const RoomChat({ required final String? roomId, required final bool isDesktop, required final bool showMembersByDefault, super.key, }) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final relatedEvent = useState(null); final relationType = useState(RelationType.reply); final pendingJump = ref.read(JumpRequestController.provider); final contextualEvent = useState( pendingJump != null && pendingJump.roomId == this.roomId ? pendingJump.rowId : null, ); final highlightedEvent = useState(null); final composerSize = useState(64); final userId = ref.watch(ClientStateController.provider)?.userId; final memberListOpened = ref .watch(MemberListOpenedController.provider) .requireValue; final theme = Theme.of(context); final nothing = Center( child: Text( "Nothing to see here...", style: theme.textTheme.headlineMedium, ), ); if (userId == null || this.roomId == null) { return Scaffold( appBar: RoomAppbar( roomId: this.roomId, isDesktop: isDesktop, onOpenDrawer: Scaffold.of(context).openDrawer, ), body: nothing, ); } final roomId = this.roomId!; final jumpRowId = ref.watch( JumpRequestController.provider.select( (request) => request?.roomId == roomId ? request?.rowId : null, ), ); final controllerProvider = RoomChatController.provider(( roomId: roomId, contextualEvent: contextualEvent.value, )); final notifier = ref.watch(controllerProvider.notifier); final client = ref.read(ClientController.provider.notifier); final controllerData = ref.watch(controllerProvider); final scroll = ChatScroll.use( controllerData: controllerData, paginate: notifier.paginate, contextualEvent: contextualEvent, markRead: () async { final room = ref.read( RoomsController.provider.select((rooms) => rooms[roomId]), ); if (room != null) { await client.markRead(room); } }, ); final lastComposerHeight = useRef(composerSize.value); useEffect(() { final delta = composerSize.value - lastComposerHeight.value; lastComposerHeight.value = composerSize.value; if (delta != 0) { final wasAtBottom = scroll.atBottom; WidgetsBinding.instance.addPostFrameCallback((_) { final controller = scroll.scrollController; if (!controller.hasClients) return; final position = controller.position; controller.jumpTo( wasAtBottom ? position.minScrollExtent : position.pixels - delta, ); }); } return null; }, [composerSize.value]); final composerNode = useFocusNode( onKeyEvent: (_, event) { if (event is KeyDownEvent && event.logicalKey == .escape) { relatedEvent.value = null; return .handled; } return .ignored; }, ); Future jumpToEvent(int rowId) async { highlightedEvent.value = rowId; await scroll.jumpToEvent(rowId); await Future.delayed(.new(milliseconds: 700), () { if (highlightedEvent.value == rowId) { highlightedEvent.value = null; } }); } useEffect(() { if (jumpRowId == null) return null; void check() { if (!context.mounted) return; if (scroll.scrollController.hasClients) { jumpToEvent(jumpRowId); Future.microtask( () => ref.read(JumpRequestController.provider.notifier).consume(), ); } else { WidgetsBinding.instance.addPostFrameCallback((_) => check()); } } check(); return null; }, [jumpRowId]); IList getEventOptions(Event event) => event.buildEventOptions( context: context, ref: ref, roomId: roomId, userId: userId, onRelation: (rowId, type) { relatedEvent.value = rowId; relationType.value = type; composerNode.requestFocus(); }, ); return Scaffold( endDrawer: PinnedEventsDrawer( roomId, getEventOptions: getEventOptions, jumpToEvent: jumpToEvent, ), body: Builder( builder: (middleContext) => Scaffold( endDrawer: showMembersByDefault ? null : MemberList(roomId), appBar: RoomAppbar( roomId: roomId, isDesktop: isDesktop, onOpenDrawer: Scaffold.of(context).openDrawer, onOpenMemberList: (thisContext) { ref .read(MemberListOpenedController.provider.notifier) .set(!memberListOpened); Scaffold.of(thisContext).openEndDrawer(); }, onOpenPinnedMessagesList: Scaffold.of(middleContext).openEndDrawer, ), body: Row( children: [ Expanded( child: Stack( children: [ Positioned.fill( child: Padding( padding: .symmetric(horizontal: 4), child: switch (controllerData) { AsyncLoading(value: _?) || AsyncData(value: _) => ChatTimeline( scroll: scroll, roomId: roomId, jumpToEvent: jumpToEvent, getEventOptions: getEventOptions, highlightedEvent: highlightedEvent.value, composerHeight: composerSize.value, ), AsyncLoading _ => Loading(), AsyncError(:final error, :final stackTrace) => ErrorDialog(error, stackTrace), }, ), ), Positioned( right: 16, bottom: composerSize.value, child: IgnorePointer( ignoring: scroll.atBottom, child: AnimatedOpacity( opacity: scroll.atBottom ? 0 : 1, duration: const Duration(milliseconds: 200), child: FloatingActionButton.small( onPressed: scroll.jumpToBottom, child: const Icon(Icons.keyboard_arrow_down), ), ), ), ), Positioned( bottom: 0, left: 0, right: 0, child: MeasureSize( onChange: (size) => composerSize.value = size.height, child: Composer( roomId, node: composerNode, onSend: (text, {required shouldMention, required tags}) => notifier .send( text, tags: tags, relationType: relationType.value, shouldMention: shouldMention, relatedEventRowId: relatedEvent.value, ) .onError(showError), relationType: relationType.value, relatedEvent: relatedEvent.value, onDismiss: () { relatedEvent.value = null; composerNode.requestFocus(); }, ), ), ), ], ), ), if (memberListOpened == true && showMembersByDefault) MemberList(roomId), ], ), ), ), ); } }