diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index affd674a2..3b4e06a6f 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -61,6 +61,7 @@ class ChatPage extends StatelessWidget implements PageShape { [], inputOptions: InputOptions( sendOnEnter: true, + focusNode: chatModel.inputNode, inputTextStyle: TextStyle( fontSize: 14, color: Theme.of(context) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 206095345..2211a0b0d 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -139,14 +139,20 @@ class ConnectionManagerState extends State { selectedTabBackgroundColor: Theme.of(context).hintColor.withOpacity(0.2), tabBuilder: (key, icon, label, themeConf) { + final client = serverModel.clients.firstWhereOrNull( + (client) => client.id.toString() == key); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - icon, Tooltip( message: key, waitDuration: Duration(seconds: 1), child: label), + Obx(() => Offstage( + offstage: + !(client?.hasUnreadChatMessage.value ?? false), + child: + Icon(Icons.circle, color: Colors.red, size: 10))) ], ); }, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index f4a50c725..92c868c34 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -29,6 +29,7 @@ class TabInfo { final IconData? unselectedIcon; final bool closable; final VoidCallback? onTabCloseButton; + final VoidCallback? onTap; final Widget page; TabInfo( @@ -38,6 +39,7 @@ class TabInfo { this.unselectedIcon, this.closable = true, this.onTabCloseButton, + this.onTap, required this.page}); } @@ -56,6 +58,8 @@ class DesktopTabState { final PageController pageController = PageController(); int selected = 0; + TabInfo get selectedTabInfo => tabs[selected]; + DesktopTabState() { scrollController.itemCount = tabs.length; } @@ -173,7 +177,6 @@ typedef LabelGetter = Rx Function(String key); int _lastClickTime = DateTime.now().millisecondsSinceEpoch; class DesktopTab extends StatelessWidget { - final Function(String)? onTabClose; final bool showTabBar; final bool showLogo; final bool showTitle; @@ -201,7 +204,6 @@ class DesktopTab extends StatelessWidget { DesktopTab({ Key? key, required this.controller, - this.onTabClose, this.showTabBar = true, this.showLogo = true, this.showTitle = true, @@ -357,7 +359,6 @@ class DesktopTab extends StatelessWidget { }, child: _ListView( controller: controller, - onTabClose: onTabClose, tabBuilder: tabBuilder, labelGetter: labelGetter, maxLabelWidth: maxLabelWidth, @@ -613,7 +614,6 @@ Future closeConfirmDialog() async { class _ListView extends StatelessWidget { final DesktopTabController controller; - final Function(String key)? onTabClose; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; @@ -625,7 +625,6 @@ class _ListView extends StatelessWidget { const _ListView( {required this.controller, - required this.onTabClose, this.tabBuilder, this.labelGetter, this.maxLabelWidth, @@ -654,7 +653,9 @@ class _ListView extends StatelessWidget { final index = e.key; final tab = e.value; return _Tab( + key: ValueKey(tab.key), index: index, + tabInfoKey: tab.key, label: labelGetter == null ? Rx(tab.label) : labelGetter!(tab.label), @@ -669,18 +670,11 @@ class _ListView extends StatelessWidget { controller.remove(index); } }, - onSelected: () => controller.jumpTo(index), - tabBuilder: tabBuilder == null - ? null - : (String key, Widget icon, Widget labelWidget, - TabThemeConf themeConf) { - return tabBuilder!( - tab.label, - icon, - labelWidget, - themeConf, - ); - }, + onTap: () { + controller.jumpTo(index); + tab.onTap?.call(); + }, + tabBuilder: tabBuilder, maxLabelWidth: maxLabelWidth, selectedTabBackgroundColor: selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, @@ -691,13 +685,14 @@ class _ListView extends StatelessWidget { class _Tab extends StatefulWidget { final int index; + final String tabInfoKey; final Rx label; final IconData? selectedIcon; final IconData? unselectedIcon; final bool closable; final int selected; final Function() onClose; - final Function() onSelected; + final Function() onTap; final TabBuilder? tabBuilder; final double? maxLabelWidth; final Color? selectedTabBackgroundColor; @@ -706,6 +701,7 @@ class _Tab extends StatefulWidget { const _Tab({ Key? key, required this.index, + required this.tabInfoKey, required this.label, this.selectedIcon, this.unselectedIcon, @@ -713,7 +709,7 @@ class _Tab extends StatefulWidget { required this.closable, required this.selected, required this.onClose, - required this.onSelected, + required this.onTap, this.maxLabelWidth, this.selectedTabBackgroundColor, this.unSelectedTabBackgroundColor, @@ -763,7 +759,7 @@ class _TabState extends State<_Tab> with RestorationMixin { ], ); } else { - return widget.tabBuilder!(widget.label.value, icon, labelWidget, + return widget.tabBuilder!(widget.tabInfoKey, icon, labelWidget, TabThemeConf(iconSize: _kIconSize)); } } @@ -780,7 +776,7 @@ class _TabState extends State<_Tab> with RestorationMixin { hover.value = value; restoreHover.value = value; }, - onTap: () => widget.onSelected(), + onTap: () => widget.onTap(), child: Container( color: isSelected ? widget.selectedTabBackgroundColor diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 9a0d89ef4..aaa5e1da5 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -14,11 +14,11 @@ class MessageBody { MessageBody(this.chatUser, this.chatMessages); void insert(ChatMessage cm) { - this.chatMessages.insert(0, cm); + chatMessages.insert(0, cm); } void clear() { - this.chatMessages.clear(); + chatMessages.clear(); } } @@ -54,6 +54,8 @@ class ChatModel with ChangeNotifier { ChatModel(this.parent); + FocusNode inputNode = FocusNode(); + ChatUser get currentUser { final user = messages[currentID]?.chatUser; if (user == null) { @@ -199,6 +201,11 @@ class ChatModel with ChangeNotifier { } receive(int id, String text) async { + final session = parent.target; + if (session == null) { + debugPrint("Failed to receive msg, session state is null"); + return; + } if (text.isEmpty) return; // mobile: first message show overlay icon if (chatIconOverlayEntry == null) { @@ -208,27 +215,32 @@ class ChatModel with ChangeNotifier { if (!_isShowChatPage) { toggleCMChatPage(id); } - parent.target?.serverModel.jumpTo(id); - late final chatUser; + int toId = currentID; + + late final ChatUser chatUser; if (id == clientModeID) { chatUser = ChatUser( - firstName: parent.target?.ffiModel.pi.username, - id: await bind.mainGetLastRemoteId(), + firstName: session.ffiModel.pi.username, + id: session.id, ); + toId = id; } else { - final client = parent.target?.serverModel.clients - .firstWhere((client) => client.id == id); - if (client == null) { - return debugPrint("Failed to receive msg,user doesn't exist"); - } + final client = + session.serverModel.clients.firstWhere((client) => client.id == id); if (isDesktop) { window_on_top(null); - var index = parent.target?.serverModel.clients - .indexWhere((client) => client.id == id); - if (index != null && index >= 0) { - gFFI.serverModel.tabController.jumpTo(index); + // disable auto jumpTo other tab when hasFocus, and mark unread message + final currentSelectedTab = + session.serverModel.tabController.state.value.selectedTabInfo; + if (currentSelectedTab.key != id.toString() && inputNode.hasFocus) { + client.hasUnreadChatMessage.value = true; + } else { + parent.target?.serverModel.jumpTo(id); + toId = id; } + } else { + toId = id; } chatUser = ChatUser(id: client.peerId, firstName: client.name); } @@ -238,7 +250,7 @@ class ChatModel with ChangeNotifier { } _messages[id]!.insert( ChatMessage(text: text, user: chatUser, createdAt: DateTime.now())); - _currentID = id; + _currentID = toId; notifyListeners(); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index b2fdc0f30..f74ca620d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; import 'package:wakelock/wakelock.dart'; import 'package:window_manager/window_manager.dart'; @@ -402,6 +403,15 @@ class ServerModel with ChangeNotifier { key: client.id.toString(), label: client.name, closable: false, + onTap: () { + if (client.hasUnreadChatMessage.value) { + client.hasUnreadChatMessage.value = false; + final chatModel = parent.target!.chatModel; + if (!chatModel.isShowChatPage) { + chatModel.toggleCMChatPage(client.id); + } + } + }, page: Desktop.buildConnectionCard(client))); Future.delayed(Duration.zero, () async { window_on_top(null); @@ -538,6 +548,8 @@ class Client { bool recording = false; bool disconnected = false; + RxBool hasUnreadChatMessage = false.obs; + Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); @@ -557,7 +569,7 @@ class Client { } Map toJson() { - final Map data = new Map(); + final Map data = {}; data['id'] = id; data['is_start'] = authorized; data['is_file_transfer'] = isFileTransfer; diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index c709d618a..592a28fcf 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -26,13 +26,11 @@ void main(List args) async { await initEnv(kAppTypeMain); for (var client in testClients) { gFFI.serverModel.clients.add(client); - gFFI.serverModel.tabController.add( - TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: buildConnectionCard(client)), - authorized: client.authorized); + gFFI.serverModel.tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: buildConnectionCard(client))); } runApp(GetMaterialApp(