refactor chat interaction handling, improve gesture detection, restore chat delay input indicator
This commit is contained in:
parent
8b54b3ba1b
commit
359509ec52
3 changed files with 151 additions and 138 deletions
|
|
@ -31,18 +31,21 @@ class Chat extends StatelessWidget {
|
|||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (chatStore.assetsStore.showEmoteMenu) {
|
||||
chatStore.assetsStore.showEmoteMenu = false;
|
||||
} else if (chatStore.textFieldFocusNode.hasFocus) {
|
||||
chatStore.unfocusInput();
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
MediaQuery(
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
children: [
|
||||
// Wrap only the message list with GestureDetector
|
||||
// so taps on ChatBottomBar don't trigger unfocus
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (chatStore.assetsStore.showEmoteMenu) {
|
||||
chatStore.assetsStore.showEmoteMenu = false;
|
||||
} else if (chatStore.textFieldFocusNode.hasFocus) {
|
||||
chatStore.unfocusInput();
|
||||
}
|
||||
},
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: chatStore.settings.messageScale.textScaler,
|
||||
),
|
||||
|
|
@ -93,68 +96,66 @@ class Chat extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
// Prevents accidental chat scrolling when swiping down from the top edge
|
||||
// to access system UI (Notification Center/Control Center) in landscape mode.
|
||||
if (context.isLandscape)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 24,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onVerticalDragStart: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
// Prevents accidental chat scrolling when swiping down from the top edge
|
||||
// to access system UI (Notification Center/Control Center) in landscape mode.
|
||||
if (context.isLandscape)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: ChatBottomBar(
|
||||
chatStore: chatStore,
|
||||
onAddChat: onAddChat,
|
||||
height: 24,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onVerticalDragStart: (_) {},
|
||||
),
|
||||
),
|
||||
AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.only(
|
||||
left: 4,
|
||||
top: 4,
|
||||
right: 4,
|
||||
bottom:
|
||||
68 +
|
||||
(chatStore.assetsStore.showEmoteMenu
|
||||
? 0
|
||||
: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
child: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: chatStore.autoScroll
|
||||
? null
|
||||
: ElevatedButton.icon(
|
||||
onPressed: chatStore.resumeScroll,
|
||||
icon: const Icon(
|
||||
Icons.arrow_downward_rounded,
|
||||
),
|
||||
label: Text(
|
||||
chatStore.messageBuffer.isNotEmpty
|
||||
? '${chatStore.messageBuffer.length} new ${chatStore.messageBuffer.length == 1 ? 'message' : 'messages'}'
|
||||
: 'Resume scroll',
|
||||
style: const TextStyle(
|
||||
fontFeatures: [
|
||||
FontFeature.tabularFigures(),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: ChatBottomBar(
|
||||
chatStore: chatStore,
|
||||
onAddChat: onAddChat,
|
||||
),
|
||||
),
|
||||
AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.only(
|
||||
left: 4,
|
||||
top: 4,
|
||||
right: 4,
|
||||
bottom:
|
||||
68 +
|
||||
(chatStore.assetsStore.showEmoteMenu
|
||||
? 0
|
||||
: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
child: Observer(
|
||||
builder: (_) => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: chatStore.autoScroll
|
||||
? null
|
||||
: ElevatedButton.icon(
|
||||
onPressed: chatStore.resumeScroll,
|
||||
icon: const Icon(Icons.arrow_downward_rounded),
|
||||
label: Text(
|
||||
chatStore.messageBuffer.isNotEmpty
|
||||
? '${chatStore.messageBuffer.length} new ${chatStore.messageBuffer.length == 1 ? 'message' : 'messages'}'
|
||||
: 'Resume scroll',
|
||||
style: const TextStyle(
|
||||
fontFeatures: [
|
||||
FontFeature.tabularFigures(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
|
|
|
|||
|
|
@ -84,10 +84,10 @@ class ChatBottomBar extends StatelessWidget {
|
|||
final isFullscreenOverlay =
|
||||
chatStore.settings.fullScreen && context.isLandscape;
|
||||
|
||||
// Check if chat delay is active (for indicator only, doesn't block input)
|
||||
final hasChatDelay =
|
||||
chatStore.settings.showVideo && chatStore.settings.chatDelay > 0;
|
||||
final delayTooltipMessage =
|
||||
'Chatting is disabled due to message delay (${chatStore.settings.chatDelay.toInt()}s)';
|
||||
|
||||
const loginTooltipMessage = 'Log in to chat';
|
||||
|
||||
final bottomBarContent = Column(
|
||||
|
|
@ -128,40 +128,44 @@ class ChatBottomBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
],
|
||||
// Wrap autocomplete sections with TextFieldTapRegion so taps
|
||||
// on them don't trigger TextField's onTapOutside callback.
|
||||
if (chatStore.settings.autocomplete &&
|
||||
chatStore.showEmoteAutocomplete &&
|
||||
matchingEmotes.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(4),
|
||||
itemCount: matchingEmotes.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(
|
||||
matchingEmotes[index],
|
||||
autocompleteMode: true,
|
||||
),
|
||||
onLongPress: () {
|
||||
HapticFeedback.lightImpact();
|
||||
TextFieldTapRegion(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(4),
|
||||
itemCount: matchingEmotes.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => InkWell(
|
||||
onTap: () => chatStore.addEmote(
|
||||
matchingEmotes[index],
|
||||
autocompleteMode: true,
|
||||
),
|
||||
onLongPress: () {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
IRCMessage.showEmoteDetailsBottomSheet(
|
||||
context,
|
||||
emote: matchingEmotes[index],
|
||||
launchExternal: chatStore.settings.launchUrlExternal,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Center(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: matchingEmotes[index].url,
|
||||
useFade: false,
|
||||
height:
|
||||
matchingEmotes[index].height?.toDouble() ??
|
||||
defaultEmoteSize,
|
||||
width: matchingEmotes[index].width?.toDouble(),
|
||||
IRCMessage.showEmoteDetailsBottomSheet(
|
||||
context,
|
||||
emote: matchingEmotes[index],
|
||||
launchExternal: chatStore.settings.launchUrlExternal,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Center(
|
||||
child: FrostyCachedNetworkImage(
|
||||
imageUrl: matchingEmotes[index].url,
|
||||
useFade: false,
|
||||
height:
|
||||
matchingEmotes[index].height?.toDouble() ??
|
||||
defaultEmoteSize,
|
||||
width: matchingEmotes[index].width?.toDouble(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -173,30 +177,32 @@ class ChatBottomBar extends StatelessWidget {
|
|||
chatStore.showMentionAutocomplete &&
|
||||
matchingChatters.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(4),
|
||||
itemCount: matchingChatters.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onPressed: () {
|
||||
final split = chatStore.textController.text.split(' ')
|
||||
..removeLast()
|
||||
..add('@${matchingChatters[index]} ');
|
||||
TextFieldTapRegion(
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(4),
|
||||
itemCount: matchingChatters.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onPressed: () {
|
||||
final split = chatStore.textController.text.split(' ')
|
||||
..removeLast()
|
||||
..add('@${matchingChatters[index]} ');
|
||||
|
||||
chatStore.textController.text = split.join(' ');
|
||||
chatStore.textController.selection =
|
||||
TextSelection.fromPosition(
|
||||
TextPosition(
|
||||
offset: chatStore.textController.text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(matchingChatters[index]),
|
||||
chatStore.textController.text = split.join(' ');
|
||||
chatStore.textController.selection =
|
||||
TextSelection.fromPosition(
|
||||
TextPosition(
|
||||
offset: chatStore.textController.text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(matchingChatters[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -211,16 +217,14 @@ class ChatBottomBar extends StatelessWidget {
|
|||
context.isLandscape)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final isDisabled =
|
||||
!chatStore.auth.isLoggedIn || hasChatDelay;
|
||||
final isDisabled = !chatStore.auth.isLoggedIn;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isDisabled
|
||||
? () {
|
||||
final message = hasChatDelay
|
||||
? delayTooltipMessage
|
||||
: loginTooltipMessage;
|
||||
chatStore.updateNotification(message);
|
||||
chatStore.updateNotification(
|
||||
loginTooltipMessage,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: IconButton(
|
||||
|
|
@ -242,16 +246,14 @@ class ChatBottomBar extends StatelessWidget {
|
|||
builder: (context) {
|
||||
final isDisabled =
|
||||
!chatStore.auth.isLoggedIn ||
|
||||
chatStore.isSendingMessage ||
|
||||
hasChatDelay;
|
||||
chatStore.isSendingMessage;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isDisabled && !chatStore.isSendingMessage
|
||||
? () {
|
||||
final message = hasChatDelay
|
||||
? delayTooltipMessage
|
||||
: loginTooltipMessage;
|
||||
chatStore.updateNotification(message);
|
||||
chatStore.updateNotification(
|
||||
loginTooltipMessage,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: TextField(
|
||||
|
|
@ -259,7 +261,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
focusNode: chatStore.textFieldFocusNode,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
// Disable text field when sending message, when not logged in, or when chat delay is active
|
||||
// Disable text field when sending message or when not logged in
|
||||
enabled: !isDisabled,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon:
|
||||
|
|
@ -283,6 +285,8 @@ class ChatBottomBar extends StatelessWidget {
|
|||
? 'Sending...'
|
||||
: chatStore.replyingToMessage != null
|
||||
? 'Reply'
|
||||
: hasChatDelay
|
||||
? 'Chat (${chatStore.settings.chatDelay.toInt()}s delay)'
|
||||
: 'Chat'
|
||||
: loginTooltipMessage,
|
||||
),
|
||||
|
|
@ -317,8 +321,7 @@ class ChatBottomBar extends StatelessWidget {
|
|||
: const Icon(Icons.send_rounded),
|
||||
onPressed:
|
||||
chatStore.auth.isLoggedIn &&
|
||||
!chatStore.isSendingMessage &&
|
||||
!hasChatDelay
|
||||
!chatStore.isSendingMessage
|
||||
? () => chatStore.sendMessage(
|
||||
chatStore.textController.text,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -74,14 +74,23 @@ class ChatTabs extends StatelessWidget {
|
|||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
|
||||
// If pressing the back button on Android while the emote menu
|
||||
// is open, close it instead of going back to the streams list.
|
||||
// On Android back gesture, close overlays first before navigating back
|
||||
final activeStore = chatTabsStore.activeChatStore;
|
||||
|
||||
// Priority 1: Close emote menu
|
||||
if (activeStore.assetsStore.showEmoteMenu) {
|
||||
activeStore.assetsStore.showEmoteMenu = false;
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Priority 2: Unfocus keyboard
|
||||
if (activeStore.textFieldFocusNode.hasFocus) {
|
||||
activeStore.unfocusInput();
|
||||
return;
|
||||
}
|
||||
|
||||
// Priority 3: Navigate back
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue