- 上游: github.com/OpenIMSDK/openim-flutter-demo tag 3.8.3-patch.3 commit b3dfdb1e8aaeaaf6f0793e10cadd20d5c184a31f - 并行目录 mobile-next/ 原样导入(含 LICENSE),不覆盖现网 mobile/ - 锁定 Flutter 3.24.5 / Dart 3.5.4 / JDK 17 / Gradle 7.6.3 / AGP 7.3.1 - 实测 Android debug 与 release 构建均成功(见 docs/mobile-next-baseline-import.md) - 本提交可单独回退:git revert 1db1229(重写前) Co-authored-by: multica-agent <github@multica.ai>
1834 lines
54 KiB
Dart
1834 lines
54 KiB
Dart
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<IMController>();
|
||
final appLogic = Get.find<AppController>();
|
||
final conversationLogic = Get.find<ConversationLogic>();
|
||
final cacheLogic = Get.find<CacheController>();
|
||
final downloadLogic = Get.find<DownloadController>();
|
||
|
||
final inputCtrl = TextEditingController();
|
||
final focusNode = FocusNode();
|
||
final scrollController = ScrollController();
|
||
final refreshController = RefreshController();
|
||
bool playOnce = false; // 点击的当前视频只能播放一次
|
||
|
||
final forceCloseToolbox = PublishSubject<bool>();
|
||
final forceCloseMenuSub = PublishSubject<bool>();
|
||
final sendStatusSub = PublishSubject<MsgStreamEv<bool>>();
|
||
|
||
late ConversationInfo conversationInfo;
|
||
Message? searchMessage;
|
||
final nickname = ''.obs;
|
||
final faceUrl = ''.obs;
|
||
Timer? typingTimer;
|
||
final typing = false.obs;
|
||
Timer? _debounce;
|
||
Message? quoteMsg;
|
||
final messageList = <Message>[].obs;
|
||
final tempMessages = <Message>[]; // 临时存放消息体,例如图片消息
|
||
var _lastCursorIndex = -1;
|
||
final onlineStatus = false.obs;
|
||
final onlineStatusDesc = ''.obs;
|
||
Timer? onlineStatusTimer;
|
||
final favoriteList = <String>[].obs;
|
||
final scaleFactor = Config.textScaleFactor.obs;
|
||
final background = "".obs;
|
||
final memberUpdateInfoMap = <String, GroupMembersInfo>{};
|
||
final groupMessageReadMembers = <String, List<String>>{};
|
||
final groupMutedStatus = 0.obs;
|
||
final groupMemberRoleLevel = 1.obs;
|
||
final muteEndTime = 0.obs;
|
||
GroupInfo? groupInfo;
|
||
GroupMembersInfo? groupMembersInfo;
|
||
List<GroupMembersInfo> ownerAndAdmin = [];
|
||
|
||
final isInGroup = true.obs;
|
||
final memberCount = 0.obs;
|
||
final privateMessageList = <Message>[];
|
||
final isInBlacklist = false.obs;
|
||
final _audioPlayer = AudioPlayer();
|
||
final _currentPlayClientMsgID = "".obs;
|
||
final isShowPopMenu = false.obs;
|
||
|
||
final scrollingCacheMessageList = <Message>[];
|
||
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 = <String?, String?>{};
|
||
final revokedTextMessage = <String, String>{};
|
||
|
||
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 = <GroupMembersInfo>[].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<List<Message>> 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<ReadReceiptInfo> 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<GroupMembersInfo>? 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<bool>(
|
||
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<bool>(
|
||
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<AssetEntity>? 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<bool> 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<GroupMembersInfo>) {
|
||
directionalUsers.assignAll(list);
|
||
}
|
||
}
|
||
}
|
||
|
||
TextSpan? directionalText() {
|
||
if (directionalUsers.isNotEmpty) {
|
||
final temp = <TextSpan>[];
|
||
|
||
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<String, dynamic> 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<bool>(
|
||
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<String, String> 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<int> plamtforms) {
|
||
var des = <String>[];
|
||
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<bool> _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<AdvancedMessage> _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<bool> onScrollToBottomLoad() async {
|
||
late List<Message> 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<void> _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();
|
||
}
|
||
}
|