diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 0e6be569e..c9b5292c2 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -7,10 +7,15 @@ import 'package:provider/provider.dart'; import '../../mobile/pages/home_page.dart'; +enum ChatPageType { + mobileMain, +} + class ChatPage extends StatelessWidget implements PageShape { late final ChatModel chatModel; + final ChatPageType? type; - ChatPage({ChatModel? chatModel}) { + ChatPage({ChatModel? chatModel, this.type}) { this.chatModel = chatModel ?? gFFI.chatModel; } @@ -22,7 +27,7 @@ class ChatPage extends StatelessWidget implements PageShape { @override final appBarActions = [ - PopupMenuButton( + PopupMenuButton( tooltip: "", icon: Icon(Icons.group), itemBuilder: (context) { @@ -31,8 +36,15 @@ class ChatPage extends StatelessWidget implements PageShape { return chatModel.messages.entries.map((entry) { final id = entry.key; final user = entry.value.chatUser; - return PopupMenuItem( - child: Text("${user.firstName} ${user.id}"), + return PopupMenuItem( + child: Row( + children: [ + Icon(id.isOut ? Icons.call_made : Icons.call_received, + color: MyTheme.accent) + .marginOnly(right: 6), + Text("${user.firstName} ${user.id}"), + ], + ), value: id, ); }).toList(); @@ -57,9 +69,9 @@ class ChatPage extends StatelessWidget implements PageShape { final chat = DashChat( onSend: chatModel.send, currentUser: chatModel.me, - messages: - chatModel.messages[chatModel.currentID]?.chatMessages ?? - [], + messages: chatModel + .messages[chatModel.currentKey]?.chatMessages ?? + [], inputOptions: InputOptions( focusNode: chatModel.inputNode, textController: chatModel.textController, @@ -128,7 +140,8 @@ class ChatPage extends StatelessWidget implements PageShape { return SelectionArea(child: chat); }), desktopType == DesktopType.cm || - chatModel.currentID == ChatModel.clientModeID + type != ChatPageType.mobileMain || + currentUser == null ? SizedBox.shrink() : Padding( padding: EdgeInsets.all(12), diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 25be23142..b560d1a41 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -100,14 +100,14 @@ class ConnectionManagerState extends State { gFFI.serverModel.tabController.onSelected = (client_id_str) { final client_id = int.tryParse(client_id_str); if (client_id != null) { - gFFI.chatModel.changeCurrentID(client_id); final client = gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id); if (client != null) { + gFFI.chatModel.changeCurrentID(MessageKey(client.peerId, client.id)); if (client.unreadChatMessageCount.value > 0) { Future.delayed(Duration.zero, () { client.unreadChatMessageCount.value = 0; - gFFI.chatModel.showChatPage(client.id); + gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id)); }); } windowManager.setTitle(getWindowNameWithId(client.peerId)); @@ -444,7 +444,8 @@ class _CmHeaderState extends State<_CmHeader> child: IconButton( onPressed: () => checkClickTime( client.id, - () => gFFI.chatModel.toggleCMChatPage(client.id), + () => gFFI.chatModel + .toggleCMChatPage(MessageKey(client.peerId, client.id)), ), icon: SvgPicture.asset('assets/chat2.svg'), splashRadius: kDesktopIconButtonSplashRadius, diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index f332f31dd..29151b9b8 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1413,7 +1413,8 @@ class _ChatMenuState extends State<_ChatMenu> { initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight); } - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.changeCurrentID( + MessageKey(widget.ffi.id, ChatModel.clientModeID)); widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); }); } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 2118b2b2f..d736e0ace 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -37,6 +37,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { class _ConnectionPageState extends State { /// Controller for the id input bar. final _idController = IDTextEditingController(); + final RxBool _idEmpty = true.obs; /// Update url. If it's not null, means an update is available. var _updateUrl = ''; @@ -60,6 +61,10 @@ class _ConnectionPageState extends State { if (_updateUrl.isNotEmpty) setState(() {}); }); } + + _idController.addListener(() { + _idEmpty.value = _idController.text.isEmpty; + }); } @override @@ -158,6 +163,14 @@ class _ConnectionPageState extends State { ), ), ), + Obx(() => Offstage( + offstage: _idEmpty.value, + child: IconButton( + onPressed: () { + _idController.clear(); + }, + icon: Icon(Icons.clear, color: MyTheme.darkGray)), + )), SizedBox( width: 60, height: 60, diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index f806c2576..308651d23 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -40,7 +40,7 @@ class _HomePageState extends State { _pages.clear(); _pages.add(ConnectionPage()); if (isAndroid) { - _pages.addAll([ChatPage(), ServerPage()]); + _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]); } _pages.add(SettingsPage()); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 028fa2ed9..61867daa2 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -351,8 +351,8 @@ class _RemotePageState extends State { color: Colors.white, icon: Icon(Icons.message), onPressed: () { - gFFI.chatModel - .changeCurrentID(ChatModel.clientModeID); + gFFI.chatModel.changeCurrentID(MessageKey( + widget.id, ChatModel.clientModeID)); gFFI.chatModel.toggleChatOverlay(); }, ) diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 8eadcaa2b..7ca3a9cb2 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -419,7 +420,8 @@ class ConnectionManager extends StatelessWidget { ? const SizedBox.shrink() : IconButton( onPressed: () { - gFFI.chatModel.changeCurrentID(client.id); + gFFI.chatModel.changeCurrentID( + MessageKey(client.peerId, client.id)); final bar = navigationBarKey.currentWidget; if (bar != null) { bar as BottomNavigationBar; diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 4eaf75d17..9ac65148b 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -20,6 +20,24 @@ import '../common/widgets/overlay.dart'; import '../main.dart'; import 'model.dart'; +class MessageKey { + final String peerId; + final int connId; + bool get isOut => connId != ChatModel.clientModeID; + + MessageKey(this.peerId, this.connId); + + @override + bool operator ==(other) { + return other is MessageKey && + other.peerId == peerId && + other.isOut == isOut; + } + + @override + int get hashCode => peerId.hashCode ^ isOut.hashCode; +} + class MessageBody { ChatUser chatUser; List chatMessages; @@ -61,15 +79,14 @@ class ChatModel with ChangeNotifier { firstName: translate("Me"), ); - late final Map _messages = {}..[clientModeID] = - MessageBody(me, []); + late final Map _messages = {}; - var _currentID = clientModeID; + MessageKey _currentKey = MessageKey('', clientModeID); late bool _isShowCMChatPage = false; - Map get messages => _messages; + Map get messages => _messages; - int get currentID => _currentID; + MessageKey get currentKey => _currentKey; bool get isShowCMChatPage => _isShowCMChatPage; @@ -119,15 +136,7 @@ class ChatModel with ChangeNotifier { ); } - ChatUser get currentUser { - final user = messages[currentID]?.chatUser; - if (user == null) { - _currentID = clientModeID; - return me; - } else { - return user; - } - } + ChatUser? get currentUser => _messages[_currentKey]?.chatUser; showChatIconOverlay({Offset offset = const Offset(200, 50)}) { if (chatIconOverlayEntry != null) { @@ -233,11 +242,11 @@ class ChatModel with ChangeNotifier { } } - showChatPage(int id) async { + showChatPage(MessageKey key) async { if (isDesktop) { if (isConnManager) { if (!_isShowCMChatPage) { - await toggleCMChatPage(id); + await toggleCMChatPage(key); } } else { if (_isChatOverlayHide()) { @@ -245,7 +254,7 @@ class ChatModel with ChangeNotifier { } } } else { - if (id == clientModeID) { + if (key.connId == clientModeID) { if (_isChatOverlayHide()) { await toggleChatOverlay(); } @@ -253,9 +262,9 @@ class ChatModel with ChangeNotifier { } } - toggleCMChatPage(int id) async { - if (gFFI.chatModel.currentID != id) { - gFFI.chatModel.changeCurrentID(id); + toggleCMChatPage(MessageKey key) async { + if (gFFI.chatModel.currentKey != key) { + gFFI.chatModel.changeCurrentID(key); } if (_isShowCMChatPage) { _isShowCMChatPage = !_isShowCMChatPage; @@ -273,23 +282,26 @@ class ChatModel with ChangeNotifier { } } - changeCurrentID(int id) { - if (_messages.containsKey(id)) { - _currentID = id; + changeCurrentID(MessageKey key) { + updateConnIdOfKey(key); + if (_messages.containsKey(key)) { + _currentKey = key; notifyListeners(); } else { - final client = parent.target?.serverModel.clients - .firstWhere((client) => client.id == id); - if (client == null) { - return debugPrint( - "Failed to changeCurrentID,remote user doesn't exist"); + String? peerName; + if (key.connId == clientModeID) { + peerName = parent.target?.ffiModel.pi.username; + } else { + peerName = parent.target?.serverModel.clients + .firstWhereOrNull((client) => client.peerId == key.peerId) + ?.name; } final chatUser = ChatUser( - id: client.peerId, - firstName: client.name, + id: key.peerId, + firstName: peerName, ); - _messages[id] = MessageBody(chatUser, []); - _currentID = id; + _messages[key] = MessageBody(chatUser, []); + _currentKey = key; notifyListeners(); } } @@ -304,23 +316,33 @@ class ChatModel with ChangeNotifier { if (desktopType == DesktopType.cm) { await showCmWindow(); } + String? peerId; + if (id == clientModeID) { + peerId = session.id; + } else { + peerId = session.serverModel.clients + .firstWhereOrNull((e) => e.id == id) + ?.peerId; + } + if (peerId == null) { + debugPrint("Failed to receive msg, peerId is null"); + return; + } + + final messagekey = MessageKey(peerId, id); // mobile: first message show overlay icon - if (!isDesktop && chatIconOverlayEntry == null) { + if (!isDesktop && chatIconOverlayEntry == null && id == clientModeID) { showChatIconOverlay(); } // show chat page - await showChatPage(id); - - int toId = currentID; - + await showChatPage(messagekey); late final ChatUser chatUser; if (id == clientModeID) { chatUser = ChatUser( firstName: session.ffiModel.pi.username, - id: session.id, + id: peerId, ); - toId = id; if (isDesktop) { if (Get.isRegistered()) { @@ -339,14 +361,18 @@ class ChatModel with ChangeNotifier { } } else { if (notSelected) { - UnreadChatCountState.find(session.id).value += 1; + UnreadChatCountState.find(peerId).value += 1; } } } } } else { - final client = - session.serverModel.clients.firstWhere((client) => client.id == id); + final client = session.serverModel.clients + .firstWhereOrNull((client) => client.id == id); + if (client == null) { + debugPrint("Failed to receive msg, client is null"); + return; + } if (isDesktop) { window_on_top(null); // disable auto jumpTo other tab when hasFocus, and mark unread message @@ -356,20 +382,13 @@ class ChatModel with ChangeNotifier { client.unreadChatMessageCount.value += 1; } else { parent.target?.serverModel.jumpTo(id); - toId = id; } - } else { - toId = id; } chatUser = ChatUser(id: client.peerId, firstName: client.name); } - - if (!_messages.containsKey(id)) { - _messages[id] = MessageBody(chatUser, []); - } - _messages[id]!.insert( + _currentKey = messagekey; + insertMessage(_currentKey, ChatMessage(text: text, user: chatUser, createdAt: DateTime.now())); - _currentID = toId; notifyListeners(); } @@ -379,17 +398,37 @@ class ChatModel with ChangeNotifier { return; } message.text = trimmedText; - _messages[_currentID]?.insert(message); - if (_currentID == clientModeID && parent.target != null) { + insertMessage(_currentKey, message); + if (_currentKey.connId == clientModeID && parent.target != null) { bind.sessionSendChat(sessionId: sessionId, text: message.text); } else { - bind.cmSendChat(connId: _currentID, msg: message.text); + bind.cmSendChat(connId: _currentKey.connId, msg: message.text); } notifyListeners(); inputNode.requestFocus(); } + insertMessage(MessageKey key, ChatMessage message) { + updateConnIdOfKey(key); + if (!_messages.containsKey(key)) { + _messages[key] = MessageBody(message.user, []); + } + _messages[key]?.insert(message); + } + + updateConnIdOfKey(MessageKey key) { + if (_messages.keys + .toList() + .firstWhereOrNull((e) => e == key && e.connId != key.connId) != + null) { + final old = _messages.remove(key); + if (old != null) { + _messages[key] = old; + } + } + } + close() { hideChatIconOverlay(); hideChatWindowOverlay();