diff --git a/mobile/lib/pages/chat_page.dart b/mobile/lib/pages/chat_page.dart index 393134c..f85ae75 100644 --- a/mobile/lib/pages/chat_page.dart +++ b/mobile/lib/pages/chat_page.dart @@ -18,15 +18,17 @@ class ChatPage extends StatefulWidget { State createState() => _ChatPageState(); } -class _ChatPageState extends State { +class _ChatPageState extends State with WidgetsBindingObserver { final ScrollController _scrollController = ScrollController(); bool _showScrollToBottom = false; + bool _isNearBottom = true; final Set _selectedIndices = {}; dynamic _replyingToMessage; @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _scrollController.addListener(_scrollListener); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -36,6 +38,7 @@ class _ChatPageState extends State { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _scrollController.removeListener(_scrollListener); _scrollController.dispose(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -46,12 +49,27 @@ class _ChatPageState extends State { super.dispose(); } + @override + void didChangeMetrics() { + super.didChangeMetrics(); + if (_isNearBottom) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _scrollToBottom(animated: true); + } + }); + } + } + void _scrollListener() { if (!_scrollController.hasClients) return; - final isScrolledUp = - _scrollController.position.maxScrollExtent - _scrollController.offset > - 100; + final maxScrollExtent = _scrollController.position.maxScrollExtent; + final offset = _scrollController.offset; + + _isNearBottom = maxScrollExtent - offset <= 100; + + final isScrolledUp = maxScrollExtent - offset > 100; if (isScrolledUp != _showScrollToBottom) { setState(() { @@ -120,9 +138,9 @@ class _ChatPageState extends State { _clearSelection(); } - String _getPresenceStatusText(ChatController state) { - if (state.isPeerTyping) return 'typing...'; - return state.isPeerOnline ? 'Online' : 'Offline'; + UserStatus _getPresenceStatusText(ChatController state) { + if (state.isPeerTyping) return UserStatus.typing; + return state.isPeerOnline ? UserStatus.online : UserStatus.offline; } bool _isSameDay(DateTime date1, DateTime date2) { @@ -145,6 +163,7 @@ class _ChatPageState extends State { final chatState = context.watch(); final activeChat = chatState.activeChat; final isSelectionMode = _selectedIndices.isNotEmpty; + final theme = Theme.of(context); return PopScope( canPop: !isSelectionMode, @@ -154,10 +173,9 @@ class _ChatPageState extends State { }); }, child: Scaffold( - backgroundColor: Colors.white, + backgroundColor: theme.scaffoldBackgroundColor, extendBody: true, extendBodyBehindAppBar: true, - appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: AnimatedSwitcher( @@ -165,24 +183,32 @@ class _ChatPageState extends State { child: isSelectionMode ? AppBar( key: const ValueKey('SelectionAppBar'), - backgroundColor: Colors.blueAccent, + backgroundColor: theme.colorScheme.primary, leading: IconButton( - icon: const Icon(Icons.close, color: Colors.white), + icon: Icon( + Icons.close, + color: theme.colorScheme.onPrimary, + ), onPressed: _clearSelection, ), title: Text( '${_selectedIndices.length} Selected', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: theme.colorScheme.onPrimary), ), actions: [ IconButton( - icon: const Icon(Icons.copy, color: Colors.white), + icon: Icon( + Icons.copy, + color: theme.colorScheme.onPrimary, + ), onPressed: () => _copySelectedMessages(activeChat), ), IconButton( - icon: const Icon(Icons.delete, color: Colors.white), + icon: Icon( + Icons.delete, + color: theme.colorScheme.onPrimary, + ), onPressed: () { - // TODO: delete on DB _clearSelection(); }, ), @@ -195,14 +221,16 @@ class _ChatPageState extends State { ), ), ), - body: Stack( children: [ activeChat.isEmpty - ? const Center( + ? Center( child: Text( "No messages yet", - style: TextStyle(color: Colors.grey, fontSize: 14), + style: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), ) : ListView.builder( @@ -214,7 +242,7 @@ class _ChatPageState extends State { ScrollViewKeyboardDismissBehavior.onDrag, padding: EdgeInsets.only( top: 120, - bottom: _replyingToMessage != null ? 180 : 120, + bottom: _replyingToMessage != null ? 220 : 140, left: 16, right: 16, ), @@ -258,19 +286,19 @@ class _ChatPageState extends State { vertical: 4, ), decoration: BoxDecoration( - color: Colors.grey[200], + color: + theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), ), child: Text( _formatDateSeparator(msg.createdAt), - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: Colors.black54, + color: theme.colorScheme.onSurfaceVariant, ), ), ), ), - GestureDetector( onLongPress: () => _toggleSelection(index), onTap: () { @@ -278,7 +306,9 @@ class _ChatPageState extends State { }, child: Container( color: isSelected - ? Colors.blue.withValues(alpha: 0.2) + ? theme.colorScheme.primary.withValues( + alpha: 0.2, + ) : Colors.transparent, child: Dismissible( key: ValueKey( @@ -295,9 +325,11 @@ class _ChatPageState extends State { alignment: Alignment.centerLeft, padding: const EdgeInsets.only(left: 16), color: Colors.transparent, - child: const Icon( + child: Icon( Icons.reply, - color: Colors.grey, + color: theme.colorScheme.primary.withValues( + alpha: 0.7, + ), ), ), child: ChatBubble( @@ -313,10 +345,9 @@ class _ChatPageState extends State { ); }, ), - Positioned( right: 16, - bottom: _replyingToMessage != null ? 140 : 90, + bottom: _replyingToMessage != null ? 180 : 100, child: AnimatedScale( scale: _showScrollToBottom ? 1.0 : 0.0, duration: const Duration(milliseconds: 250), @@ -326,8 +357,8 @@ class _ChatPageState extends State { duration: const Duration(milliseconds: 200), child: FloatingActionButton( mini: true, - backgroundColor: Colors.white, - foregroundColor: Colors.blue, + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.primary, elevation: 4, onPressed: () => _scrollToBottom(animated: true), child: const Icon(Icons.keyboard_arrow_down), @@ -335,7 +366,6 @@ class _ChatPageState extends State { ), ), ), - Align( alignment: Alignment.bottomCenter, child: Column( @@ -345,16 +375,19 @@ class _ChatPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white, - border: const Border( - left: BorderSide(color: Colors.blue, width: 4), - top: BorderSide(color: Colors.black12, width: 1), + color: theme.colorScheme.surface, + border: Border( + left: BorderSide( + color: theme.colorScheme.primary, + width: 4, + ), + top: BorderSide(color: theme.dividerColor, width: 1), ), - boxShadow: const [ + boxShadow: [ BoxShadow( - color: Colors.black12, + color: theme.shadowColor.withValues(alpha: 0.1), blurRadius: 4, - offset: Offset(0, -2), + offset: const Offset(0, -2), ), ], ), @@ -364,11 +397,11 @@ class _ChatPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( "Replying to message", style: TextStyle( fontWeight: FontWeight.bold, - color: Colors.blue, + color: theme.colorScheme.primary, fontSize: 12, ), ), @@ -377,13 +410,19 @@ class _ChatPageState extends State { _replyingToMessage.content, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.black87), + style: TextStyle( + color: theme.colorScheme.onSurface, + ), ), ], ), ), IconButton( - icon: const Icon(Icons.close, size: 20), + icon: Icon( + Icons.close, + size: 20, + color: theme.iconTheme.color, + ), onPressed: () => setState(() => _replyingToMessage = null), ), diff --git a/mobile/lib/pages/home_page.dart b/mobile/lib/pages/home_page.dart index a2f320c..2e6842d 100644 --- a/mobile/lib/pages/home_page.dart +++ b/mobile/lib/pages/home_page.dart @@ -58,8 +58,7 @@ class _HomePageState extends State { @override void dispose() { _searchController.dispose(); - _scrollController - .dispose(); + _scrollController.dispose(); super.dispose(); } @@ -70,6 +69,7 @@ class _HomePageState extends State { required String label, required double width, }) { + final theme = Theme.of(context); final isSelected = _page == index; return GestureDetector( @@ -92,7 +92,9 @@ class _HomePageState extends State { curve: Curves.easeOutBack, child: Icon( isSelected ? activeIcon : icon, - color: isSelected ? const Color(0xFF1890FF) : Colors.black54, + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, size: 24, ), ), @@ -109,8 +111,8 @@ class _HomePageState extends State { ? FontWeight.bold : FontWeight.w600, color: isSelected - ? const Color(0xFF1890FF) - : Colors.black54, + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, ), ), ), @@ -123,6 +125,7 @@ class _HomePageState extends State { } Widget buildGlassNavigationBar({Key? key}) { + final theme = Theme.of(context); return SafeArea( key: key, child: Padding( @@ -134,21 +137,21 @@ class _HomePageState extends State { child: Container( height: 68, decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.35), + color: theme.colorScheme.surface.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(24), border: Border.all( - color: Colors.white.withValues(alpha: 0.45), + color: theme.colorScheme.surface.withValues(alpha: 0.45), width: 1.5, ), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.04), + color: theme.shadowColor.withValues(alpha: 0.04), blurRadius: 24, spreadRadius: 2, offset: const Offset(0, 8), ), BoxShadow( - color: const Color(0xFF1890FF).withValues(alpha: 0.03), + color: theme.colorScheme.primary.withValues(alpha: 0.03), blurRadius: 16, spreadRadius: -4, offset: const Offset(0, -2), @@ -175,13 +178,15 @@ class _HomePageState extends State { ), child: Container( decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.85), + color: theme.colorScheme.surface.withValues( + alpha: 0.85, + ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( - color: const Color( - 0xFF1890FF, - ).withValues(alpha: 0.08), + color: theme.colorScheme.primary.withValues( + alpha: 0.08, + ), blurRadius: 12, offset: const Offset(0, 4), ), @@ -227,8 +232,10 @@ class _HomePageState extends State { } Widget buildHomeTab(ChatController chatState) { + final theme = Theme.of(context); + return RefreshIndicator( - color: const Color(0xFF1890FF), + color: theme.colorScheme.primary, onRefresh: _isSelectionMode ? () async {} : () => chatState.loadInbox(), child: CustomScrollView( controller: _scrollController, @@ -249,7 +256,9 @@ class _HomePageState extends State { child: BackdropFilter( enabled: true, filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: Container(color: Colors.white.withValues(alpha: 0.4)), + child: Container( + color: theme.scaffoldBackgroundColor.withValues(alpha: 0.7), + ), ), ), leading: _isSelectionMode @@ -265,18 +274,18 @@ class _HomePageState extends State { }, child: Hero( tag: 'User Profile', - child: const Padding( - padding: EdgeInsets.only( + child: Padding( + padding: const EdgeInsets.only( left: 16.0, top: 10.0, bottom: 10.0, ), child: CircleAvatar( radius: 18, - backgroundColor: Color(0xFFD6E4FF), + backgroundColor: theme.colorScheme.primaryContainer, child: Icon( Icons.person, - color: Color(0xFF1890FF), + color: theme.colorScheme.primary, size: 18, ), ), @@ -287,7 +296,10 @@ class _HomePageState extends State { title: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: _isSelectionMode - ? Text('${_selectedChatIds.length} Selected') + ? Text( + '${_selectedChatIds.length} Selected', + style: TextStyle(color: theme.colorScheme.onSurface), + ) : AnimatedSwitcher( switchInCurve: Curves.decelerate, switchOutCurve: Curves.decelerate, @@ -310,16 +322,17 @@ class _HomePageState extends State { controller: _searchController, key: const ValueKey('search'), autofocus: true, - style: const TextStyle(color: Colors.black87), + style: TextStyle( + color: theme.colorScheme.onSurface, + ), decoration: InputDecoration( hintText: 'Search conversations...', - hintStyle: const TextStyle( - color: Colors.black38, + hintStyle: TextStyle( + color: theme.colorScheme.onSurfaceVariant, ), filled: true, - fillColor: Colors.black.withValues( - alpha: 0.05, - ), + fillColor: theme.colorScheme.onSurface + .withValues(alpha: 0.08), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 0, @@ -331,15 +344,15 @@ class _HomePageState extends State { ), ), ) - : const Text( + : Text( 'Elephant', style: TextStyle( letterSpacing: 0.5, fontSize: 20, fontWeight: FontWeight.bold, - color: Colors.black87, + color: theme.colorScheme.onSurface, ), - key: ValueKey('title'), + key: const ValueKey('title'), ), ), ), @@ -355,12 +368,13 @@ class _HomePageState extends State { }, icon: Icon( _isSearchOpen ? Icons.close : Icons.search, - color: Colors.black87, + color: theme.colorScheme.onSurface, ), ), if (!_isSearchOpen) !_isSelectionMode ? PopupMenuButton( + iconColor: theme.colorScheme.onSurface, onSelected: (String value) { switch (value) { case 'settings': @@ -393,6 +407,7 @@ class _HomePageState extends State { ], ) : PopupMenuButton( + iconColor: theme.colorScheme.onSurface, onSelected: (String value) { switch (value) { case 'Select all': @@ -427,12 +442,14 @@ class _HomePageState extends State { ], ), chatState.inbox.isEmpty - ? const SliverFillRemaining( + ? SliverFillRemaining( hasScrollBody: false, child: Center( child: Text( "No conversations yet", - style: TextStyle(color: Colors.grey), + style: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + ), ), ), ) @@ -467,7 +484,6 @@ class _HomePageState extends State { ); }, ), - const SliverPadding(padding: EdgeInsets.only(bottom: 120)), ], ), @@ -477,6 +493,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { final chatState = context.watch(); + final theme = Theme.of(context); return PopScope( canPop: _selectedChatIds.isEmpty, @@ -487,23 +504,29 @@ class _HomePageState extends State { }); }, child: Scaffold( - backgroundColor: Colors.white, + backgroundColor: theme.scaffoldBackgroundColor, extendBodyBehindAppBar: true, extendBody: true, body: IndexedStack( index: _page, children: [ - const Center( + Center( child: Text( "No stories available", - style: TextStyle(color: Colors.black54, fontSize: 16), + style: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + fontSize: 16, + ), ), ), buildHomeTab(chatState), - const Center( + Center( child: Text( "No recent calls", - style: TextStyle(color: Colors.black54, fontSize: 16), + style: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + fontSize: 16, + ), ), ), ], @@ -511,16 +534,19 @@ class _HomePageState extends State { floatingActionButton: _page == 1 ? AnimatedSlide( duration: const Duration(milliseconds: 300), - offset: _isFabVisible ? Offset.zero : const Offset(0, 2), + offset: (_isFabVisible && !_isSelectionMode) + ? Offset.zero + : const Offset(0, 2), child: AnimatedOpacity( duration: const Duration(milliseconds: 300), - opacity: _isFabVisible ? 1.0 : 0.0, + opacity: (_isFabVisible && !_isSelectionMode) ? 1.0 : 0.0, child: Padding( padding: const EdgeInsets.only(bottom: 10), child: FloatingActionButton( heroTag: "fab_pen", - backgroundColor: const Color(0xFF1890FF), - child: const Icon(Icons.edit, color: Colors.white), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + child: const Icon(Icons.edit), onPressed: () { Navigator.push( context, diff --git a/mobile/lib/widgets/chat_screen_modular_widgets.dart b/mobile/lib/widgets/chat_screen_modular_widgets.dart index 6e39540..8b9e4eb 100644 --- a/mobile/lib/widgets/chat_screen_modular_widgets.dart +++ b/mobile/lib/widgets/chat_screen_modular_widgets.dart @@ -1,32 +1,49 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:intl/intl.dart'; + +enum UserStatus { offline, online, typing } class GlassAppBar extends StatelessWidget implements PreferredSizeWidget { final String name; - final String status; + final UserStatus status; const GlassAppBar({super.key, required this.name, required this.status}); + String _getStatusText() { + switch (status) { + case UserStatus.typing: + return "typing..."; + case UserStatus.online: + return "Online"; + case UserStatus.offline: + return "Offline"; + } + } + @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: AppBar( - backgroundColor: Colors.white.withValues(alpha: 0.4), + backgroundColor: theme.scaffoldBackgroundColor.withValues(alpha: 0.4), elevation: 0, scrolledUnderElevation: 0, leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black87), + icon: Icon(Icons.arrow_back, color: theme.iconTheme.color), onPressed: () => Navigator.pop(context), ), title: Row( children: [ - const CircleAvatar( - backgroundColor: Color(0xFFD6E4FF), - child: Icon(Icons.person, color: Color(0xFF1890FF)), + CircleAvatar( + backgroundColor: theme.colorScheme.primaryContainer, + child: Icon(Icons.person, color: theme.colorScheme.primary), ), const SizedBox(width: 12), Column( @@ -34,20 +51,20 @@ class GlassAppBar extends StatelessWidget implements PreferredSizeWidget { children: [ Text( name, - style: const TextStyle( - color: Colors.black87, + style: TextStyle( + color: theme.textTheme.titleLarge?.color, fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( - status, + _getStatusText(), style: TextStyle( - color: status == "typing..." + color: status == UserStatus.typing ? Colors.green - : Colors.black54, + : theme.textTheme.bodySmall?.color, fontSize: 12, - fontWeight: status == "typing..." + fontWeight: status == UserStatus.typing ? FontWeight.bold : FontWeight.normal, ), @@ -79,10 +96,46 @@ class ChatBubble extends StatelessWidget { required this.isRead, }); + MarkdownStyleSheet _getMarkdownStyle( + bool isMe, + Color primaryColor, + Color onPrimary, + Color onSurface, + ) { + return MarkdownStyleSheet( + p: TextStyle(color: isMe ? onPrimary : onSurface, fontSize: 15), + strong: TextStyle( + color: isMe ? onPrimary : onSurface, + fontWeight: FontWeight.bold, + ), + em: TextStyle( + color: isMe ? onPrimary : onSurface, + fontStyle: FontStyle.italic, + ), + del: TextStyle( + color: isMe + ? onPrimary.withValues(alpha: 0.7) + : onSurface.withValues(alpha: 0.5), + decoration: TextDecoration.lineThrough, + ), + code: TextStyle( + backgroundColor: isMe + ? primaryColor.withValues(alpha: 0.8) + : Colors.grey.withValues(alpha: 0.3), + color: isMe ? onPrimary : Colors.red[800], + fontFamily: 'monospace', + ), + a: TextStyle( + color: isMe ? onPrimary.withValues(alpha: 0.9) : primaryColor, + decoration: TextDecoration.underline, + ), + ); + } + @override Widget build(BuildContext context) { - final String formattedTime = - "${timestamp.hour % 12 == 0 ? 12 : timestamp.hour % 12}:${timestamp.minute.toString().padLeft(2, '0')} ${timestamp.hour >= 12 ? 'PM' : 'AM'}"; + final theme = Theme.of(context); + final String formattedTime = DateFormat.jm().format(timestamp); return Align( alignment: isMe ? Alignment.centerRight : Alignment.centerLeft, @@ -93,7 +146,9 @@ class ChatBubble extends StatelessWidget { maxWidth: MediaQuery.of(context).size.width * 0.75, ), decoration: BoxDecoration( - color: isMe ? const Color(0xFF1890FF) : const Color(0xFFF0F2F5), + color: isMe + ? theme.colorScheme.primary + : theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), @@ -108,32 +163,11 @@ class ChatBubble extends StatelessWidget { MarkdownBody( data: message, selectable: false, - styleSheet: MarkdownStyleSheet( - p: TextStyle( - color: isMe ? Colors.white : Colors.black87, - fontSize: 15, - ), - strong: TextStyle( - color: isMe ? Colors.white : Colors.black87, - fontWeight: FontWeight.bold, - ), - em: TextStyle( - color: isMe ? Colors.white : Colors.black87, - fontStyle: FontStyle.italic, - ), - del: TextStyle( - color: isMe ? Colors.white70 : Colors.black54, - decoration: TextDecoration.lineThrough, - ), - code: TextStyle( - backgroundColor: isMe ? Colors.blue[800] : Colors.grey[300], - color: isMe ? Colors.blue[50] : Colors.red[800], - fontFamily: 'monospace', - ), - a: TextStyle( - color: isMe ? Colors.blue[100] : Colors.blue[700], - decoration: TextDecoration.underline, - ), + styleSheet: _getMarkdownStyle( + isMe, + theme.colorScheme.primary, + theme.colorScheme.onPrimary, + theme.colorScheme.onSurface, ), ), const SizedBox(height: 4), @@ -143,7 +177,9 @@ class ChatBubble extends StatelessWidget { Text( formattedTime, style: TextStyle( - color: isMe ? Colors.white70 : Colors.black38, + color: isMe + ? theme.colorScheme.onPrimary.withValues(alpha: 0.7) + : theme.colorScheme.onSurfaceVariant, fontSize: 10, ), ), @@ -152,7 +188,9 @@ class ChatBubble extends StatelessWidget { Icon( isRead ? Icons.done_all : Icons.done, size: 14, - color: isRead ? Colors.lightBlueAccent : Colors.white70, + color: isRead + ? Colors.lightBlueAccent + : theme.colorScheme.onPrimary.withValues(alpha: 0.7), ), ], ], @@ -180,11 +218,13 @@ class ChatInputArea extends StatefulWidget { class _ChatInputAreaState extends State { final _controller = TextEditingController(); - final FocusNode _focusNode = FocusNode(); + final FocusNode _focusNode = FocusNode(); bool _isTyping = false; Timer? _typingTimer; - bool _showFormattingToolbar = false; + bool _showFormattingToolbar = false; + final int _charLimit = 5000; + bool _isOverLimit = false; @override void initState() { @@ -207,9 +247,6 @@ class _ChatInputAreaState extends State { super.dispose(); } - final int _charLimit = 5000; // limit - bool _isOverLimit = false; - void _handleOnChange(String text) { setState(() { _isOverLimit = text.length > _charLimit; @@ -289,9 +326,11 @@ class _ChatInputAreaState extends State { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + return SafeArea( top: false, - bottom: false, + bottom: true, child: ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), @@ -347,23 +386,38 @@ class _ChatInputAreaState extends State { minLines: 1, maxLines: 5, maxLength: _charLimit, - maxLengthEnforcement: .none, + maxLengthEnforcement: MaxLengthEnforcement.none, style: TextStyle( - color: _isOverLimit ? Colors.red : Colors.black87, + color: _isOverLimit + ? theme.colorScheme.error + : theme.colorScheme.onSurface, fontSize: 15, ), + buildCounter: + ( + context, { + required currentLength, + required isFocused, + required maxLength, + }) => _isOverLimit + ? Text('$currentLength/$maxLength') + : null, decoration: InputDecoration( hintText: "Write a message...", - hintStyle: const TextStyle(color: Colors.black38), + hintStyle: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + ), fillColor: _isOverLimit - ? Colors.red.withValues(alpha: 0.1) - : const Color(0xFFF5F5F5), + ? theme.colorScheme.error.withValues(alpha: 0.1) + : theme.colorScheme.surfaceContainerHighest, filled: true, errorText: _isOverLimit ? 'character limit exceeded' : null, counterStyle: TextStyle( - color: _isOverLimit ? Colors.red : Colors.black54, + color: _isOverLimit + ? theme.colorScheme.error + : theme.colorScheme.onSurfaceVariant, fontWeight: _isOverLimit ? FontWeight.bold : FontWeight.normal, @@ -371,8 +425,8 @@ class _ChatInputAreaState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: _isOverLimit - ? const BorderSide( - color: Colors.red, + ? BorderSide( + color: theme.colorScheme.error, width: 1.5, ) : BorderSide.none, @@ -380,14 +434,17 @@ class _ChatInputAreaState extends State { focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: _isOverLimit - ? const BorderSide(color: Colors.red, width: 2) + ? BorderSide( + color: theme.colorScheme.error, + width: 2, + ) : BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: _isOverLimit - ? const BorderSide( - color: Colors.red, + ? BorderSide( + color: theme.colorScheme.error, width: 1.5, ) : BorderSide.none, @@ -403,7 +460,7 @@ class _ChatInputAreaState extends State { IconButton( icon: _isOverLimit ? const Icon(Icons.not_interested) - : const Icon(Icons.send, color: Color(0xFF1890FF)), + : Icon(Icons.send, color: theme.colorScheme.primary), onPressed: _isOverLimit ? null : _submit, ), ], @@ -425,8 +482,13 @@ class _FormatButton extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return IconButton( - icon: Icon(icon, size: 20, color: Colors.black54), + icon: Icon( + icon, + size: 20, + color: theme.iconTheme.color?.withValues(alpha: 0.6) ?? Colors.black54, + ), onPressed: onPressed, visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 4), diff --git a/mobile/lib/widgets/custom_cards.dart b/mobile/lib/widgets/custom_cards.dart index a939295..dc2f870 100644 --- a/mobile/lib/widgets/custom_cards.dart +++ b/mobile/lib/widgets/custom_cards.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:provider/provider.dart'; import '../models/conversation.dart'; import '../pages/chat_page.dart'; @@ -52,7 +53,7 @@ class CustomChatCard extends StatelessWidget { } else { final controller = context.read(); await controller.openChat(conversation.chatUserId); - + if (context.mounted) { Navigator.push( context, @@ -118,18 +119,50 @@ class CustomChatCard extends StatelessWidget { ), ), const SizedBox(height: 4), - Text( - conversation.lastMessage, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: hasUnread ? Colors.black87 : Colors.black54, - fontWeight: hasUnread - ? FontWeight.w600 - : FontWeight.normal, + MarkdownBody( + data: conversation.lastMessage, + selectable: false, + styleSheet: MarkdownStyleSheet( + p: TextStyle( + color: hasUnread ? Colors.black87 : Colors.black54, + fontSize: 15, + ), + strong: TextStyle( + color: hasUnread + ? Colors.black87 + : const Color.fromARGB(171, 0, 0, 0), + fontWeight: FontWeight.bold, + ), + em: TextStyle( + color: hasUnread ? Colors.black87 : Colors.black54, + fontStyle: FontStyle.italic, + ), + del: TextStyle( + color: hasUnread ? Colors.black87 : Colors.black54, + decoration: TextDecoration.lineThrough, + ), + code: TextStyle( + color: hasUnread ? Colors.black87 : Colors.black54, + fontFamily: 'monospace', + ), + a: TextStyle( + color: hasUnread ? Colors.black87 : Colors.black54, + decoration: TextDecoration.underline, + ), ), ), + // Text( + // conversation.lastMessage, + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle( + // fontSize: 14, + // color: hasUnread ? Colors.black87 : Colors.black54, + // fontWeight: hasUnread + // ? FontWeight.w600 + // : FontWeight.normal, + // ), + // ), ], ), ),