import 'dart:convert'; import 'dart:math'; import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; import '../../conversation/conversation.dart'; import '../../theme/theme.dart'; import '../chat.dart'; const onEndReachedThreshold = 0.75; class ChatComponentWidget extends StatelessWidget { const ChatComponentWidget._({required super.key}); // Builder wrapper function that takes care of state management requirements static Widget builder( {required TypedKey localConversationRecordKey, Key? key}) => Builder(builder: (context) { // Get the account info final accountInfo = context.watch().state; // Get the account record cubit final accountRecordCubit = context.read(); // Get the contact list cubit final contactListCubit = context.watch(); // Get the active conversation cubit final activeConversationCubit = context .select( (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (activeConversationCubit == null) { return waitingPage(); } // Get the messages cubit final messagesCubit = context.select< ActiveSingleContactChatBlocMapCubit, SingleContactMessagesCubit?>( (x) => x.tryOperateSync(localConversationRecordKey, closure: (cubit) => cubit)); if (messagesCubit == null) { return waitingPage(); } // Make chat component state return BlocProvider( key: key, create: (context) => ChatComponentCubit.singleContact( accountInfo: accountInfo, accountRecordCubit: accountRecordCubit, contactListCubit: contactListCubit, activeConversationCubit: activeConversationCubit, messagesCubit: messagesCubit, ), child: ChatComponentWidget._(key: key)); }); ///////////////////////////////////////////////////////////////////// void _handleSendPressed( ChatComponentCubit chatComponentCubit, types.PartialText message) { final text = message.text; if (text.startsWith('/')) { chatComponentCubit.runCommand(text); return; } chatComponentCubit.sendMessage(message); } // void _handleAttachmentPressed() async { // // // } Future _handlePageForward( ChatComponentCubit chatComponentCubit, WindowState messageWindow, ScrollNotification notification) async { print( '_handlePageForward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); // Go forward a page final tail = min(messageWindow.length, messageWindow.windowTail + (messageWindow.windowCount ~/ 4)) % messageWindow.length; // Set follow final follow = messageWindow.length == 0 || tail == 0; // xxx incorporate scroll position // final scrollOffset = (notification.metrics.maxScrollExtent - // notification.metrics.minScrollExtent) * // (1.0 - onEndReachedThreshold); // chatComponentCubit.scrollOffset = scrollOffset; await chatComponentCubit.setWindow( tail: tail, count: messageWindow.windowCount, follow: follow); // chatComponentCubit.state.scrollController.position.jumpTo( // chatComponentCubit.state.scrollController.offset + scrollOffset); //chatComponentCubit.scrollOffset = 0; } Future _handlePageBackward( ChatComponentCubit chatComponentCubit, WindowState messageWindow, ScrollNotification notification, ) async { print( '_handlePageBackward: messagesState.length=${messageWindow.length} messagesState.windowTail=${messageWindow.windowTail} messagesState.windowCount=${messageWindow.windowCount} ScrollNotification=$notification'); // Go back a page final tail = max( messageWindow.windowCount, (messageWindow.windowTail - (messageWindow.windowCount ~/ 4)) % messageWindow.length); // Set follow final follow = messageWindow.length == 0 || tail == 0; // xxx incorporate scroll position // final scrollOffset = -(notification.metrics.maxScrollExtent - // notification.metrics.minScrollExtent) * // (1.0 - onEndReachedThreshold); // chatComponentCubit.scrollOffset = scrollOffset; await chatComponentCubit.setWindow( tail: tail, count: messageWindow.windowCount, follow: follow); // chatComponentCubit.scrollOffset = scrollOffset; // chatComponentCubit.state.scrollController.position.jumpTo( // chatComponentCubit.state.scrollController.offset + scrollOffset); //chatComponentCubit.scrollOffset = 0; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; final chatTheme = makeChatTheme(scale, scaleConfig, textTheme); final errorChatTheme = (ChatThemeEditor(chatTheme) ..inputTextColor = scale.errorScale.primary ..sendButtonIcon = Image.asset( 'assets/icon-send.png', color: scale.errorScale.primary, package: 'flutter_chat_ui', )) .commit(); // Get the enclosing chat component cubit that contains our state // (created by ChatComponentWidget.builder()) final chatComponentCubit = context.watch(); final chatComponentState = chatComponentCubit.state; final localUser = chatComponentState.localUser; if (localUser == null) { return waitingPage(); } final messageWindow = chatComponentState.messageWindow.asData?.value; if (messageWindow == null) { return chatComponentState.messageWindow.buildNotData(); } final isLastPage = messageWindow.windowStart == 0; final isFirstPage = messageWindow.windowEnd == messageWindow.length - 1; final title = chatComponentState.title; if (chatComponentCubit.scrollOffset != 0) { chatComponentState.scrollController.position.correctPixels( chatComponentState.scrollController.position.pixels + chatComponentCubit.scrollOffset); chatComponentCubit.scrollOffset = 0; } return Column( children: [ Container( height: 48, decoration: BoxDecoration( color: scale.primaryScale.subtleBorder, ), child: Row(children: [ Align( alignment: AlignmentDirectional.centerStart, child: Padding( padding: const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), child: Text(title, textAlign: TextAlign.start, style: textTheme.titleMedium! .copyWith(color: scale.primaryScale.borderText)), )), const Spacer(), IconButton( icon: Icon(Icons.close, color: scale.primaryScale.borderText), onPressed: () async { context.read().setActiveChat(null); }).paddingLTRB(16, 0, 16, 0) ]), ), DecoratedBox( decoration: BoxDecoration(color: scale.primaryScale.subtleBackground), child: NotificationListener( onNotification: (notification) { if (chatComponentCubit.scrollOffset != 0) { return false; } if (!isFirstPage && notification.metrics.pixels <= ((notification.metrics.maxScrollExtent - notification.metrics.minScrollExtent) * (1.0 - onEndReachedThreshold) + notification.metrics.minScrollExtent)) { // final scrollOffset = (notification.metrics.maxScrollExtent - notification.metrics.minScrollExtent) * (1.0 - onEndReachedThreshold); chatComponentCubit.scrollOffset = scrollOffset; // singleFuture(chatComponentState.chatKey, () async { await _handlePageForward( chatComponentCubit, messageWindow, notification); }); } else if (!isLastPage && notification.metrics.pixels >= ((notification.metrics.maxScrollExtent - notification.metrics.minScrollExtent) * onEndReachedThreshold + notification.metrics.minScrollExtent)) { // final scrollOffset = -(notification.metrics.maxScrollExtent - notification.metrics.minScrollExtent) * (1.0 - onEndReachedThreshold); chatComponentCubit.scrollOffset = scrollOffset; // singleFuture(chatComponentState.chatKey, () async { await _handlePageBackward( chatComponentCubit, messageWindow, notification); }); } return false; }, child: ValueListenableBuilder( valueListenable: chatComponentState.textEditingController, builder: (context, textEditingValue, __) { final messageIsValid = utf8.encode(textEditingValue.text).lengthInBytes < 2048; return Chat( key: chatComponentState.chatKey, theme: messageIsValid ? chatTheme : errorChatTheme, messages: messageWindow.window.toList(), scrollToBottomOnSend: isFirstPage, scrollController: chatComponentState.scrollController, inputOptions: InputOptions( inputClearMode: messageIsValid ? InputClearMode.always : InputClearMode.never, textEditingController: chatComponentState.textEditingController), // isLastPage: isLastPage, // onEndReached: () async { // await _handlePageBackward( // chatComponentCubit, messageWindow); // }, //onEndReachedThreshold: onEndReachedThreshold, //onAttachmentPressed: _handleAttachmentPressed, //onMessageTap: _handleMessageTap, //onPreviewDataFetched: _handlePreviewDataFetched, onSendPressed: (pt) { try { if (!messageIsValid) { showErrorToast(context, translate('chat.message_too_long')); return; } _handleSendPressed(chatComponentCubit, pt); } on FormatException { showErrorToast(context, translate('chat.message_too_long')); } }, listBottomWidget: messageIsValid ? null : Text(translate('chat.message_too_long'), style: TextStyle( color: scale.errorScale.primary)) .toCenter(), //showUserAvatars: false, //showUserNames: true, user: localUser, emptyState: const EmptyChatWidget()) .paddingLTRB(0, 2, 0, 0); }))).expanded(), ], ); } }