import 'dart:async'; import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:common_utils/common_utils.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:mime/mime.dart'; import 'package:openim_common/openim_common.dart'; import 'package:pull_to_refresh_new/pull_to_refresh.dart'; import 'package:rxdart/rxdart.dart'; import 'package:sprintf/sprintf.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:video_compress/video_compress.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; import 'package:openim_live/openim_live.dart'; import '../../core/controller/app_controller.dart'; import '../../core/controller/im_controller.dart'; import '../../core/im_callback.dart'; import '../../routes/app_navigator.dart'; import '../contacts/select_contacts/select_contacts_logic.dart'; import '../conversation/conversation_logic.dart'; import 'group_setup/group_member_list/group_member_list_logic.dart'; class ChatLogic extends SuperController { final imLogic = Get.find(); final appLogic = Get.find(); final conversationLogic = Get.find(); final cacheLogic = Get.find(); final downloadLogic = Get.find(); final inputCtrl = TextEditingController(); final focusNode = FocusNode(); final scrollController = ScrollController(); final refreshController = RefreshController(); bool playOnce = false; // 点击的当前视频只能播放一次 final forceCloseToolbox = PublishSubject(); final forceCloseMenuSub = PublishSubject(); final sendStatusSub = PublishSubject>(); late ConversationInfo conversationInfo; Message? searchMessage; final nickname = ''.obs; final faceUrl = ''.obs; Timer? typingTimer; final typing = false.obs; Timer? _debounce; Message? quoteMsg; final messageList = [].obs; final tempMessages = []; // 临时存放消息体,例如图片消息 var _lastCursorIndex = -1; final onlineStatus = false.obs; final onlineStatusDesc = ''.obs; Timer? onlineStatusTimer; final favoriteList = [].obs; final scaleFactor = Config.textScaleFactor.obs; final background = "".obs; final memberUpdateInfoMap = {}; final groupMessageReadMembers = >{}; final groupMutedStatus = 0.obs; final groupMemberRoleLevel = 1.obs; final muteEndTime = 0.obs; GroupInfo? groupInfo; GroupMembersInfo? groupMembersInfo; List ownerAndAdmin = []; final isInGroup = true.obs; final memberCount = 0.obs; final privateMessageList = []; final isInBlacklist = false.obs; final _audioPlayer = AudioPlayer(); final _currentPlayClientMsgID = "".obs; final isShowPopMenu = false.obs; final scrollingCacheMessageList = []; final announcement = ''.obs; late StreamSubscription conversationSub; late StreamSubscription memberAddSub; late StreamSubscription memberDelSub; late StreamSubscription joinedGroupAddedSub; late StreamSubscription joinedGroupDeletedSub; late StreamSubscription memberInfoChangedSub; late StreamSubscription groupInfoUpdatedSub; late StreamSubscription friendInfoChangedSub; StreamSubscription? userStatusChangedSub; StreamSubscription? selfInfoUpdatedSub; late StreamSubscription connectionSub; final syncStatus = IMSdkStatus.syncEnded.obs; int? lastMinSeq; final showCallingMember = false.obs; bool _isReceivedMessageWhenSyncing = false; bool _isStartSyncing = false; bool _isFirstLoad = true; final copyTextMap = {}; final revokedTextMessage = {}; String? groupOwnerID; final _pageSize = 40; RTCBridge? get rtcBridge => PackageBridge.rtcBridge; bool get rtcIsBusy => rtcBridge?.hasConnection == true; String? get userID => conversationInfo.userID; String? get groupID => conversationInfo.groupID; bool get isSingleChat => null != userID && userID!.trim().isNotEmpty; bool get isGroupChat => null != groupID && groupID!.trim().isNotEmpty; String get memberStr => isSingleChat ? "" : "($memberCount)"; String? get senderName => isSingleChat ? OpenIM.iMManager.userInfo.nickname : groupMembersInfo?.nickname; bool get isAdminOrOwner => groupMemberRoleLevel.value == GroupRoleLevel.admin || groupMemberRoleLevel.value == GroupRoleLevel.owner; final directionalUsers = [].obs; bool isCurrentChat(Message message) { var senderId = message.sendID; var receiverId = message.recvID; var groupId = message.groupID; var isCurSingleChat = message.isSingleChat && isSingleChat && (senderId == userID || senderId == OpenIM.iMManager.userID && receiverId == userID); var isCurGroupChat = message.isGroupChat && isGroupChat && groupID == groupId; return isCurSingleChat || isCurGroupChat; } void scrollBottom() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { scrollController.jumpTo(0); }); } Future> searchMediaMessage() async { final messageList = await OpenIM.iMManager.messageManager.searchLocalMessages( conversationID: conversationInfo.conversationID, messageTypeList: [MessageType.picture, MessageType.video], count: 500); return messageList.searchResultItems?.first.messageList?.reversed.toList() ?? []; } @override void onReady() { _readDraftText(); _queryUserOnlineStatus(); _resetGroupAtType(); _getInputState(); _clearUnreadCount(); scrollController.addListener(() { focusNode.unfocus(); }); super.onReady(); } @override void onInit() { var arguments = Get.arguments; conversationInfo = arguments['conversationInfo']; searchMessage = arguments['searchMessage']; nickname.value = conversationInfo.showName ?? ''; faceUrl.value = conversationInfo.faceURL ?? ''; _initChatConfig(); _initPlayListener(); _setSdkSyncDataListener(); conversationSub = imLogic.conversationChangedSubject.listen((value) { final obj = value.firstWhereOrNull((e) => e.conversationID == conversationInfo.conversationID); if (obj != null) { conversationInfo = obj; } }); imLogic.onRecvNewMessage = (Message message) async { if (isCurrentChat(message)) { if (message.contentType == MessageType.typing) { } else { if (!messageList.contains(message) && !scrollingCacheMessageList.contains(message)) { _isReceivedMessageWhenSyncing = true; if (isShowPopMenu.value || scrollController.offset != 0) { scrollingCacheMessageList.add(message); } else { messageList.add(message); scrollBottom(); } } } } }; imLogic.onRecvMessageRevoked = (RevokedInfo info) { var message = messageList.firstWhereOrNull((e) => e.clientMsgID == info.clientMsgID); message?.notificationElem = NotificationElem(detail: jsonEncode(info)); message?.contentType = MessageType.revokeMessageNotification; if (null != message) { messageList.refresh(); } }; imLogic.onRecvC2CReadReceipt = (List list) { try { for (var readInfo in list) { if (readInfo.userID == userID) { for (var e in messageList) { if (readInfo.msgIDList?.contains(e.clientMsgID) == true) { e.isRead = true; e.hasReadTime = _timestamp; } } } } messageList.refresh(); } catch (e) {} }; joinedGroupAddedSub = imLogic.joinedGroupAddedSubject.listen((event) { if (event.groupID == groupID) { isInGroup.value = true; _queryGroupInfo(); } }); joinedGroupDeletedSub = imLogic.joinedGroupDeletedSubject.listen((event) { if (event.groupID == groupID) { isInGroup.value = false; inputCtrl.clear(); } }); memberAddSub = imLogic.memberAddedSubject.listen((info) { var groupId = info.groupID; if (groupId == groupID) { _putMemberInfo([info]); } }); memberDelSub = imLogic.memberDeletedSubject.listen((info) { if (info.groupID == groupID && info.userID == OpenIM.iMManager.userID) { isInGroup.value = false; inputCtrl.clear(); } }); memberInfoChangedSub = imLogic.memberInfoChangedSubject.listen((info) { if (info.groupID == groupID) { if (info.userID == OpenIM.iMManager.userID) { muteEndTime.value = info.muteEndTime ?? 0; groupMemberRoleLevel.value = info.roleLevel ?? GroupRoleLevel.member; groupMembersInfo = info; (); } _putMemberInfo([info]); final index = ownerAndAdmin.indexWhere((element) => element.userID == info.userID); if (info.roleLevel == GroupRoleLevel.member) { if (index > -1) { ownerAndAdmin.removeAt(index); } } else if (info.roleLevel == GroupRoleLevel.admin || info.roleLevel == GroupRoleLevel.owner) { if (index == -1) { ownerAndAdmin.add(info); } else { ownerAndAdmin[index] = info; } } for (var msg in messageList) { if (msg.sendID == info.userID) { if (msg.isNotificationType) { final map = json.decode(msg.notificationElem!.detail!); final ntf = GroupNotification.fromJson(map); ntf.opUser?.nickname = info.nickname; ntf.opUser?.faceURL = info.faceURL; msg.notificationElem?.detail = jsonEncode(ntf); } else { msg.senderFaceUrl = info.faceURL; msg.senderNickname = info.nickname; } } } messageList.refresh(); } }); groupInfoUpdatedSub = imLogic.groupInfoUpdatedSubject.listen((value) { if (groupID == value.groupID) { groupInfo = value; nickname.value = value.groupName ?? ''; faceUrl.value = value.faceURL ?? ''; groupMutedStatus.value = value.status ?? 0; memberCount.value = value.memberCount ?? 0; } }); friendInfoChangedSub = imLogic.friendInfoChangedSubject.listen((value) { if (userID == value.userID) { nickname.value = value.getShowName(); faceUrl.value = value.faceURL ?? ''; for (var msg in messageList) { if (msg.sendID == value.userID) { msg.senderFaceUrl = value.faceURL; msg.senderNickname = value.nickname; } } messageList.refresh(); } }); selfInfoUpdatedSub = imLogic.selfInfoUpdatedSubject.listen((value) { for (var msg in messageList) { if (msg.sendID == value.userID) { msg.senderFaceUrl = value.faceURL; msg.senderNickname = value.nickname; } } messageList.refresh(); }); inputCtrl.addListener(() { sendTypingMsg(focus: true); if (_debounce?.isActive ?? false) _debounce?.cancel(); _debounce = Timer(1.seconds, () { sendTypingMsg(focus: false); }); }); focusNode.addListener(() { _lastCursorIndex = inputCtrl.selection.start; focusNodeChanged(focusNode.hasFocus); }); imLogic.onSignalingMessage = (value) { if (value.userID == userID) { messageList.add(value.message); scrollBottom(); } }; imLogic.inputStateChangedSubject.listen((value) { if (value.conversationID == conversationInfo.conversationID && value.userID == userID) { typing.value = value.platformIDs?.isNotEmpty == true; } }); super.onInit(); } Future chatSetup() => isSingleChat ? AppNavigator.startChatSetup(conversationInfo: conversationInfo) : AppNavigator.startGroupChatSetup(conversationInfo: conversationInfo); void _putMemberInfo(List? list) { list?.forEach((member) { memberUpdateInfoMap[member.userID!] = member; }); messageList.refresh(); } void sendTextMsg() async { var content = IMUtils.safeTrim(inputCtrl.text); if (content.isEmpty) return; Message message = await OpenIM.iMManager.messageManager.createTextMessage( text: content, ); _sendMessage(message); } Future sendPicture({required String path, bool sendNow = true}) async { final file = await IMUtils.compressImageAndGetFile(File(path)); var message = await OpenIM.iMManager.messageManager.createImageMessageFromFullPath( imagePath: file!.path, ); if (sendNow) { return _sendMessage(message); } else { messageList.add(message); tempMessages.add(message); } } void sendVoice(int duration, String path) async { var message = await OpenIM.iMManager.messageManager.createSoundMessageFromFullPath( soundPath: path, duration: duration, ); _sendMessage(message); } Future sendVideo( {required String videoPath, required String mimeType, required int duration, required String thumbnailPath, bool sendNow = true}) async { var d = duration > 1000.0 ? duration / 1000.0 : duration; var message = await OpenIM.iMManager.messageManager.createVideoMessageFromFullPath( videoPath: videoPath, videoType: mimeType, duration: d.toInt(), snapshotPath: thumbnailPath, ); if (sendNow) { return _sendMessage(message); } else { messageList.add(message); tempMessages.add(message); } } void sendFile({required String filePath, required String fileName}) async { var message = await OpenIM.iMManager.messageManager.createFileMessageFromFullPath( filePath: filePath, fileName: fileName, ); _sendMessage(message); } void sendLocation({ required dynamic location, }) async { var message = await OpenIM.iMManager.messageManager.createLocationMessage( latitude: location['latitude'], longitude: location['longitude'], description: location['description'], ); _sendMessage(message); } sendForwardRemarkMsg( String content, { String? userId, String? groupId, }) async { final message = await OpenIM.iMManager.messageManager.createTextMessage( text: content, ); _sendMessage(message, userId: userId, groupId: groupId); } sendForwardMsg( Message originalMessage, { String? userId, String? groupId, }) async { var message = await OpenIM.iMManager.messageManager.createForwardMessage( message: originalMessage, ); _sendMessage(message, userId: userId, groupId: groupId); } void sendTypingMsg({bool focus = false}) async { if (isSingleChat) { OpenIM.iMManager.conversationManager .changeInputStates(conversationID: conversationInfo.conversationID, focus: focus); } } void sendCarte({ required String userID, String? nickname, String? faceURL, }) async { var message = await OpenIM.iMManager.messageManager.createCardMessage( userID: userID, nickname: nickname!, faceURL: faceURL, ); _sendMessage(message); } void sendCustomMsg({ required String data, required String extension, required String description, }) async { var message = await OpenIM.iMManager.messageManager.createCustomMessage( data: data, extension: extension, description: description, ); _sendMessage(message); } Future _sendMessage( Message message, { String? userId, String? groupId, bool addToUI = true, }) { log('send : ${json.encode(message)}'); userId = IMUtils.emptyStrToNull(userId); groupId = IMUtils.emptyStrToNull(groupId); if (null == userId && null == groupId || userId == userID && userId != null || groupId == groupID && groupId != null) { if (addToUI) { messageList.add(message); scrollBottom(); } } Logger.print('uid:$userID userId:$userId gid:$groupID groupId:$groupId'); _reset(message); bool useOuterValue = null != userId || null != groupId; final recvUserID = useOuterValue ? userId : userID; message.recvID = recvUserID; return OpenIM.iMManager.messageManager .sendMessage( message: message, userID: recvUserID, groupID: useOuterValue ? groupId : groupID, offlinePushInfo: Config.offlinePushInfo, ) .then((value) => _sendSucceeded(message, value)) .catchError((error, _) => _senFailed(message, groupId, userId, error, _)) .whenComplete(() => _completed()); } void _sendSucceeded(Message oldMsg, Message newMsg) { Logger.print('message send success----'); oldMsg.update(newMsg); sendStatusSub.addSafely(MsgStreamEv( id: oldMsg.clientMsgID!, value: true, )); } void _senFailed(Message message, String? groupId, String? userId, error, stack) async { Logger.print('message send failed userID: $userId groupId:$groupId, catch error :$error $stack'); message.status = MessageStatus.failed; sendStatusSub.addSafely(MsgStreamEv( id: message.clientMsgID!, value: false, )); if (error is PlatformException) { int code = int.tryParse(error.code) ?? 0; if (isSingleChat) { int? customType; if (code == SDKErrorCode.hasBeenBlocked) { customType = CustomMessageType.blockedByFriend; } else if (code == SDKErrorCode.notFriend) { customType = CustomMessageType.deletedByFriend; } if (null != customType) { final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage(type: customType)) ..status = 2 ..isRead = true; if (userId != null) { if (userId == userID) { messageList.add(hintMessage); } } else { messageList.add(hintMessage); } OpenIM.iMManager.messageManager.insertSingleMessageToLocalStorage( message: hintMessage, receiverID: userId ?? userID, senderID: OpenIM.iMManager.userID, ); } } else { if ((code == SDKErrorCode.userIsNotInGroup || code == SDKErrorCode.groupDisbanded) && null == groupId) { final status = groupInfo?.status; final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage( type: status == 2 ? CustomMessageType.groupDisbanded : CustomMessageType.removedFromGroup)) ..status = 2 ..isRead = true; messageList.add(hintMessage); OpenIM.iMManager.messageManager.insertGroupMessageToLocalStorage( message: hintMessage, groupID: groupID, senderID: OpenIM.iMManager.userID, ); } } } } void _reset(Message message) { if (message.contentType == MessageType.text) { inputCtrl.clear(); } } void _completed() { messageList.refresh(); } void deleteMsg(Message message) async { LoadingView.singleton.wrap(asyncFunction: () => _deleteMessage(message)); } _deleteMessage(Message message) async { try { await OpenIM.iMManager.messageManager .deleteMessageFromLocalAndSvr( conversationID: conversationInfo.conversationID, clientMsgID: message.clientMsgID!, ) .then((value) => privateMessageList.remove(message)) .then((value) => messageList.remove(message)); } catch (e) { await OpenIM.iMManager.messageManager .deleteMessageFromLocalStorage( conversationID: conversationInfo.conversationID, clientMsgID: message.clientMsgID!, ) .then((value) => privateMessageList.remove(message)) .then((value) => messageList.remove(message)); } } void forward(Message? message) async { final result = await AppNavigator.startSelectContacts( action: SelAction.forward, ex: null != message ? IMUtils.parseMsg(message) : null, ); if (null != result) { final checkedList = result['checkedList']; for (var info in checkedList) { final userID = IMUtils.convertCheckedToUserID(info); final groupID = IMUtils.convertCheckedToGroupID(info); if (null != message) { sendForwardMsg(message, userId: userID, groupId: groupID); } } } } void markMessageAsRead(Message message, bool visible) async { Logger.print('markMessageAsRead: ${message.textElem?.content}, $visible'); if (visible && message.contentType! < 1000 && message.contentType! != MessageType.voice) { var data = IMUtils.parseCustomMessage(message); if (null != data && data['viewType'] == CustomMessageType.call) { Logger.print('markMessageAsRead: call message $data'); return; } _markMessageAsRead(message); } } _markMessageAsRead(Message message) async { if (!message.isRead! && message.sendID != OpenIM.iMManager.userID) { try { Logger.print('mark conversation message as read:${message.clientMsgID!} ${message.isRead}'); await OpenIM.iMManager.conversationManager .markConversationMessageAsRead(conversationID: conversationInfo.conversationID); } catch (e) { Logger.print('failed to send group message read receipt: ${message.clientMsgID} ${message.isRead}'); } finally { message.isRead = true; message.hasReadTime = _timestamp; messageList.refresh(); } } } _clearUnreadCount() { if (conversationInfo.unreadCount > 0) { OpenIM.iMManager.conversationManager .markConversationMessageAsRead(conversationID: conversationInfo.conversationID); } } void _getInputState() async { if (conversationInfo.isSingleChat) { final result = await OpenIM.iMManager.conversationManager.getInputStates(conversationInfo.conversationID, userID!); typing.value = result?.isNotEmpty == true; } } void _changeInputStatus(bool focus) async { if (conversationInfo.isSingleChat) { await OpenIM.iMManager.conversationManager .changeInputStates(conversationID: conversationInfo.conversationID, focus: focus); } } void closeToolbox() { forceCloseToolbox.addSafely(true); } void onTapLocation() async { var location = await Get.to( const ChatWebViewMap(host: Config.locationHost, webKey: Config.webKey, webServerKey: Config.webServerKey), transition: Transition.cupertino, popGesture: true, ); if (null != location) { Logger.print(location); sendLocation(location: location); } } void onTapAlbum() async { final List? assets = await AssetPicker.pickAssets(Get.context!, pickerConfig: AssetPickerConfig( sortPathsByModifiedDate: true, filterOptions: PMFilter.defaultValue(containsPathModified: true), selectPredicate: (_, entity, isSelected) async { if (entity.type == AssetType.image) { if (await allowSendImageType(entity)) { return true; } IMViews.showToast(StrRes.supportsTypeHint); return false; } if (entity.videoDuration > const Duration(seconds: 5 * 60)) { IMViews.showToast(sprintf(StrRes.selectVideoLimit, [5]) + StrRes.minute); return false; } return true; })); if (null != assets) { for (var asset in assets) { await _handleAssets(asset, sendNow: false); } for (var msg in tempMessages) { await _sendMessage(msg, addToUI: false); } tempMessages.clear(); } } void onTapCamera() async { final AssetEntity? entity = await CameraPicker.pickFromCamera( Get.context!, locale: Get.locale, pickerConfig: CameraPickerConfig( enableAudio: true, enableRecording: true, enableScaledPreview: false, maximumRecordingDuration: 60.seconds, onMinimumRecordDurationNotMet: () { IMViews.showToast(StrRes.tapTooShort); }, ), ); _handleAssets(entity); } void onTapFile() async { await FilePicker.platform.clearTemporaryFiles(); FilePickerResult? result = await FilePicker.platform.pickFiles( allowMultiple: true, ); if (result != null) { for (var file in result.files) { String? mimeType = lookupMimeType(file.name); if (mimeType != null) { if (IMUtils.allowImageType(mimeType)) { sendPicture(path: file.path!); continue; } else if (mimeType.contains('video/')) { try { final videoPath = file.path!; final mediaInfo = await VideoCompress.getMediaInfo(videoPath); var thumbnailFile = await VideoCompress.getFileThumbnail( videoPath, quality: 60, ); sendVideo( videoPath: videoPath, mimeType: mimeType, duration: mediaInfo.duration!.toInt(), thumbnailPath: thumbnailFile.path, ); continue; } catch (e, s) { Logger.print('e :$e s:$s'); } } } sendFile(filePath: file.path!, fileName: file.name); } } else {} } Future allowSendImageType(AssetEntity entity) async { final mimeType = await entity.mimeTypeAsync; return IMUtils.allowImageType(mimeType); } void onTapCarte() async { var result = await AppNavigator.startSelectContacts( action: SelAction.carte, ); if (result is UserInfo || result is FriendInfo) { sendCarte( userID: result.userID!, nickname: result.nickname, faceURL: result.faceURL, ); } } Future _handleAssets(AssetEntity? asset, {bool sendNow = true}) async { if (null != asset) { Logger.print('--------assets type-----${asset.type} create time: ${asset.createDateTime}'); final originalFile = await asset.file; final originalPath = originalFile!.path; var path = originalPath.toLowerCase().endsWith('.gif') ? originalPath : originalFile.path; Logger.print('--------assets path-----$path'); switch (asset.type) { case AssetType.image: await sendPicture(path: path, sendNow: sendNow); break; case AssetType.video: var thumbnailFile = await IMUtils.getVideoThumbnail(File(path)); LoadingView.singleton.show(); final file = await IMUtils.compressVideoAndGetFile(File(path)); LoadingView.singleton.dismiss(); await sendVideo( videoPath: file!.path, mimeType: asset.mimeType ?? IMUtils.getMediaType(path) ?? '', duration: asset.duration, thumbnailPath: thumbnailFile.path, sendNow: sendNow, ); break; default: break; } if (Platform.isIOS) { originalFile.deleteSync(); } } } void onTapDirectionalMessage() async { if (null != groupInfo) { final list = await AppNavigator.startGroupMemberList( groupInfo: groupInfo!, opType: GroupMemberOpType.call, ); if (list is List) { directionalUsers.assignAll(list); } } } TextSpan? directionalText() { if (directionalUsers.isNotEmpty) { final temp = []; for (var e in directionalUsers) { final r = TextSpan( text: '${e.nickname ?? ''} ${directionalUsers.last == e ? '' : ','} ', style: Styles.ts_0089FF_14sp, ); temp.add(r); } return TextSpan( text: '${StrRes.directedTo}:', style: Styles.ts_8E9AB0_14sp, children: temp, ); } return null; } void onClearDirectional() { directionalUsers.clear(); } void parseClickEvent(Message msg) async { log('parseClickEvent:${jsonEncode(msg)}'); if (msg.contentType == MessageType.custom) { var data = msg.customElem!.data; var map = json.decode(data!); var customType = map['customType']; if (CustomMessageType.call == customType && !isInBlacklist.value) { } else if (CustomMessageType.meeting == customType) { joinMeeting(msg); } else if (CustomMessageType.tag == customType) { final data = map['data']; if (null != data['soundElem']) { final soundElem = SoundElem.fromJson(data['soundElem']); msg.soundElem = soundElem; _playVoiceMessage(msg); } } return; } if (msg.contentType == MessageType.voice) { _playVoiceMessage(msg); _markMessageAsRead(msg); return; } IMUtils.parseClickEvent( msg, onViewUserInfo: (userInfo) { viewUserInfo(userInfo, isCard: msg.isCardType); }, meetingItemClick: joinMeeting, onForward: () => forward(msg), ); } void onLongPressLeftAvatar(Message message) { if (isInvalidGroup) return; if (isGroupChat) { var uid = message.sendID!; var cursor = inputCtrl.selection.base.offset; if (!focusNode.hasFocus) { focusNode.requestFocus(); cursor = _lastCursorIndex; } if (cursor < 0) cursor = 0; var start = inputCtrl.text.substring(0, cursor); var end = inputCtrl.text.substring(cursor); var at = '@$uid '; inputCtrl.text = '$start$at$end'; Logger.print('start:$start end:$end at:$at content:${inputCtrl.text}'); inputCtrl.selection = TextSelection.collapsed(offset: '$start$at'.length); _lastCursorIndex = inputCtrl.selection.start; } } void onTapLeftAvatar(Message message) { viewUserInfo(UserInfo() ..userID = message.sendID ..nickname = message.senderNickname ..faceURL = message.senderFaceUrl); } void onTapRightAvatar() { viewUserInfo(OpenIM.iMManager.userInfo); } void viewUserInfo(UserInfo userInfo, {bool isCard = false}) { if (isGroupChat && !isAdminOrOwner && !isCard) { if (groupInfo!.lookMemberInfo != 1) { AppNavigator.startUserProfilePane( userID: userInfo.userID!, nickname: userInfo.nickname, faceURL: userInfo.faceURL, groupID: groupID, offAllWhenDelFriend: isSingleChat, ); } } else { AppNavigator.startUserProfilePane( userID: userInfo.userID!, nickname: userInfo.nickname, faceURL: userInfo.faceURL, groupID: groupID, offAllWhenDelFriend: isSingleChat, forceCanAdd: isCard, ); } } void clickLinkText(url, type) async { if (await canLaunch(url)) { await launch(url); } } void _readDraftText() { var draftText = Get.arguments['draftText']; Logger.print('readDraftText:$draftText'); if (null != draftText && "" != draftText) { var map = json.decode(draftText!); String text = map['text']; Map atMap = map['at']; Logger.print('text:$text atMap:$atMap'); inputCtrl.text = text; inputCtrl.selection = TextSelection.fromPosition(TextPosition( offset: text.length, )); if (text.isNotEmpty) { focusNode.requestFocus(); } } } exit() async { if (isShowPopMenu.value) { forceCloseMenuSub.add(true); return false; } Get.back(); return true; } void focusNodeChanged(bool hasFocus) { _changeInputStatus(hasFocus); if (hasFocus) { Logger.print('focus:$hasFocus'); scrollBottom(); } } void copy(Message message) { final content = copyTextMap[message.clientMsgID] ?? message.textElem?.content; if (null != content) { IMUtils.copy(text: content.replaceAll('\u200B', '')); } } Message indexOfMessage(int index, {bool calculate = true}) => IMUtils.calChatTimeInterval( messageList, calculate: calculate, ).reversed.elementAt(index); ValueKey itemKey(Message message) => ValueKey(message.clientMsgID!); @override void onClose() { sendTypingMsg(); _clearUnreadCount(); _unSubscribeUserOnlineStatus(); inputCtrl.dispose(); focusNode.dispose(); _audioPlayer.dispose(); forceCloseToolbox.close(); conversationSub.cancel(); sendStatusSub.close(); memberAddSub.cancel(); memberDelSub.cancel(); memberInfoChangedSub.cancel(); groupInfoUpdatedSub.cancel(); friendInfoChangedSub.cancel(); userStatusChangedSub?.cancel(); selfInfoUpdatedSub?.cancel(); forceCloseMenuSub.close(); joinedGroupAddedSub.cancel(); joinedGroupDeletedSub.cancel(); connectionSub.cancel(); _debounce?.cancel(); super.onClose(); } String? getShowTime(Message message) { if (message.exMap['showTime'] == true) { return IMUtils.getChatTimeline(message.sendTime!); } return null; } void clearAllMessage() { messageList.clear(); } void onAddEmoji(String emoji) { var input = inputCtrl.text; if (_lastCursorIndex != -1 && input.isNotEmpty) { var part1 = input.substring(0, _lastCursorIndex); var part2 = input.substring(_lastCursorIndex); inputCtrl.text = '$part1$emoji$part2'; _lastCursorIndex = _lastCursorIndex + emoji.length; } else { inputCtrl.text = '$input$emoji'; _lastCursorIndex = emoji.length; } inputCtrl.selection = TextSelection.fromPosition(TextPosition( offset: _lastCursorIndex, )); } void onDeleteEmoji() { final input = inputCtrl.text; final regexEmoji = emojiFaces.keys.toList().join('|').replaceAll('[', '\\[').replaceAll(']', '\\]'); final list = [regexEmoji]; final pattern = '(${list.toList().join('|')})'; final emojiReg = RegExp(regexEmoji); var reg = RegExp(pattern); var cursor = _lastCursorIndex; if (cursor == 0) return; Match? match; if (reg.hasMatch(input)) { for (var m in reg.allMatches(input)) { var matchText = m.group(0)!; var start = m.start; var end = start + matchText.length; if (end == cursor) { match = m; break; } } } var matchText = match?.group(0); if (matchText != null) { var start = match!.start; var end = start + matchText.length; if (emojiReg.hasMatch(matchText)) { inputCtrl.text = input.replaceRange(start, end, ""); cursor = start; } else { inputCtrl.text = input.replaceRange(cursor - 1, cursor, ''); --cursor; } } else { inputCtrl.text = input.replaceRange(cursor - 1, cursor, ''); --cursor; } _lastCursorIndex = cursor; } String? get subTile => typing.value ? StrRes.typing : onlineStatusDesc.value; bool showOnlineStatus() => !typing.value && onlineStatusDesc.isNotEmpty; bool enabledReadStatus(Message message) { if (message.isNotificationType) { return false; } return true; } void favoriteManage() => AppNavigator.startFavoriteMange(); void addEmoji(Message message) { if (message.contentType == MessageType.picture) { var url = message.pictureElem?.sourcePicture?.url; var width = message.pictureElem?.sourcePicture?.width; var height = message.pictureElem?.sourcePicture?.height; cacheLogic.addFavoriteFromUrl(url, width, height); IMViews.showToast(StrRes.addSuccessfully); } else if (message.contentType == MessageType.customFace) { var index = message.faceElem?.index; var data = message.faceElem?.data; if (-1 != index) { } else if (null != data) { var map = json.decode(data); var url = map['url']; var width = map['width']; var height = map['height']; cacheLogic.addFavoriteFromUrl(url, width, height); IMViews.showToast(StrRes.addSuccessfully); } } } void sendFavoritePic(int index, String url) async { var emoji = cacheLogic.favoriteList.elementAt(index); var message = await OpenIM.iMManager.messageManager.createFaceMessage( data: json.encode({'url': emoji.url, 'width': emoji.width, 'height': emoji.height}), ); _sendMessage(message); } void _initChatConfig() async { scaleFactor.value = DataSp.getChatFontSizeFactor(); var path = DataSp.getChatBackground(otherId) ?? ''; if (path.isNotEmpty && (await File(path).exists())) { background.value = path; } } String get otherId => isSingleChat ? userID! : groupID!; void failedResend(Message message) { Logger.print('failedResend: ${message.clientMsgID}'); if (message.status == MessageStatus.sending) { return; } sendStatusSub.addSafely(MsgStreamEv( id: message.clientMsgID!, value: true, )); Logger.print('failedResending: ${message.clientMsgID}'); _sendMessage(message..status = MessageStatus.sending, addToUI: false); } int readTime(Message message) { var isPrivate = message.attachedInfoElem?.isPrivateChat ?? false; var burnDuration = message.attachedInfoElem?.burnDuration ?? 30; burnDuration = burnDuration > 0 ? burnDuration : 30; if (isPrivate) { var hasReadTime = message.hasReadTime ?? 0; if (hasReadTime > 0) { var end = hasReadTime + (burnDuration * 1000); var diff = (end - _timestamp) ~/ 1000; if (diff > 0) { privateMessageList.addIf(() => !privateMessageList.contains(message), message); } return diff < 0 ? 0 : diff; } } return 0; } static int get _timestamp => DateTime.now().millisecondsSinceEpoch; void destroyMsg() { for (var message in privateMessageList) { OpenIM.iMManager.messageManager.deleteMessageFromLocalAndSvr( conversationID: conversationInfo.conversationID, clientMsgID: message.clientMsgID!, ); } } Future _queryMyGroupMemberInfo() async { if (!isGroupChat) { return; } var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( groupID: groupID!, userIDList: [OpenIM.iMManager.userID], ); groupMembersInfo = list.firstOrNull; groupMemberRoleLevel.value = groupMembersInfo?.roleLevel ?? GroupRoleLevel.member; muteEndTime.value = groupMembersInfo?.muteEndTime ?? 0; if (null != groupMembersInfo) { memberUpdateInfoMap[OpenIM.iMManager.userID] = groupMembersInfo!; } return; } Future _queryOwnerAndAdmin() async { if (isGroupChat) { ownerAndAdmin = await OpenIM.iMManager.groupManager.getGroupMemberList(groupID: groupID!, filter: 5, count: 20); } return; } void _isJoinedGroup() async { if (!isGroupChat) { return; } isInGroup.value = await OpenIM.iMManager.groupManager.isJoinedGroup( groupID: groupID!, ); if (!isInGroup.value) { return; } _queryGroupInfo(); _queryOwnerAndAdmin(); } void _queryGroupInfo() async { if (!isGroupChat) { return; } var list = await OpenIM.iMManager.groupManager.getGroupsInfo( groupIDList: [groupID!], ); groupInfo = list.firstOrNull; groupOwnerID = groupInfo?.ownerUserID; groupMutedStatus.value = groupInfo?.status ?? 0; if (null != groupInfo?.memberCount) { memberCount.value = groupInfo!.memberCount!; } _queryMyGroupMemberInfo(); } bool get havePermissionMute => isGroupChat && (groupInfo?.ownerUserID == OpenIM.iMManager.userID /*|| groupMembersInfo?.roleLevel == 2*/ ); bool isNotificationType(Message message) => message.contentType! >= 1000; Map getAtMapping(Message message) { return {}; } void _queryUserOnlineStatus() { if (isSingleChat) { OpenIM.iMManager.userManager.subscribeUsersStatus([userID!]).then((value) { final status = value.firstWhereOrNull((element) => element.userID == userID); _configUserStatusChanged(status); }); userStatusChangedSub = imLogic.userStatusChangedSubject.listen((value) { if (value.userID == userID) { _configUserStatusChanged(value); } }); } } void _unSubscribeUserOnlineStatus() { if (isSingleChat) { OpenIM.iMManager.userManager.unsubscribeUsersStatus([userID!]); } } void _configUserStatusChanged(UserStatusInfo? status) { if (status != null) { onlineStatus.value = status.status == 1; onlineStatusDesc.value = status.status == 0 ? StrRes.offline : _onlineStatusDes(status.platformIDs!) + StrRes.online; } } String _onlineStatusDes(List plamtforms) { var des = []; for (final platform in plamtforms) { switch (platform) { case 1: des.add('iOS'); break; case 2: des.add('Android'); break; case 3: des.add('Windows'); break; case 4: des.add('Mac'); break; case 5: des.add('Web'); break; case 6: des.add('mini_web'); break; case 7: des.add('Linux'); break; case 8: des.add('Android_pad'); break; case 9: des.add('iPad'); break; default: } } return des.join('/'); } void _checkInBlacklist() async { if (userID != null) { var list = await OpenIM.iMManager.friendshipManager.getBlacklist(); var user = list.firstWhereOrNull((e) => e.userID == userID); isInBlacklist.value = user != null; } } bool isExceed24H(Message message) { int milliseconds = message.sendTime!; return !DateUtil.isToday(milliseconds); } bool isPlaySound(Message message) { return _currentPlayClientMsgID.value == message.clientMsgID!; } void _initPlayListener() { _audioPlayer.playerStateStream.listen((state) { switch (state.processingState) { case ProcessingState.idle: case ProcessingState.loading: case ProcessingState.buffering: case ProcessingState.ready: break; case ProcessingState.completed: _currentPlayClientMsgID.value = ""; break; } }); } void _playVoiceMessage(Message message) async { var isClickSame = _currentPlayClientMsgID.value == message.clientMsgID; if (_audioPlayer.playerState.playing) { _currentPlayClientMsgID.value = ""; _audioPlayer.stop(); } if (!isClickSame) { bool isValid = await _initVoiceSource(message); if (isValid) { _audioPlayer.setVolume(rtcIsBusy ? 0 : 1.0); _audioPlayer.seek(Duration.zero); _audioPlayer.play(); _currentPlayClientMsgID.value = message.clientMsgID!; } } } void stopVoice() { if (_audioPlayer.playerState.playing) { _currentPlayClientMsgID.value = ''; _audioPlayer.stop(); } } Future _initVoiceSource(Message message) async { bool isReceived = message.sendID != OpenIM.iMManager.userID; String? path = message.soundElem?.soundPath; String? url = message.soundElem?.sourceUrl; bool isExistSource = false; if (isReceived) { if (null != url && url.trim().isNotEmpty) { isExistSource = true; _audioPlayer.setUrl(url); } } else { bool existFile = false; if (path != null && path.trim().isNotEmpty) { var file = File(path); existFile = await file.exists(); } if (existFile) { isExistSource = true; _audioPlayer.setFilePath(path!); } else if (null != url && url.trim().isNotEmpty) { isExistSource = true; _audioPlayer.setUrl(url); } } return isExistSource; } void onPopMenuShowChanged(show) { isShowPopMenu.value = show; if (!show && scrollingCacheMessageList.isNotEmpty) { messageList.addAll(scrollingCacheMessageList); scrollingCacheMessageList.clear(); } } String? getNewestNickname(Message message) { if (isSingleChat) null; return message.senderNickname; } String? getNewestFaceURL(Message message) { return message.senderFaceUrl; } bool get isInvalidGroup => !isInGroup.value && isGroupChat; void _resetGroupAtType() { if (conversationInfo.groupAtType != GroupAtType.atNormal) { OpenIM.iMManager.conversationManager.resetConversationGroupAtType( conversationID: conversationInfo.conversationID, ); } } void revokeMsgV2(Message message) async { late bool canRevoke; if (isGroupChat) { if (message.sendID == OpenIM.iMManager.userID) { canRevoke = true; } else { var list = await LoadingView.singleton .wrap(asyncFunction: () => OpenIM.iMManager.groupManager.getGroupOwnerAndAdmin(groupID: groupID!)); var sender = list.firstWhereOrNull((e) => e.userID == message.sendID); var revoker = list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID); if (revoker != null && sender == null) { canRevoke = true; } else if (revoker == null && sender != null) { canRevoke = false; } else if (revoker != null && sender != null) { if (revoker.roleLevel == sender.roleLevel) { canRevoke = false; } else if (revoker.roleLevel == GroupRoleLevel.owner) { canRevoke = true; } else { canRevoke = false; } } else { canRevoke = false; } } } else { if (message.sendID == OpenIM.iMManager.userID) { canRevoke = true; } } if (canRevoke) { try { await LoadingView.singleton.wrap( asyncFunction: () => OpenIM.iMManager.messageManager.revokeMessage( conversationID: conversationInfo.conversationID, clientMsgID: message.clientMsgID!, ), ); message.contentType = MessageType.revokeMessageNotification; message.notificationElem = NotificationElem(detail: jsonEncode(_buildRevokeInfo(message))); messageList.refresh(); } catch (e) { IMViews.showToast(e.toString()); } } else { IMViews.showToast('no permission'); } } RevokedInfo _buildRevokeInfo(Message message) { return RevokedInfo.fromJson({ 'revokerID': OpenIM.iMManager.userInfo.userID, 'revokerRole': 0, 'revokerNickname': OpenIM.iMManager.userInfo.nickname, 'clientMsgID': message.clientMsgID, 'revokeTime': 0, 'sourceMessageSendTime': 0, 'sourceMessageSendID': message.sendID, 'sourceMessageSenderNickname': message.senderNickname, 'sessionType': message.sessionType, }); } bool showCopyMenu(Message message) { return message.isTextType; } bool showDelMenu(Message message) { return true; } bool showForwardMenu(Message message) { if (message.status != MessageStatus.succeeded) { return false; } if (message.isNotificationType) { return false; } return true; } bool showReplyMenu(Message message) { if (message.status != MessageStatus.succeeded) { return false; } return message.isTextType || message.isVideoType || message.isPictureType || message.isLocationType || message.isFileType || message.isCardType || message.isCustomFaceType; } bool showRevokeMenu(Message message) { if (message.status != MessageStatus.succeeded || message.isNotificationType || isExceed24H(message) && isSingleChat) { return false; } if (isGroupChat) { if (groupMemberRoleLevel.value == GroupRoleLevel.owner || (groupMemberRoleLevel.value == GroupRoleLevel.admin && ownerAndAdmin.firstWhereOrNull((element) => element.userID == message.sendID) == null)) { return true; } } if (message.sendID == OpenIM.iMManager.userID) { if (DateTime.now().millisecondsSinceEpoch - (message.sendTime ??= 0) < (1000 * 60 * 5)) { return true; } } return false; } bool showAddEmojiMenu(Message message) { if (message.status != MessageStatus.succeeded) { return false; } return message.contentType == MessageType.picture || message.contentType == MessageType.customFace; } WillPopCallback? willPop() { return isShowPopMenu.value ? () async => exit() : null; } void expandCallingMemberPanel() { showCallingMember.value = !showCallingMember.value; } void call() { if (rtcIsBusy) { IMViews.showToast(StrRes.callingBusy); return; } IMViews.openIMCallSheet(nickname.value, (index) { imLogic.call( callObj: CallObj.single, callType: index == 0 ? CallType.audio : CallType.video, inviteeUserIDList: [if (isSingleChat) userID!], ); }); } void onScrollToTop() { if (scrollingCacheMessageList.isNotEmpty) { messageList.addAll(scrollingCacheMessageList); scrollingCacheMessageList.clear(); } } String get markText { String? phoneNumber = imLogic.userInfo.value.phoneNumber; if (phoneNumber != null) { int start = phoneNumber.length > 4 ? phoneNumber.length - 4 : 0; final sub = phoneNumber.substring(start); return "${OpenIM.iMManager.userInfo.nickname!}$sub"; } return OpenIM.iMManager.userInfo.nickname ?? ''; } bool isFailedHintMessage(Message message) { if (message.contentType == MessageType.custom) { var data = message.customElem!.data; var map = json.decode(data!); var customType = map['customType']; return customType == CustomMessageType.deletedByFriend || customType == CustomMessageType.blockedByFriend; } return false; } void sendFriendVerification() => AppNavigator.startSendVerificationApplication(userID: userID); void _setSdkSyncDataListener() { connectionSub = imLogic.imSdkStatusPublishSubject.listen((value) { syncStatus.value = value.status; if (value.status == IMSdkStatus.syncStart) { _isStartSyncing = true; } else if (value.status == IMSdkStatus.syncEnded) { if (/*_isReceivedMessageWhenSyncing &&*/ _isStartSyncing) { _isReceivedMessageWhenSyncing = false; _isStartSyncing = false; _isFirstLoad = true; _loadHistoryForSyncEnd(); } } else if (value.status == IMSdkStatus.syncFailed) { _isReceivedMessageWhenSyncing = false; _isStartSyncing = false; } }); } bool get isSyncFailed => syncStatus.value == IMSdkStatus.syncFailed; String? get syncStatusStr { switch (syncStatus.value) { case IMSdkStatus.syncStart: case IMSdkStatus.synchronizing: return StrRes.synchronizing; case IMSdkStatus.syncFailed: return StrRes.syncFailed; default: return null; } } bool showBubbleBg(Message message) { return !isNotificationType(message) && !isFailedHintMessage(message) && !isRevokeMessage(message); } bool isRevokeMessage(Message message) { return message.contentType == MessageType.revokeMessageNotification; } void markRevokedMessage(Message message) { if (message.contentType == MessageType.text) { revokedTextMessage[message.clientMsgID!] = jsonEncode(message); } } Future _fetchHistoryMessages() { Logger.print( '_fetchHistoryMessages: is first load: $_isFirstLoad, last client id: ${_isFirstLoad ? null : messageList.firstOrNull?.clientMsgID}'); return OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList( conversationID: conversationInfo.conversationID, count: _pageSize, startMsg: _isFirstLoad ? null : messageList.firstOrNull, ); } Future onScrollToBottomLoad() async { late List list; final result = await _fetchHistoryMessages(); if (result.messageList == null || result.messageList!.isEmpty) { _getGroupInfoAfterLoadMessage(); return false; } list = result.messageList!; if (_isFirstLoad) { _isFirstLoad = false; // remove the message that has been timed down list.removeWhere((msg) => _isBeDeleteMessage(msg)); messageList.assignAll(list); scrollBottom(); _getGroupInfoAfterLoadMessage(); } else { list.removeWhere((msg) => _isBeDeleteMessage(msg)); messageList.insertAll(0, list); } return result.isEnd != true; } Future _loadHistoryForSyncEnd() async { final result = await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList( conversationID: conversationInfo.conversationID, count: messageList.length < _pageSize ? _pageSize : messageList.length, startMsg: null, ); if (result.messageList == null || result.messageList!.isEmpty) return; final list = result.messageList!; list.removeWhere((msg) => _isBeDeleteMessage(msg)); final offset = scrollController.offset; messageList.assignAll(list); scrollController.jumpTo(offset); } bool _isBeDeleteMessage(Message message) { final isPrivate = message.attachedInfoElem?.isPrivateChat ?? false; final hasReadTime = message.hasReadTime ?? 0; if (isPrivate && hasReadTime > 0) { return readTime(message) <= 0; } return false; } void _getGroupInfoAfterLoadMessage() { if (isGroupChat && ownerAndAdmin.isEmpty) { _isJoinedGroup(); } else { _checkInBlacklist(); } } recommendFriendCarte(UserInfo userInfo) async { final result = await AppNavigator.startSelectContacts( action: SelAction.recommend, ex: '[${StrRes.carte}]${userInfo.nickname}', ); if (null != result) { final customEx = result['customEx']; final checkedList = result['checkedList']; for (var info in checkedList) { final userID = IMUtils.convertCheckedToUserID(info); final groupID = IMUtils.convertCheckedToGroupID(info); if (customEx is String && customEx.isNotEmpty) { _sendMessage( await OpenIM.iMManager.messageManager.createTextMessage( text: customEx, ), userId: userID, groupId: groupID, ); } _sendMessage( await OpenIM.iMManager.messageManager.createCardMessage( userID: userInfo.userID!, nickname: userInfo.nickname!, faceURL: userInfo.faceURL, ), userId: userID, groupId: groupID, ); } } } void joinMeeting(Message msg) {} @override void onDetached() {} @override void onHidden() {} @override void onInactive() {} @override void onPaused() {} @override void onResumed() { _loadHistoryForSyncEnd(); } }