// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages, unnecessary_import import 'package:didvan/views/ai/ai_state.dart'; import 'package:didvan/views/widgets/hoshan_app_bar.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/ai/ai_chat_args.dart'; import 'package:didvan/models/ai/bots_model.dart'; import 'package:didvan/models/ai/chats_model.dart'; import 'package:didvan/models/ai/files_model.dart'; import 'package:didvan/models/ai/messages_model.dart'; import 'package:didvan/models/ai/ai_model_enum.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/alert_data.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/utils/extension.dart'; import 'package:didvan/views/ai/ai_chat_state.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/ai/widgets/ai_message_bar.dart'; import 'package:didvan/views/ai/widgets/audio_wave.dart'; import 'package:didvan/views/ai/widgets/hoshan_drawer.dart'; import 'package:didvan/views/widgets/didvan/didvan_markdown.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/marquee_text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/video/chat_video_player.dart'; import 'package:didvan/views/widgets/video/custome_controls.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; class AiChatPage extends StatefulWidget { final AiChatArgs args; const AiChatPage({Key? key, required this.args}) : super(key: key); @override _AiChatPageState createState() => _AiChatPageState(); } class _AiChatPageState extends State with TickerProviderStateMixin { final GlobalKey scaffKey = GlobalKey(); FocusNode focusNode = FocusNode(); BotsModel? _searchBot; BotsModel? _geminiBot; BotsModel? _grokBot; bool _isSearchMode = false; AiModel _selectedModel = AiModel.chatGPT; late BotsModel _currentBot; late AnimationController _fadeController; late AnimationController _messageController; late Animation _fadeAnimation; late Animation _slideAnimation; @override void didUpdateWidget(covariant AiChatPage oldWidget) { super.didUpdateWidget(oldWidget); if (widget.args.bot.id != oldWidget.args.bot.id) { debugPrint("--- AI CHAT PAGE: Bot Updated ---"); debugPrint( "Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}"); debugPrint("---------------------------------"); setState(() { _currentBot = widget.args.bot; _isSearchMode = _currentBot.id == 36; }); final state = context.read(); state.clearChat(); if (widget.args.bot.id == 35) { state.fetchSuggestedQuestions(); } if (widget.args.chat != null) { state.chatId = widget.args.chat!.id!; state.chat = widget.args.chat; state.getAllMessages(state.chatId!); } if (widget.args.prompts != null) { state.messages.add(MessageModel( dateTime: DateTime.now() .subtract(const Duration(minutes: 210)) .toIso8601String(), prompts: [widget.args.prompts!])); state.message.clear(); state.update(); state.postMessage(widget.args.bot, widget.args.assistantsName != null); } } } @override void initState() { super.initState(); _fadeController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); _messageController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _fadeAnimation = CurvedAnimation( parent: _fadeController, curve: Curves.easeIn, ); _slideAnimation = Tween( begin: const Offset(0, 0.1), end: Offset.zero, ).animate(CurvedAnimation( parent: _messageController, curve: Curves.easeOutCubic, )); _fadeController.forward(); _messageController.forward(); debugPrint("--- AI CHAT PAGE: Init ---"); debugPrint( "Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}"); debugPrint("----------------------------"); _currentBot = widget.args.bot; _isSearchMode = _currentBot.id == 36; final state = context.read(); if (widget.args.chat != null) { state.chatId = widget.args.chat!.id!; state.chat = widget.args.chat; } WidgetsBinding.instance.addPostFrameCallback((_) async { try { final bots = context.read().bots; if (mounted) { setState(() { _searchBot = bots.firstWhere((b) => b.id == 36); try { _geminiBot = bots.firstWhere((b) => b.id == 35 && (b.name?.toLowerCase().contains('gemini') ?? false)); } catch (e) { _geminiBot = BotsModel( id: 35, name: 'Gemini', responseType: 'text', attachmentType: [ 'audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx' ], attachment: 1, ); } try { _grokBot = bots.firstWhere((b) => b.id == 35 && (b.name?.toLowerCase().contains('grok') ?? false)); } catch (e) { _grokBot = BotsModel( id: 35, name: 'Grok', responseType: 'text', attachmentType: [ 'audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx' ], attachment: 1, ); } }); } } catch (e) { if (mounted) { setState(() { _searchBot = BotsModel( id: 36, name: 'GPT-4o-mini-search', responseType: 'text', attachmentType: [ 'audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx' ], attachment: 1, ); _geminiBot = BotsModel( id: 35, name: 'Gemini', responseType: 'text', attachmentType: [ 'audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx' ], attachment: 1, ); _grokBot = BotsModel( id: 35, name: 'Grok', responseType: 'text', attachmentType: [ 'audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx' ], attachment: 1, ); }); debugPrint( "Search bot (ID 36) not found in History state: $e. Using fallback with audio support."); } } if (widget.args.bot.id == 35) { state.fetchSuggestedQuestions(); } if (state.chatId != null) { state.getAllMessages(state.chatId!).then((value) => Future.delayed( const Duration(milliseconds: 100), () { focusNode.requestFocus(); if (state.messages.isNotEmpty) { state.scrollController.animateTo( state.scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }, )); } else { state.appState = AppState.idle; Future.delayed( const Duration(milliseconds: 100), () => focusNode.requestFocus(), ); } if (widget.args.prompts != null) { state.messages.add(MessageModel( dateTime: DateTime.now() .subtract(const Duration(minutes: 210)) .toIso8601String(), prompts: [widget.args.prompts!])); state.message.clear(); state.update(); await state.postMessage( widget.args.bot, widget.args.assistantsName != null); Future.delayed(const Duration(milliseconds: 100), () { state.scrollController.animateTo( state.scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); }); } }); } @override void dispose() { _fadeController.dispose(); _messageController.dispose(); super.dispose(); } BotsModel _getSelectedBot() { switch (_selectedModel) { case AiModel.gemini: return _geminiBot ?? _currentBot; case AiModel.grok: return _grokBot ?? _currentBot; case AiModel.chatGPT: default: return _currentBot; } } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { if (context.read().refresh) { context.read().getChats(); context.read().refresh = false; } context.read().endChat(); await Future.delayed(Duration.zero); return true; }, child: Consumer( builder: (context, state, child) => Scaffold( appBar: HoshanAppBar( onBack: () async { if (context.read().refresh) { context.read().getChats(); context.read().refresh = false; } context.read().endChat(); await Future.delayed(const Duration(milliseconds: 50)); if (context.mounted) { Navigator.pop(context); } }, withActions: false, ), key: scaffKey, resizeToAvoidBottomInset: true, drawer: HoshanDrawer( scaffKey: scaffKey, ), body: StateHandler( state: state, onRetry: () { if (state.chatId != null) { state.getAllMessages(state.chatId!); } if (widget.args.bot.id == 35) { state.fetchSuggestedQuestions(); } }, builder: (context, state) { final userProvider = context.watch(); return SingleChildScrollView( reverse: true, controller: state.scrollController, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (state.messages.isEmpty) FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 90), userProvider.isLoadingWelcome ? const SizedBox(height: 20) : userProvider.welcomeMessage != null ? TweenAnimationBuilder( duration: const Duration(milliseconds: 800), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOut, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.scale( scale: 0.8 + (0.2 * value), child: child, ), ); }, child: Padding( padding: const EdgeInsets.only( bottom: 12.0, left: 24, right: 24), child: Center( child: DidvanText( userProvider.welcomeMessage!, textAlign: TextAlign.center, fontSize: 18, fontWeight: FontWeight.bold, color: const Color.fromARGB( 255, 0, 126, 167), ), ), ), ) : const SizedBox(height: 20), TweenAnimationBuilder( duration: const Duration(milliseconds: 1000), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOut, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, 20 * (1 - value)), child: child, ), ); }, child: const Text( 'چطور می‌تونم کمکت کنم؟', style: TextStyle( color: Color.fromARGB(255, 0, 53, 70), fontSize: 16, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 70), if (widget.args.bot.id == 35) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric( horizontal: 24.0), child: Row( children: [ const SizedBox(width: 7), SvgPicture.asset( 'lib/assets/icons/message-question.svg', height: 25, color: const Color.fromARGB( 255, 0, 126, 167), ), const SizedBox(width: 7), const DidvanText( 'سوالات پیشنهادی:', fontSize: 16, fontWeight: FontWeight.normal, color: Color.fromARGB( 255, 0, 126, 167), ), ], ), ), const SizedBox(height: 12), state.isLoadingQuestions ? Center( child: SpinKitThreeBounce( color: Theme.of(context) .colorScheme .primary, size: 18, ), ) : state.suggestedQuestions.isEmpty ? Padding( padding: const EdgeInsets.symmetric( horizontal: 24.0), child: Center( child: DidvanText( 'سوالی برای پیشنهاد وجود ندارد.', fontSize: 12, color: Theme.of(context) .colorScheme .caption, ), ), ) : ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: state .suggestedQuestions.length, itemBuilder: (context, index) { final question = state.suggestedQuestions[ index]; return TweenAnimationBuilder< double>( duration: Duration( milliseconds: 400 + (index * 100)), tween: Tween( begin: 0.0, end: 1.0), curve: Curves.easeOut, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset( 30 * (1 - value), 0), child: child, ), ); }, child: InkWell( onTap: () { if (state.messages .isNotEmpty && DateTime.parse(state .messages .last .dateTime) .toPersianDateStr() .contains(DateTime.parse(DateTime .now() .subtract(const Duration( minutes: 210)) .toIso8601String()) .toPersianDateStr())) { state.messages.last .prompts .add(Prompts( error: false, text: question, fileLocal: null, finished: true, role: 'user', createdAt: DateTime .now() .subtract( const Duration( minutes: 210)) .toIso8601String(), )); } else { state.messages.add(MessageModel( dateTime: DateTime .now() .subtract( const Duration( minutes: 210)) .toIso8601String(), prompts: [ Prompts( error: false, text: question, finished: true, fileLocal: null, role: 'user', createdAt: DateTime .now() .subtract(const Duration( minutes: 210)) .toIso8601String(), ) ])); } state.message.clear(); state.update(); state.postMessage( _currentBot, widget.args .assistantsName != null); Future.delayed( const Duration( milliseconds: 100), () { state.scrollController .animateTo( state .scrollController .position .maxScrollExtent, duration: const Duration( milliseconds: 300), curve: Curves.easeOut, ); }); }, child: Column( children: [ Container( margin: const EdgeInsets .symmetric( horizontal: 24, vertical: 4), child: DidvanText( question, fontSize: 12, color: const Color .fromARGB( 255, 102, 102, 102), ), ), const Padding( padding: EdgeInsets .fromLTRB(110, 5, 20, 5), child: Divider( height: 1, color: Color .fromARGB( 255, 210, 210, 210), ), ), ], ), ), ); }, ), const SizedBox(height: 10), ], ) else const SizedBox(height: 20), ], ), ), ), if (state.messages.isNotEmpty) ListView.builder( itemCount: state.messages.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, mIndex) { final prompts = state.messages[mIndex].prompts; final time = state.messages[mIndex].dateTime; return Column( children: [ timeLabel(context, time), ListView.builder( itemCount: prompts.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final message = prompts[index]; return messageBubble( message, context, state, index, mIndex); }, ), ], ); }, ), ], ), ); }, ), bottomNavigationBar: Padding( padding: EdgeInsets.only( bottom: _currentBot.id == 35 ? MediaQuery.of(context).viewInsets.bottom : (MediaQuery.of(context).viewInsets.bottom - 72.0) .clamp(0.0, double.infinity), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ AiMessageBar( bot: _isSearchMode && _searchBot != null ? _searchBot! : _getSelectedBot(), attch: widget.args.attach, assistantsName: widget.args.assistantsName, showSearchToggle: _currentBot.id == 35 && _searchBot != null, isSearchMode: _isSearchMode, onSearchModeToggled: (bool isSearchOn) { if (_searchBot == null) return; setState(() { _isSearchMode = isSearchOn; }); }, showModelSelector: _currentBot.id == 35 && _geminiBot != null && _grokBot != null, selectedModel: _selectedModel, onModelChanged: (AiModel model) { setState(() { _selectedModel = model; }); }, ), ], ), ), ), ), ); } Center timeLabel(BuildContext context, String time) { return Center( child: Container( margin: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.splash, borderRadius: DesignConfig.lowBorderRadius, ), child: DidvanText( DateTime.parse(time).toPersianDateStr(), style: Theme.of(context).textTheme.labelSmall, color: DesignConfig.isDark ? Theme.of(context).colorScheme.white : Theme.of(context).colorScheme.black, ), ), ); } Widget messageBubble(Prompts message, BuildContext context, AiChatState state, int index, int mIndex) { FilesModel? file = message.fileLocal ?? (message.file == null ? null : FilesModel(message.file.toString().replaceAll(' ', ''), duration: message.duration != null ? Duration(seconds: message.duration!) : null, audio: message.audio, video: message.video)); final isUser = message.role.toString().contains('user'); return TweenAnimationBuilder( duration: Duration(milliseconds: 300 + (index * 50)), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOut, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(isUser ? -20 * (1 - value) : 20 * (1 - value), 0), child: child, ), ); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: message.role.toString().contains('user') ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ Row( mainAxisAlignment: message.role.toString().contains('user') ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ Container( constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width / 1.3), decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius.copyWith( bottomLeft: !message.role.toString().contains('user') ? Radius.zero : null, bottomRight: message.role.toString().contains('user') ? Radius.zero : null, ), color: message.error != null && message.error! ? Theme.of(context).colorScheme.error.withOpacity(0.4) : (message.role.toString().contains('user') ? Theme.of(context).colorScheme.surface : Theme.of(context).colorScheme.focused) .withOpacity(0.9), border: Border.all( color: Theme.of(context).colorScheme.border, width: 0.5, ), ), child: Container( child: message.finished != null && !message.finished! ? Column( children: [ ValueListenableBuilder>( valueListenable: state.messageOnstream, builder: (context, value, child) { return StreamBuilder( stream: value, builder: (context, snapshot) { if (!snapshot.hasData) { return const SizedBox(); } return Padding( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16), child: Directionality( textDirection: snapshot.data .toString() .startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: DidvanMarkdownText( text: "${snapshot.data}...", ), ), ); }, ); }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: SpinKitThreeBounce( color: Theme.of(context).colorScheme.primary, size: 18, ), ), ], ) : Column( children: [ if (file != null) (file.isAudio()) ? Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), child: AudioWave( file: file.path, totalDuration: file.duration, ), ) : file.isVideo() ? Padding( padding: const EdgeInsets.fromLTRB( 16, 16, 16, 0), child: ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: ChatVideoPlayer( src: RequestHelper.baseUrl + file.path, custome: const CustomControls(), ), ), ) : file.isImage() ? Padding( padding: const EdgeInsets.all(8.0), child: messageImage(file), ) : Padding( padding: const EdgeInsets.all(8.0), child: messageFile( context, message, state), ), if (message.text != null && message.text!.isNotEmpty && ((message.audio == null || (message.audio != null && !message.audio!)))) Padding( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16), child: Directionality( textDirection: message.text .toString() .startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: DidvanMarkdownText( text: message.text.toString(), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (message.role .toString() .contains('user') && widget.args.assistantsName == null) PopupMenuButton( offset: const Offset(0, 46), onSelected: (value) async { navigatorKey.currentState!.pushNamed( Routes.aiChat, arguments: AiChatArgs( bot: value, prompts: message, isTool: widget.args.isTool ?? context .read< HistoryAiChatState>() .bots, ), ); }, itemBuilder: (BuildContext context) { final bots = widget.args.isTool ?? context .read() .bots; return [ ...List.generate( bots.length, (index) => PopupMenuItem( value: bots[index], height: 72, child: Container( constraints: const BoxConstraints( maxWidth: 200), child: Row( children: [ SkeletonImage( imageUrl: bots[index] .image .toString(), width: 42, height: 42, borderRadius: BorderRadius .circular(360), ), const SizedBox(width: 12), Expanded( child: Directionality( textDirection: TextDirection.ltr, child: DidvanText( bots[index] .name .toString(), maxLines: 1, overflow: TextOverflow .ellipsis, ), ), ), ], ), ), ), ), ]; }, child: const SizedBox(), ), if (message.role .toString() .contains('user') && index == state.messages[mIndex].prompts .length - 2 && (_currentBot.editable != null && _currentBot.editable!)) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { state.isEdite = true; state.message.text = message.text.toString(); state.update(); }, child: Icon( Icons.edit_outlined, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (message.file != null && kIsWeb) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { final url = '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; MediaService.downloadFileFromWeb( url); }, child: Icon( DidvanIcons.download_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (message.file != null && !kIsWeb) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { debugPrint( "Download button tapped on iOS"); final url = '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; await MediaService.downloadFile(url, name: message.fileName); }, child: Icon( DidvanIcons.download_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (message.error != null && message.error!) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { state.messages.last.prompts .remove(message); state.messages.last.prompts.add( message.copyWith(error: false)); state.file = file; state.update(); final botToUse = _isSearchMode && _searchBot != null ? _searchBot! : _getSelectedBot(); await state.postMessage( botToUse, widget.args.assistantsName != null); Future.delayed( const Duration( milliseconds: 100), () { state.scrollController.animateTo( state.scrollController.position .maxScrollExtent, duration: const Duration( milliseconds: 300), curve: Curves.easeOut, ); }); }, child: Icon( DidvanIcons.refresh_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (message.text != null && message.text!.isNotEmpty && (file == null || !file.isImage())) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { await Clipboard.setData( ClipboardData( text: state.messages[mIndex] .prompts[index].text .toString())); Future.delayed( Duration.zero, () => ActionSheetUtils(context) .showAlert( AlertData( message: "متن با موفقیت کپی شد", aLertType: ALertType.success, ), ), ); }, child: Icon( DidvanIcons.copy_regular, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { if (message.id != null) { state.deleteMessage( message.id!, mIndex, index); } else { state.messages[mIndex].prompts .removeAt(index); state.update(); } }, child: Icon( DidvanIcons.trash_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), ], ), ), const SizedBox(height: 8), ], ), ), ), ], ), const SizedBox(height: 4), Row( mainAxisSize: MainAxisSize.min, children: [ DidvanText( DateTimeUtils.timeWithAmPm(message.createdAt.toString()), style: Theme.of(context).textTheme.labelSmall, color: Theme.of(context).colorScheme.caption, ), ], ), ], ), ), ); } Container messageFile( BuildContext context, Prompts message, AiChatState state) { final String fileName = message.fileName ?? message.fileLocal?.name ?? ''; return Container( decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius, color: Theme.of(context).colorScheme.border, ), padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), margin: const EdgeInsets.fromLTRB(8, 8, 8, 0), child: Row( children: [ const Icon(Icons.file_copy), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: MediaQuery.sizeOf(context).width, child: MarqueeText( text: fileName, style: const TextStyle(fontSize: 14), stop: const Duration(seconds: 3), textDirection: fileName.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, ), ), ], ), ), ], ), ); } Widget messageImage(FilesModel file) { return GestureDetector( onTap: () => ActionSheetUtils(context) .openInteractiveViewer(context, file.path, !file.isNetwork()), child: file.isNetwork() ? file.path.startsWith('blob:') ? ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: Image.network(file.path), ) : SkeletonImage( pWidth: MediaQuery.sizeOf(context).width / 1, pHeight: MediaQuery.sizeOf(context).height / 6, imageUrl: file.path, ) : ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: Image.file(file.main), ), ); } }