From 7cd53519216a70fde4140bfc221ac2539e859a86 Mon Sep 17 00:00:00 2001 From: Janez T Date: Fri, 6 Mar 2026 20:01:00 +0100 Subject: [PATCH] Improve message input layout --- lib/screens/home_screen.dart | 32 ++- lib/screens/messages_tab.dart | 392 +++++++++++++++++++++++----------- 2 files changed, 302 insertions(+), 122 deletions(-) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 03ffe2c..ded6a7b 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -103,15 +103,28 @@ class _HomeScreenState extends State void _initTabController() { _tabController = TabController(length: _enabledTabs.length, vsync: this); _tabController.addListener(_onTabChanged); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _handleTabActivated(_currentTab); + }); } void _onTabChanged() { + final previousTab = _currentTab; + final nextIndex = _tabController.index; + setState(() { - _currentIndex = _tabController.index; + _currentIndex = nextIndex; if (_currentTab != _HomeTab.map) { _isMapFullscreen = false; } }); + + final nextTab = _currentTab; + if (previousTab != nextTab) { + _handleTabActivated(nextTab); + } } void _updateTabController({ @@ -158,6 +171,23 @@ class _HomeScreenState extends State } } + void _handleTabActivated(_HomeTab tab) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + switch (tab) { + case _HomeTab.messages: + context.read().markAllAsRead(); + break; + case _HomeTab.contacts: + context.read().markAllAsViewed(); + break; + case _HomeTab.map: + break; + } + }); + } + Future _loadRxTxPreference() async { final prefs = await SharedPreferences.getInstance(); if (mounted) { diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 73ab81e..7692d53 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -287,6 +287,18 @@ class _MessagesTabState extends State { return 'Select recipient'; } + String _getDestinationLabel() { + if (_destinationType == + MessageDestinationPreferences.destinationTypeChannel && + _selectedRecipient != null) { + return _selectedRecipient!.getLocalizedDisplayName(context); + } + if (_selectedRecipient != null) { + return _selectedRecipient!.displayName; + } + return 'Public Channel'; + } + Future _sendMessage() async { final text = _textController.text.trim(); if (text.isEmpty) return; @@ -1561,95 +1573,182 @@ class _MessagesTabState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 8, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Quick actions (+) button - IconButton( - icon: Icon(_isRecording ? Icons.stop : Icons.add), - tooltip: _isRecording - ? 'Stop recording' - : 'More actions', - onPressed: _isRecording - ? _stopAndSendVoice - : _showComposerActions, - style: IconButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.primaryContainer, - foregroundColor: _isRecording - ? Colors.red - : Theme.of( - context, - ).colorScheme.onPrimaryContainer, + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.95), + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon( + _isRecording ? Icons.stop : Icons.add, + ), + tooltip: _isRecording + ? 'Stop recording' + : 'More actions', + onPressed: _isRecording + ? _stopAndSendVoice + : _showComposerActions, + color: _isRecording + ? Colors.red + : Theme.of( + context, + ).colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(22), + onTap: _showRecipientSelector, + child: Ink( + height: 46, + decoration: BoxDecoration( + color: + _destinationType == + MessageDestinationPreferences + .destinationTypeChannel + ? Theme.of(context) + .colorScheme + .surfaceContainerHighest + : Theme.of( + context, + ).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Theme.of( + context, + ).dividerColor.withValues(alpha: 0.7), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + ), + child: Row( + children: [ + Icon( + _getDestinationIcon(), + size: 18, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _getDestinationLabel(), + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: + _destinationType == + MessageDestinationPreferences + .destinationTypeChannel + ? Theme.of( + context, + ).colorScheme.onSurface + : Theme.of(context) + .colorScheme + .onSecondaryContainer, + ), + ), + ), + Icon( + Icons.expand_more_rounded, + size: 20, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ), + ), + ), + ], ), - ), - const SizedBox(width: 4), - // Destination switcher button - IconButton( - icon: Icon(_getDestinationIcon()), - tooltip: _getDestinationTooltip(), - onPressed: _showRecipientSelector, - style: IconButton.styleFrom( - backgroundColor: - _destinationType == - MessageDestinationPreferences - .destinationTypeChannel - ? Theme.of( - context, - ).colorScheme.surfaceContainerHighest - : Theme.of( - context, - ).colorScheme.secondaryContainer, - foregroundColor: - _destinationType == - MessageDestinationPreferences - .destinationTypeChannel - ? Theme.of(context).colorScheme.onSurface - : Theme.of( - context, - ).colorScheme.onSecondaryContainer, - ), - ), - const SizedBox(width: 4), - Expanded( - child: TextField( - controller: _textController, - focusNode: _focusNode, - maxLength: _maxCharacters, - maxLines: null, - maxLengthEnforcement: MaxLengthEnforcement.enforced, - style: const TextStyle(fontSize: 14), - decoration: InputDecoration( - hintText: AppLocalizations.of( - context, - )!.typeYourMessage, - hintStyle: const TextStyle(fontSize: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + constraints: const BoxConstraints( + minHeight: 48, + maxHeight: 140, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(28), + border: Border.all( + color: _focusNode.hasFocus + ? Theme.of( + context, + ).colorScheme.primary + : Theme.of(context).dividerColor + .withValues(alpha: 0.6), + width: _focusNode.hasFocus ? 1.5 : 1, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), + child: TextField( + controller: _textController, + focusNode: _focusNode, + minLines: 1, + maxLines: 4, + keyboardType: TextInputType.multiline, + inputFormatters: [ + LengthLimitingTextInputFormatter( + _maxCharacters, + ), + ], + style: const TextStyle(fontSize: 15), + textAlignVertical: + TextAlignVertical.center, + decoration: InputDecoration( + hintText: AppLocalizations.of( + context, + )!.typeYourMessage, + hintStyle: TextStyle( + fontSize: 15, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + border: InputBorder.none, + isCollapsed: true, + ), + textInputAction: TextInputAction.newline, + ), + ), + ), ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - isDense: true, - counterText: _characterCount >= 150 - ? '$_characterCount/$_maxCharacters' - : '', - counterStyle: TextStyle( - fontSize: 10, - color: _characterCount > _maxCharacters * 0.9 - ? Colors.orange - : Theme.of( - context, - ).textTheme.bodySmall?.color, - ), - suffixIcon: Builder( + const SizedBox(width: 10), + Builder( builder: (context) { final canSendText = !_isRecording && @@ -1698,48 +1797,99 @@ class _MessagesTabState extends State { (_voiceSupported && _isRecording) ? () => _stopAndSendVoice() : null, - child: IconButton( - icon: _isSendingVoice - ? const SizedBox( - width: 18, - height: 18, - child: - CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : Icon( - _isRecording - ? Icons.mic - : Icons.send_rounded, - size: 22, - color: _isRecording - ? Colors.red - : (_textController.text - .trim() - .isEmpty - ? Theme.of( - context, - ).disabledColor - : Theme.of(context) + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + duration: const Duration( + milliseconds: 180, + ), + width: 48, + height: 48, + decoration: BoxDecoration( + color: + canSendText || _isRecording + ? Theme.of( + context, + ).colorScheme.primary + : Theme.of(context) + .colorScheme + .surfaceContainerHighest, + shape: BoxShape.circle, + boxShadow: + canSendText || _isRecording + ? [ + BoxShadow( + color: + Theme.of(context) .colorScheme - .primary), - ), - onPressed: canSendText - ? _sendMessage - : null, + .primary + .withValues( + alpha: 0.28, + ), + blurRadius: 16, + offset: const Offset( + 0, + 6, + ), + ), + ] + : null, + ), + child: _isSendingVoice + ? Center( + child: + CircularProgressIndicator( + strokeWidth: 2, + color: + Theme.of( + context, + ) + .colorScheme + .onPrimary, + ), + ) + : Icon( + _isRecording + ? Icons.mic_rounded + : Icons.send_rounded, + color: + canSendText || + _isRecording + ? Theme.of(context) + .colorScheme + .onPrimary + : Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + const SizedBox(height: 6), + Text( + '$_characterCount/$_maxCharacters', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: + _characterCount > + _maxCharacters * 0.9 + ? Colors.orange.shade800 + : Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ], ), ), ), ); }, ), - ), - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendMessage(), + ], ), - ), - ], + ], + ), ), ), ],