123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- import 'dart:async';
- import 'package:dash_chat_2/dash_chat_2.dart';
- import 'package:desktop_multi_window/desktop_multi_window.dart';
- import 'package:draggable_float_widget/draggable_float_widget.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_hbb/common/shared_state.dart';
- import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
- import 'package:flutter_hbb/mobile/pages/home_page.dart';
- import 'package:flutter_hbb/models/platform_model.dart';
- import 'package:flutter_hbb/models/state_model.dart';
- import 'package:get/get.dart';
- import 'package:uuid/uuid.dart';
- import 'package:window_manager/window_manager.dart';
- import 'package:flutter_svg/flutter_svg.dart';
- import '../consts.dart';
- import '../common.dart';
- 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<ChatMessage> chatMessages;
- MessageBody(this.chatUser, this.chatMessages);
- void insert(ChatMessage cm) {
- chatMessages.insert(0, cm);
- }
- void clear() {
- chatMessages.clear();
- }
- }
- class ChatModel with ChangeNotifier {
- static final clientModeID = -1;
- OverlayEntry? chatIconOverlayEntry;
- OverlayEntry? chatWindowOverlayEntry;
- bool isConnManager = false;
- RxBool isWindowFocus = true.obs;
- BlockableOverlayState _blockableOverlayState = BlockableOverlayState();
- final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
- Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
- TextEditingController textController = TextEditingController();
- RxInt mobileUnreadSum = 0.obs;
- MessageKey? latestReceivedKey;
- Offset chatWindowPosition = Offset(20, 80);
- void setChatWindowPosition(Offset position) {
- chatWindowPosition = position;
- notifyListeners();
- }
- @override
- void dispose() {
- textController.dispose();
- super.dispose();
- }
- final ChatUser me = ChatUser(
- id: Uuid().v4().toString(),
- firstName: translate("Me"),
- );
- late final Map<MessageKey, MessageBody> _messages = {};
- MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value
- late bool _isShowCMSidePage = false;
- Map<MessageKey, MessageBody> get messages => _messages;
- MessageKey get currentKey => _currentKey;
- bool get isShowCMSidePage => _isShowCMSidePage;
- void setOverlayState(BlockableOverlayState blockableOverlayState) {
- _blockableOverlayState = blockableOverlayState;
- _blockableOverlayState.addMiddleBlockedListener((v) {
- if (!v) {
- isWindowFocus.value = false;
- if (isWindowFocus.value) {
- isWindowFocus.toggle();
- }
- }
- });
- }
- final WeakReference<FFI> parent;
- late final SessionID sessionId;
- late FocusNode inputNode;
- ChatModel(this.parent) {
- sessionId = parent.target!.sessionId;
- inputNode = FocusNode(
- onKey: (_, event) {
- bool isShiftPressed = event.isKeyPressed(LogicalKeyboardKey.shiftLeft);
- bool isEnterPressed = event.isKeyPressed(LogicalKeyboardKey.enter);
- // don't send empty messages
- if (isEnterPressed && isEnterPressed && textController.text.isEmpty) {
- return KeyEventResult.handled;
- }
- if (isEnterPressed && !isShiftPressed) {
- final ChatMessage message = ChatMessage(
- text: textController.text,
- user: me,
- createdAt: DateTime.now(),
- );
- send(message);
- textController.clear();
- return KeyEventResult.handled;
- }
- return KeyEventResult.ignored;
- },
- );
- }
- ChatUser? get currentUser => _messages[_currentKey]?.chatUser;
- showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
- if (chatIconOverlayEntry != null) {
- chatIconOverlayEntry!.remove();
- }
- // mobile check navigationBar
- final bar = navigationBarKey.currentWidget;
- if (bar != null) {
- if ((bar as BottomNavigationBar).currentIndex == 1) {
- return;
- }
- }
- final overlayState = _blockableOverlayState.state;
- if (overlayState == null) return;
- final overlay = OverlayEntry(builder: (context) {
- return DraggableFloatWidget(
- config: DraggableFloatWidgetBaseConfig(
- initPositionYInTop: false,
- initPositionYMarginBorder: 100,
- borderTopContainTopBar: true,
- ),
- child: FloatingActionButton(
- onPressed: () {
- if (chatWindowOverlayEntry == null) {
- showChatWindowOverlay();
- } else {
- hideChatWindowOverlay();
- }
- },
- backgroundColor: Theme.of(context).colorScheme.primary,
- child: SvgPicture.asset('assets/chat2.svg'),
- ),
- );
- });
- overlayState.insert(overlay);
- chatIconOverlayEntry = overlay;
- }
- hideChatIconOverlay() {
- if (chatIconOverlayEntry != null) {
- chatIconOverlayEntry!.remove();
- chatIconOverlayEntry = null;
- }
- }
- showChatWindowOverlay({Offset? chatInitPos}) {
- if (chatWindowOverlayEntry != null) return;
- isWindowFocus.value = true;
- _blockableOverlayState.setMiddleBlocked(true);
- final overlayState = _blockableOverlayState.state;
- if (overlayState == null) return;
- if (isMobile &&
- !gFFI.chatModel.currentKey.isOut && // not in remote page
- gFFI.chatModel.latestReceivedKey != null) {
- gFFI.chatModel.changeCurrentKey(gFFI.chatModel.latestReceivedKey!);
- gFFI.chatModel.mobileClearClientUnread(gFFI.chatModel.currentKey.connId);
- }
- final overlay = OverlayEntry(builder: (context) {
- return Listener(
- onPointerDown: (_) {
- if (!isWindowFocus.value) {
- isWindowFocus.value = true;
- _blockableOverlayState.setMiddleBlocked(true);
- }
- },
- child: DraggableChatWindow(
- position: chatInitPos ?? chatWindowPosition,
- width: 250,
- height: 350,
- chatModel: this));
- });
- overlayState.insert(overlay);
- chatWindowOverlayEntry = overlay;
- requestChatInputFocus();
- }
- hideChatWindowOverlay() {
- if (chatWindowOverlayEntry != null) {
- _blockableOverlayState.setMiddleBlocked(false);
- chatWindowOverlayEntry!.remove();
- chatWindowOverlayEntry = null;
- return;
- }
- }
- _isChatOverlayHide() =>
- ((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
- chatWindowOverlayEntry == null);
- toggleChatOverlay({Offset? chatInitPos}) {
- if (_isChatOverlayHide()) {
- gFFI.invokeMethod("enable_soft_keyboard", true);
- if (!(isDesktop || isWebDesktop)) {
- showChatIconOverlay();
- }
- showChatWindowOverlay(chatInitPos: chatInitPos);
- } else {
- hideChatIconOverlay();
- hideChatWindowOverlay();
- }
- }
- hideChatOverlay() {
- if (!_isChatOverlayHide()) {
- hideChatIconOverlay();
- hideChatWindowOverlay();
- }
- }
- showChatPage(MessageKey key) async {
- if (isDesktop) {
- if (isConnManager) {
- if (!_isShowCMSidePage) {
- await toggleCMChatPage(key);
- }
- } else {
- if (_isChatOverlayHide()) {
- await toggleChatOverlay();
- }
- }
- } else {
- if (key.connId == clientModeID) {
- if (_isChatOverlayHide()) {
- await toggleChatOverlay();
- }
- }
- }
- }
- toggleCMChatPage(MessageKey key) async {
- if (gFFI.chatModel.currentKey != key) {
- gFFI.chatModel.changeCurrentKey(key);
- }
- await toggleCMSidePage();
- }
- toggleCMFilePage() async {
- await toggleCMSidePage();
- }
- var _togglingCMSidePage = false; // protect order for await
- toggleCMSidePage() async {
- if (_togglingCMSidePage) return false;
- _togglingCMSidePage = true;
- if (_isShowCMSidePage) {
- _isShowCMSidePage = !_isShowCMSidePage;
- notifyListeners();
- await windowManager.show();
- await windowManager.setSizeAlignment(
- kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
- } else {
- final currentSelectedTab =
- gFFI.serverModel.tabController.state.value.selectedTabInfo;
- final client = parent.target?.serverModel.clients.firstWhereOrNull(
- (client) => client.id.toString() == currentSelectedTab.key);
- if (client != null) {
- client.unreadChatMessageCount.value = 0;
- }
- requestChatInputFocus();
- await windowManager.show();
- await windowManager.setSizeAlignment(
- kConnectionManagerWindowSizeOpenChat, Alignment.topRight);
- _isShowCMSidePage = !_isShowCMSidePage;
- notifyListeners();
- }
- _togglingCMSidePage = false;
- }
- changeCurrentKey(MessageKey key) {
- updateConnIdOfKey(key);
- 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;
- }
- if (!_messages.containsKey(key)) {
- final chatUser = ChatUser(
- id: key.peerId,
- firstName: peerName,
- );
- _messages[key] = MessageBody(chatUser, []);
- } else {
- if (peerName != null && peerName.isNotEmpty) {
- _messages[key]?.chatUser.firstName = peerName;
- }
- }
- _currentKey = key;
- notifyListeners();
- mobileClearClientUnread(key.connId);
- }
- 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;
- 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) {
- showChatIconOverlay();
- }
- // show chat page
- await showChatPage(messagekey);
- late final ChatUser chatUser;
- if (id == clientModeID) {
- chatUser = ChatUser(
- firstName: session.ffiModel.pi.username,
- id: peerId,
- );
- if (isDesktop) {
- if (Get.isRegistered<DesktopTabController>()) {
- DesktopTabController tabController = Get.find<DesktopTabController>();
- var index = tabController.state.value.tabs
- .indexWhere((e) => e.key == session.id);
- final notSelected =
- index >= 0 && tabController.state.value.selected != index;
- // minisized: top and switch tab
- // not minisized: add count
- if (await WindowController.fromWindowId(stateGlobal.windowId)
- .isMinimized()) {
- windowOnTop(stateGlobal.windowId);
- if (notSelected) {
- tabController.jumpTo(index);
- }
- } else {
- if (notSelected) {
- UnreadChatCountState.find(peerId).value += 1;
- }
- }
- }
- }
- } else {
- final client = session.serverModel.clients
- .firstWhereOrNull((client) => client.id == id);
- if (client == null) {
- debugPrint("Failed to receive msg, client is null");
- return;
- }
- if (isDesktop) {
- windowOnTop(null);
- // 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.unreadChatMessageCount.value += 1;
- } else {
- parent.target?.serverModel.jumpTo(id);
- }
- } else {
- if (HomePage.homeKey.currentState?.isChatPageCurrentTab != true ||
- _currentKey != messagekey) {
- client.unreadChatMessageCount.value += 1;
- mobileUpdateUnreadSum();
- }
- }
- chatUser = ChatUser(id: client.peerId, firstName: client.name);
- }
- insertMessage(messagekey,
- ChatMessage(text: text, user: chatUser, createdAt: DateTime.now()));
- if (id == clientModeID || _currentKey.peerId.isEmpty) {
- // client or invalid
- _currentKey = messagekey;
- mobileClearClientUnread(messagekey.connId);
- }
- latestReceivedKey = messagekey;
- notifyListeners();
- }
- send(ChatMessage message) {
- String trimmedText = message.text.trim();
- if (trimmedText.isEmpty) {
- return;
- }
- message.text = trimmedText;
- insertMessage(_currentKey, message);
- if (_currentKey.connId == clientModeID && parent.target != null) {
- bind.sessionSendChat(sessionId: sessionId, text: message.text);
- } else {
- 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 value = _messages.remove(key);
- if (value != null) {
- _messages[key] = value;
- }
- }
- if (_currentKey == key || _currentKey.peerId.isEmpty) {
- _currentKey = key; // hash != assign
- }
- }
- void mobileUpdateUnreadSum() {
- if (!isMobile) return;
- var sum = 0;
- parent.target?.serverModel.clients
- .map((e) => sum += e.unreadChatMessageCount.value)
- .toList();
- Future.delayed(Duration.zero, () {
- mobileUnreadSum.value = sum;
- });
- }
- void mobileClearClientUnread(int id) {
- if (!isMobile) return;
- final client = parent.target?.serverModel.clients
- .firstWhereOrNull((client) => client.id == id);
- if (client != null) {
- Future.delayed(Duration.zero, () {
- client.unreadChatMessageCount.value = 0;
- mobileUpdateUnreadSum();
- });
- }
- }
- close() {
- hideChatIconOverlay();
- hideChatWindowOverlay();
- notifyListeners();
- }
- resetClientMode() {
- _messages[clientModeID]?.clear();
- }
- void requestChatInputFocus() {
- Timer(Duration(milliseconds: 100), () {
- if (inputNode.hasListeners && inputNode.canRequestFocus) {
- inputNode.requestFocus();
- }
- });
- }
- void onVoiceCallWaiting() {
- _voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
- }
- void onVoiceCallStarted() {
- _voiceCallStatus.value = VoiceCallStatus.connected;
- if (isAndroid) {
- parent.target?.invokeMethod("on_voice_call_started");
- }
- }
- void onVoiceCallClosed(String reason) {
- _voiceCallStatus.value = VoiceCallStatus.notStarted;
- if (isAndroid) {
- // We can always invoke "on_voice_call_closed"
- // no matter if the `_voiceCallStatus` was `VoiceCallStatus.notStarted` or not.
- parent.target?.invokeMethod("on_voice_call_closed");
- }
- }
- void onVoiceCallIncoming() {
- if (isConnManager) {
- _voiceCallStatus.value = VoiceCallStatus.incoming;
- }
- }
- void closeVoiceCall() {
- bind.sessionCloseVoiceCall(sessionId: sessionId);
- }
- }
- enum VoiceCallStatus {
- notStarted,
- waitingForResponse,
- connected,
- // Connection manager only.
- incoming
- }
|