import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import 'dart:ui' as ui; import '../../../core/models/conversation.dart'; import '../../../core/providers/app_providers.dart'; import '../../../shared/theme/app_theme.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/utils/ui_utils.dart'; import '../../../shared/widgets/conduit_components.dart'; import '../providers/chat_providers.dart'; // Optimized delete conversation provider with error handling final deleteConversationProvider = FutureProvider.family(( ref, conversationId, ) async { final api = ref.read(apiServiceProvider); if (api == null) throw Exception('No API service available'); await api.deleteConversation(conversationId); ref.invalidate(conversationsProvider); }); /// Optimized conversation tile with Conduit design aesthetics class ModernConversationTile extends ConsumerStatefulWidget { final Conversation conversation; final bool isActive; final Future Function() onTap; final VoidCallback onDelete; const ModernConversationTile({ super.key, required this.conversation, required this.isActive, required this.onTap, required this.onDelete, }); @override ConsumerState createState() => _ModernConversationTileState(); } class _ModernConversationTileState extends ConsumerState with SingleTickerProviderStateMixin { bool _isLoading = false; late AnimationController _animationController; late Animation _scaleAnimation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(milliseconds: 150), vsync: this, ); _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( margin: const EdgeInsets.symmetric( horizontal: Spacing.md, vertical: Spacing.xs, ), child: Dismissible( key: Key(widget.conversation.id), direction: DismissDirection.horizontal, background: _buildSwipeBackground(DismissDirection.startToEnd), secondaryBackground: _buildSwipeBackground( DismissDirection.endToStart, ), confirmDismiss: _handleDismiss, child: _buildTileContent(), ), ), ); }, ); } Widget _buildSwipeBackground(DismissDirection direction) { final isArchive = direction == DismissDirection.startToEnd; return Container( decoration: BoxDecoration( gradient: LinearGradient( colors: isArchive ? [ AppTheme.brandPrimary.withValues(alpha: 0.1), AppTheme.brandPrimary.withValues(alpha: 0.2), ] : [ AppTheme.error.withValues(alpha: 0.1), AppTheme.error.withValues(alpha: 0.2), ], ), borderRadius: BorderRadius.circular(AppBorderRadius.lg), ), alignment: isArchive ? Alignment.centerLeft : Alignment.centerRight, padding: EdgeInsets.symmetric(horizontal: Spacing.lg), child: Container( width: Spacing.xxl, height: Spacing.xxl, decoration: BoxDecoration( color: isArchive ? AppTheme.brandPrimary : AppTheme.error, borderRadius: BorderRadius.circular(AppBorderRadius.md), boxShadow: ConduitShadows.low, ), child: Icon( isArchive ? (Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive) : (Platform.isIOS ? CupertinoIcons.delete : Icons.delete), color: AppTheme.neutral50, size: AppTypography.headlineMedium, ), ), ); } Future _handleDismiss(DismissDirection direction) async { if (direction == DismissDirection.startToEnd) { await _handleArchive(); } else { widget.onDelete(); } return false; } Widget _buildTileContent() { return GestureDetector( onTapDown: (_) => _animationController.forward(), onTapUp: (_) => _animationController.reverse(), onTapCancel: () => _animationController.reverse(), onTap: _isLoading ? null : _handleTap, child: Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( gradient: widget.isActive ? LinearGradient( colors: [ AppTheme.brandPrimary.withValues(alpha: 0.15), AppTheme.brandPrimary.withValues(alpha: 0.08), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : LinearGradient( colors: [ AppTheme.neutral700.withValues(alpha: 0.6), AppTheme.neutral700.withValues(alpha: 0.3), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppBorderRadius.lg), border: Border.all( color: widget.isActive ? AppTheme.brandPrimary.withValues(alpha: 0.3) : AppTheme.neutral600.withValues(alpha: 0.2), width: widget.isActive ? BorderWidth.medium : BorderWidth.thin, ), boxShadow: widget.isActive ? ConduitShadows.low : null, ), child: Row( children: [ _buildLeadingIcon(), const SizedBox(width: Spacing.md), Expanded(child: _buildContent()), _buildTrailingActions(), ], ), ), ); } Widget _buildLeadingIcon() { if (_isLoading) { return SizedBox( width: Spacing.xl, height: Spacing.xl, child: CircularProgressIndicator.adaptive( strokeWidth: BorderWidth.thick, valueColor: AlwaysStoppedAnimation( widget.isActive ? AppTheme.brandPrimary : AppTheme.neutral300, ), ), ); } return Container( width: Spacing.xl, height: Spacing.xl, decoration: BoxDecoration( gradient: widget.isActive ? LinearGradient( colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : LinearGradient( colors: [ AppTheme.neutral600.withValues(alpha: 0.8), AppTheme.neutral500.withValues(alpha: 0.6), ], ), borderRadius: BorderRadius.circular(AppBorderRadius.sm), ), child: Stack( alignment: Alignment.center, children: [ Icon( Platform.isIOS ? CupertinoIcons.chat_bubble_2_fill : Icons.chat_rounded, color: AppTheme.neutral50, size: Spacing.md, ), if (widget.conversation.pinned) Positioned( top: Spacing.xxs, right: Spacing.xxs, child: Container( width: Spacing.sm, height: Spacing.sm, decoration: const BoxDecoration( color: AppTheme.warning, shape: BoxShape.circle, ), ), ), ], ), ); } Widget _buildContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.conversation.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: widget.isActive ? AppTheme.neutral50 : AppTheme.neutral100, fontWeight: FontWeight.w600, fontSize: AppTypography.bodyLarge, letterSpacing: -0.2, ), ), const SizedBox(height: Spacing.xs), Row( children: [ Icon( Platform.isIOS ? CupertinoIcons.time : Icons.access_time_rounded, size: AppTypography.labelMedium, color: AppTheme.neutral400, ), const SizedBox(width: Spacing.xs), Text( _formatDate(widget.conversation.updatedAt), style: const TextStyle( color: AppTheme.neutral400, fontSize: AppTypography.labelMedium, fontWeight: FontWeight.w500, ), ), if (widget.conversation.messages.isNotEmpty) ...[ Container( margin: const EdgeInsets.symmetric(horizontal: Spacing.sm), width: Spacing.xxs, height: Spacing.xxs, decoration: const BoxDecoration( color: AppTheme.neutral400, shape: BoxShape.circle, ), ), Text( '${widget.conversation.messages.length} messages', style: const TextStyle( color: AppTheme.neutral400, fontSize: AppTypography.labelMedium, fontWeight: FontWeight.w500, ), ), ], ], ), if (widget.conversation.tags.isNotEmpty) ...[ const SizedBox(height: Spacing.sm), _buildTags(), ], ], ); } Widget _buildTags() { return Wrap( spacing: Spacing.xs, runSpacing: Spacing.xs, children: widget.conversation.tags.take(3).map((tag) { return Container( padding: const EdgeInsets.symmetric( horizontal: Spacing.xs + Spacing.xxs, vertical: Spacing.xxs, ), decoration: BoxDecoration( color: AppTheme.brandPrimary.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(AppBorderRadius.xs), border: Border.all( color: AppTheme.brandPrimary.withValues(alpha: 0.2), width: BorderWidth.thin, ), ), child: Text( tag, style: const TextStyle( color: AppTheme.brandPrimary, fontSize: AppTypography.labelSmall, fontWeight: FontWeight.w600, ), ), ); }).toList(), ); } Widget _buildTrailingActions() { return PopupMenuButton( icon: Container( width: Spacing.xl, height: Spacing.xl, decoration: BoxDecoration( color: AppTheme.neutral700.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(AppBorderRadius.sm), ), child: Icon( Platform.isIOS ? CupertinoIcons.ellipsis : Icons.more_vert_rounded, color: AppTheme.neutral300, size: Spacing.md, ), ), color: AppTheme.neutral800, elevation: Elevation.high + Spacing.xs, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.md), side: BorderSide( color: AppTheme.neutral600.withValues(alpha: 0.3), width: BorderWidth.thin, ), ), onSelected: _handleMenuAction, itemBuilder: (context) => _buildMenuItems(), ); } List> _buildMenuItems() { return [ _buildMenuItem( 'pin', widget.conversation.pinned ? (Platform.isIOS ? CupertinoIcons.pin_slash : Icons.push_pin_outlined) : (Platform.isIOS ? CupertinoIcons.pin_fill : Icons.push_pin_rounded), widget.conversation.pinned ? 'Unpin' : 'Pin', ), _buildMenuItem( 'archive', Platform.isIOS ? CupertinoIcons.archivebox : Icons.archive_rounded, 'Archive', ), _buildMenuItem( 'share', Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded, 'Share', ), _buildMenuItem( 'clone', Platform.isIOS ? CupertinoIcons.doc_on_doc : Icons.content_copy_rounded, 'Clone', ), PopupMenuItem( enabled: false, child: Divider(color: AppTheme.neutral600, height: BorderWidth.regular), ), _buildMenuItem( 'delete', Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded, 'Delete', isDestructive: true, ), ]; } PopupMenuItem _buildMenuItem( String value, IconData icon, String label, { bool isDestructive = false, }) { return PopupMenuItem( value: value, child: Row( children: [ Container( width: Spacing.lg + Spacing.xs, height: Spacing.lg + Spacing.xs, decoration: BoxDecoration( color: isDestructive ? AppTheme.error.withValues(alpha: 0.1) : AppTheme.neutral700.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(AppBorderRadius.xs), ), child: Icon( icon, size: Spacing.md, color: isDestructive ? AppTheme.error : AppTheme.neutral200, ), ), const SizedBox(width: Spacing.sm), Text( label, style: TextStyle( color: isDestructive ? AppTheme.error : AppTheme.neutral50, fontWeight: FontWeight.w500, ), ), ], ), ); } Future _handleTap() async { setState(() => _isLoading = true); try { await widget.onTap(); } finally { if (mounted) { setState(() => _isLoading = false); } } } Future _handleMenuAction(String action) async { switch (action) { case 'pin': await _handlePin(); break; case 'archive': await _handleArchive(); break; case 'share': await _handleShare(); break; case 'clone': await _handleClone(); break; case 'delete': widget.onDelete(); break; } } Future _handlePin() async { try { await pinConversation( ref, widget.conversation.id, !widget.conversation.pinned, ); if (mounted) { UiUtils.showMessage( context, widget.conversation.pinned ? 'Conversation unpinned' : 'Conversation pinned', ); } } catch (e) { if (mounted) { UiUtils.showMessage( context, 'Failed to ${widget.conversation.pinned ? 'unpin' : 'pin'} conversation', ); } } } Future _handleArchive() async { try { await archiveConversation(ref, widget.conversation.id, true); if (mounted) { UiUtils.showMessage(context, 'Conversation archived'); } } catch (e) { if (mounted) { UiUtils.showMessage(context, 'Failed to archive conversation'); } } } Future _handleShare() async { try { final shareId = await shareConversation(ref, widget.conversation.id); if (mounted && shareId != null) { _showShareDialog(shareId); } } catch (e) { if (mounted) { UiUtils.showMessage(context, 'Failed to share conversation'); } } } Future _handleClone() async { try { await cloneConversation(ref, widget.conversation.id); if (mounted) { Navigator.pop(context); UiUtils.showMessage(context, 'Conversation cloned'); } } catch (e) { if (mounted) { UiUtils.showMessage(context, 'Failed to clone conversation'); } } } void _showShareDialog(String shareId) { final shareUrl = '${ref.read(apiServiceProvider)?.serverConfig.url}/s/$shareId'; showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: AppTheme.neutral800, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppBorderRadius.lg), side: BorderSide( color: AppTheme.neutral600.withValues(alpha: 0.3), width: BorderWidth.thin, ), ), title: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( gradient: LinearGradient( colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight], ), borderRadius: BorderRadius.circular(AppBorderRadius.sm), ), child: const Icon( Icons.share_rounded, color: AppTheme.neutral50, size: Spacing.md, ), ), const SizedBox(width: Spacing.sm), const Text( 'Share Conversation', style: TextStyle( color: AppTheme.neutral50, fontWeight: FontWeight.w600, ), ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Anyone with this link can view the conversation:', style: TextStyle(color: AppTheme.neutral300), ), const SizedBox(height: Spacing.md), Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( color: AppTheme.neutral700.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(AppBorderRadius.md), border: Border.all( color: AppTheme.neutral600.withValues(alpha: 0.3), width: BorderWidth.thin, ), ), child: SelectableText( shareUrl, style: const TextStyle( fontFamily: 'monospace', color: AppTheme.neutral50, fontSize: AppTypography.labelMedium, ), ), ), ], ), actions: [ ConduitButton( text: 'Close', isSecondary: true, onPressed: () => Navigator.pop(context), ), ConduitButton( text: 'Copy Link', onPressed: () async { await Clipboard.setData(ClipboardData(text: shareUrl)); if (context.mounted) { UiUtils.showMessage(context, 'Link copied to clipboard'); Navigator.pop(context); } }, ), ], ), ); } String _formatDate(DateTime date) { final now = DateTime.now(); // Convert to local timezone if needed final localDate = date.toLocal(); final localNow = now.toLocal(); final difference = localNow.difference(localDate); // Handle negative differences (future dates) if (difference.isNegative) { return 'Just now'; } if (difference.inDays == 0) { if (difference.inHours == 0) { if (difference.inMinutes <= 1) { return 'Just now'; } return '${difference.inMinutes}m'; } return '${difference.inHours}h'; } else if (difference.inDays == 1) { return 'Yesterday'; } else if (difference.inDays < 7) { return '${difference.inDays}d'; } else if (difference.inDays < 365) { return '${localDate.month}/${localDate.day}'; } else { return '${localDate.month}/${localDate.day}/${localDate.year}'; } } } /// Optimized archived chats view with improved performance class ModernArchivedChatsView extends ConsumerWidget { final ScrollController scrollController; const ModernArchivedChatsView({super.key, required this.scrollController}); @override Widget build(BuildContext context, WidgetRef ref) { final archivedConversations = ref.watch(archivedConversationsProvider); return Container( decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppTheme.neutral800, AppTheme.neutral900], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.only( topLeft: ui.Radius.circular(AppBorderRadius.lg), topRight: ui.Radius.circular(AppBorderRadius.lg), ), border: Border.all( color: AppTheme.neutral600.withValues(alpha: 0.2), width: BorderWidth.thin, ), ), child: Column( children: [ _buildHandle(), _buildHeader(context), const Divider(color: AppTheme.neutral600, height: 1, thickness: 0.5), Expanded(child: _buildContent(context, archivedConversations, ref)), ], ), ); } Widget _buildHandle() { return Container( margin: const EdgeInsets.symmetric(vertical: Spacing.sm), width: Spacing.xxl, height: Spacing.xs, decoration: BoxDecoration( color: AppTheme.neutral500, borderRadius: BorderRadius.circular(AppBorderRadius.xs), ), ); } Widget _buildHeader(BuildContext context) { return Padding( padding: const EdgeInsets.all(Spacing.lg), child: Row( children: [ Container( width: Spacing.xxl, height: Spacing.xxl, decoration: BoxDecoration( gradient: LinearGradient( colors: [AppTheme.brandPrimary, AppTheme.brandPrimaryLight], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppBorderRadius.md), ), child: const Icon( Icons.archive_rounded, color: AppTheme.neutral50, size: AppTypography.headlineMedium, ), ), const SizedBox(width: Spacing.md), const Expanded( child: Text( 'Archived Conversations', style: TextStyle( color: AppTheme.neutral50, fontSize: AppTypography.headlineSmall, fontWeight: FontWeight.w600, letterSpacing: -0.3, ), ), ), ConduitIconButton( icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close_rounded, onPressed: () => Navigator.pop(context), ), ], ), ); } Widget _buildContent( BuildContext context, List conversations, WidgetRef ref, ) { if (conversations.isEmpty) { return _buildEmptyState(); } return ListView.builder( controller: scrollController, padding: const EdgeInsets.all(Spacing.md), itemCount: conversations.length, itemBuilder: (context, index) { final conversation = conversations[index]; return ModernArchivedConversationTile( conversation: conversation, onUnarchive: () => _handleUnarchive(ref, context, conversation.id), ); }, ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: Spacing.xxl + Spacing.xl, height: Spacing.xxl + Spacing.xl, decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppTheme.neutral600.withValues(alpha: 0.3), AppTheme.neutral700.withValues(alpha: 0.1), ], ), borderRadius: BorderRadius.circular(AppBorderRadius.round), ), child: const Icon( Icons.archive_rounded, size: Spacing.xxl, color: AppTheme.neutral400, ), ), const SizedBox(height: Spacing.lg), const Text( 'Nothing archived yet', style: TextStyle( color: AppTheme.neutral50, fontSize: AppTypography.headlineSmall, fontWeight: FontWeight.w600, ), ), const SizedBox(height: Spacing.sm), const Text( 'Conversations you archive will appear here', style: TextStyle( color: AppTheme.neutral400, fontSize: AppTypography.labelLarge, ), textAlign: TextAlign.center, ), ], ), ); } Future _handleUnarchive( WidgetRef ref, BuildContext context, String conversationId, ) async { try { await archiveConversation(ref, conversationId, false); if (context.mounted) { UiUtils.showMessage(context, 'Conversation unarchived'); } } catch (e) { if (context.mounted) { UiUtils.showMessage(context, 'Failed to unarchive conversation'); } } } } /// Optimized archived conversation tile class ModernArchivedConversationTile extends StatelessWidget { final Conversation conversation; final VoidCallback onUnarchive; const ModernArchivedConversationTile({ super.key, required this.conversation, required this.onUnarchive, }); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: Spacing.sm), child: Container( padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppTheme.neutral700.withValues(alpha: 0.4), AppTheme.neutral700.withValues(alpha: 0.2), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppBorderRadius.lg), border: Border.all( color: AppTheme.neutral600.withValues(alpha: 0.2), width: BorderWidth.thin, ), ), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: AppTheme.neutral600.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(AppBorderRadius.sm), ), child: const Icon( Icons.archive_rounded, color: AppTheme.neutral300, size: 16, ), ), const SizedBox(width: Spacing.md), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( conversation.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppTheme.neutral50, fontWeight: FontWeight.w600, fontSize: AppTypography.bodyLarge, ), ), const SizedBox(height: Spacing.xs), Text( _formatArchivedDate(conversation.updatedAt), style: const TextStyle( color: AppTheme.neutral400, fontSize: AppTypography.labelMedium, fontWeight: FontWeight.w500, ), ), ], ), ), ConduitIconButton( icon: Platform.isIOS ? CupertinoIcons.arrow_up_bin : Icons.unarchive_rounded, onPressed: onUnarchive, tooltip: 'Unarchive', ), ], ), ), ); } String _formatArchivedDate(DateTime date) { final now = DateTime.now(); final difference = now.difference(date); if (difference.inDays == 0) { return 'Today'; } else if (difference.inDays == 1) { return 'Yesterday'; } else if (difference.inDays < 7) { return '${difference.inDays} days ago'; } else { return '${date.month}/${date.day}/${date.year}'; } } }