Compare commits

...
Author SHA1 Message Date
613d38dd06 docs: 记录 B-273 Android 客户端清理与候选包
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 10:04:59 +08:00
1774e596ba test(mobile-next): 补充 B-273 悬浮窗、语音文案与位置清理定向测试
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 10:04:49 +08:00
694891c393 fix(mobile-next): 清理悬浮窗、语音文案与位置发送
按 B-273 / B-269 收口 Android 客户端:去掉悬浮窗申请与旧浮窗服务,通话入口统一为语音通话,删除位置发送与定位权限,保留历史位置只读展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 10:04:39 +08:00
设计开发队长andmultica-agent 092039bc1a fix(mobile-next): 收口我的信息只读字段显示
Co-authored-by: multica-agent <github@multica.ai>
2026-08-21 00:52:22 +08:00
7bda2f280a docs: 记录 B-261 /user/find/full 404 定位与构建证据
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 23:58:42 +08:00
9001688c9e fix(mobile-next): 我的信息改走 OpenIM SDK,避免账号服务 404
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 23:58:37 +08:00
29 changed files with 1057 additions and 877 deletions
@@ -0,0 +1,64 @@
# B-261 Android「我的信息」404
分支:本卡施工分支(基于联网修复复测包 `1895830` / `agent/agent/a4ad7eed`
对照用户安装包:Gitea `v3.8.3-network-fix-retest``changlian-android-3.8.3-b225.apk`
本卡只改客户端用户资料读取,不改现网服务、不改账号数据、不覆盖旧 Release。
**结论:404 是客户端把官方 chat 接口打到了账号服务。责任在客户端。已改为走 OpenIM SDK,不再请求 `/user/find/full`。**
## 直接原因
用户截图:`GET http://192.168.200.11:10010/user/find/full` → 404。
| 核对项 | 结果 |
|---|---|
| 账号服务真实路由 | 只有 `/api/login``/api/directory``/api/rtc_token`、管理接口;没有 `/user/find/full` |
| 现网探测 | `GET/POST http://192.168.200.11:10010/user/find/full` 均 404`GET /api/health` 200,员工数 3 |
| 当前发布包调用方 | 官方页「我的信息」走 `Apis.queryMyFullInfo()``HttpUtil.post(Urls.getUsersFullInfo)` |
| URL 来源 | B-225 把 `Config.appAuthUrl` 从已关闭的 :10008 改到 :10010,登录能通,但这条 chat 路径仍打到账号服务 |
| 代码方法 | 源码是 POST;GET/POST 对不存在的路径都是 404,和截图一致 |
`mobile/` 和 PC 早已用 OpenIM SDK 取资料,不打这条路径。新 `mobile-next/` 漏改。
责任边界:客户端请求了账号服务没有的接口。不是工号密码错误,也不是服务器宕机。不需要改现网。
## 最小修复
`Apis.getUserFullInfo` / `queryMyFullInfo` / `searchUserFullInfo` / `updateUserInfo` 改为 OpenIM SDK(与 PC `pc-client/src/api/login.ts` 同一策略)。
- 能同步:工号、昵称、头像、勿扰
- 不再有值:手机号、邮箱、性别、生日等原 chat 业务字段
- 未改:`account-service/`、docker、旧 `mobile/`、PC
## 回退
从新到旧:`git revert` 本验收记录提交,再 `git revert` 代码提交。用户侧可继续用 `v3.8.3-network-fix-retest` 或旧包 `v1.0.6-internal-test`
## 构建
- Flutter 3.24.5 / Dart 3.5.4 / Temurin 17.0.20
- `flutter build apk --release` 成功
- 上游告警同 B-225Flutter Gradle apply-script 弃用;部分依赖 Kotlin metadata 1.8.0 vs 工程预期 1.6.0。release 通过。
| 产物 | 版本 | 包名 | 大小 | SHA256 |
|---|---|---|---|---|
| Android release APK | 3.8.3+180 | `io.openim.flutter.demo` | 53,286,915 B | `42D74E85AEC52A3889ED79CE7345C3BA5F69B4B3FA994032C2D38DF6A9CF5987` |
路径:`mobile-next/build/app/outputs/flutter-apk/app-release.apk`(APK 内嵌时间戳,哈希不可位级复现)
## 已测 / 未测
已测:
- 现网 `:10010/user/find/full` GET/POST 404`/api/health` 200
- `UserFullInfoMapper` 单测 3 项通过
- 改动文件 `flutter analyze` 无新增 error(剩余 8 条均为 `apis.dart` 原有 warning/info
- Android release 构建通过,校验值见上表
- `Apis` 不再引用 `Urls.getUsersFullInfo` / `updateUserInfo` / `searchUserFullInfo`
未测(不得写成已通过):
- 公司内网真机登录后打开「我的信息」
- 登录、联系人、发消息完整回归
残余:官方 `searchFriendInfo`、改密、验证码仍指向已下线 chat 路径;工号登录主路径和「我的信息」不再走这些接口。
@@ -0,0 +1,103 @@
# B-273 Android 悬浮窗、语音文案与位置发送清理
分支:`agent/agent/3f3d8cc3`(基于 `agent/agent/2b5c0459` HEAD `092039b`
施工依据:B-269 第七、八、十节;B-91 视觉规范 v2(本卡不做换肤)。
本卡只改 `mobile-next/` 客户端,不发布、不改现网、不覆盖 Gitea Release。
**结论:三项清理已落地;Android release 真实构建通过。缺真机账号,首次安装权限弹窗、前后台语音、历史位置消息展示继续标为未测。**
## 基线
- 开工 HEAD`092039b` `fix(mobile-next): 收口我的信息只读字段显示`
- 远端 `origin/agent/agent/2b5c0459` 与开工提交一致,未覆盖他人变更
## 三项对照
| # | 目标 | 修改位置 | 结果 |
|---|---|---|---|
| 1 | 去掉悬浮窗 | `AndroidManifest.xml``tools:node="remove"` 剔除 `SYSTEM_ALERT_WINDOW`;删除 IM 同步后申请;删除旧 `CallService` 声明;来电不再走 overlay 分支 | 合并清单无悬浮窗权限、无 `CallService` |
| 2 | 通话入口改成语音 | `toolboxCall` / `audioAndVideoCall` →「语音通话」;资料页改麦克风图标;通话选择表只留语音;三个入口仍只发 `CallType.audio` | 相册「拍摄」和「视频」消息文案未改 |
| 3 | 去掉位置发送 | 工具箱/选点页/`sendLocation` 删除;去掉 `geolocator`;定位权限 `tools:node="remove"`;保留历史位置气泡与 `MapView`,解析失败回退「位置消息」+ 地址 | 合并清单无定位权限 |
旧插件 `local_plugin/flutter_openim_live_alert` 未物理删除(B-269:可先停用、另卡再删),应用清单已不再声明其服务。
`REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` 仍由 `flutter_background`(已接通通话前台服务)合并进来;客户端登录/同步不再申请。`CALL_PHONE` / `READ_PHONE_STATE` / `REQUEST_INSTALL_PACKAGES` 仍有拨号、WebRTC、升级外链用途,未动。
## 提交主题(可逐项 `git revert`
从新到旧回退:先本验收记录,再测试,再代码。
| 主题 | SHA | 文件 |
|---|---|---|
| 本验收记录 | 本文件当次提交 | `docs/acceptance/b273-android-client-cleanup.md` |
| 定向测试 | `1774e59` | `mobile-next/test/company/b273_client_cleanup_test.dart` |
| 三项清理代码 | `694891c` | 悬浮窗、语音文案、位置发送 |
回退示例(新到旧):
```
git revert <本验收提交>
git revert 1774e59
git revert 694891c
```
回退不影响 `mobile/``pc-client/``account-service/``config/``scripts/`、现网服务与已发布安装包。
## 环境
- Flutter 3.24.5 / Dart 3.5.4`C:\flutter324`
- Temurin JDK 17.0.20+8
- Android SDK:既有 B-95 工具链(compileSdk 34
- `org.gradle.jvmargs=-Xmx4096M`
## 检查与构建
### 定点测试
```
cd mobile-next
flutter test test/company/b273_client_cleanup_test.dart test/company/call_visual_rework_test.dart test/company/call_session_guard_test.dart test/company/rtc_token_mapper_test.dart test/company/user_full_info_mapper_test.dart
```
25 项通过:清理清单/文案/位置回退、通话视觉 Token、占线/去重/超时、换票映射、「我的信息」映射。
定点 `flutter analyze` 修改文件:**0 error**。存量 info/warning 未作为本卡阻塞。
### Android(真实构建)
```
cd mobile-next
flutter build apk --release
```
| 产物 | 版本 | 包名 | 大小 | SHA256 |
|---|---|---|---|---|
| Android release APK | 3.8.3+180 | `io.openim.flutter.demo` | 53,096,372 B | `F50D9DAA57D4BCCA6A4841F2B8156E034C794C859CEDD3C02B7856103E084913` |
路径:`mobile-next/build/app/outputs/flutter-apk/app-release.apk`(APK 内嵌时间戳,哈希不可位级复现)
构建期告警同 B-261Flutter Gradle apply-script 弃用;部分依赖 Kotlin metadata 1.8.0 vs 工程预期 1.6.0。release 通过。
合并清单 `build/app/intermediates/merged_manifests/release/AndroidManifest.xml`**无** `SYSTEM_ALERT_WINDOW`、**无** `ACCESS_*LOCATION`、**无** `CallService`
候选包只附本卡,未发布 Gitea Release。
## 已测 / 未测
已测:
- 定点测试 25 项通过
- 改动文件 `flutter analyze` 无新增 error
- Android release 构建通过,合并清单无悬浮窗/定位权限
- 通话三个入口源码仍只发 `CallType.audio`;相册/拍摄/视频消息代码未删
未测(不得写成已通过):
- 真机首次安装、登录、IM 同步是否还弹出「显示在其他应用上层」
- 前后台/锁屏语音呼出与来电
- 历史位置消息真机展示与地图不可用回退
- 工具箱相册/拍摄/文件/名片真机回归
## 越界比对
相对 `092039b`:仅 `mobile-next/` 与本验收文档。`mobile/``pc-client/``account-service/`、现网配置 **零改动**
@@ -12,10 +12,10 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- B-273: 彻底移除悬浮窗;合并清单也禁止依赖再声明 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" tools:node="remove" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission-sdk-23 android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />
@@ -26,14 +26,15 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- B-273: 位置发送已移除,合并清单禁止再声明定位权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" tools:node="remove" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" /> <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
@@ -50,9 +51,6 @@
android:usesPermissionFlags="neverForLocation" /> android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
@@ -130,10 +128,6 @@
android:exported="false" android:exported="false"
android:stopWithTask="false" /> android:stopWithTask="false" />
<service
android:name="io.openim.live.alert.flutter_openim_live_alert.services.CallService"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider" android:authorities="${applicationId}.fileProvider"
@@ -1,12 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart'; import 'package:openim_common/openim_common.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:openim_live/openim_live.dart'; import 'package:openim_live/openim_live.dart';
import '../../company/feature_flags.dart'; import '../../company/feature_flags.dart';
@@ -20,7 +18,8 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
late Rx<UserFullInfo> userInfo; late Rx<UserFullInfo> userInfo;
late String atAllTag; late String atAllTag;
final _rtcTokenApi = RtcTokenApi(); final _rtcTokenApi = RtcTokenApi();
final callGuard = CallSessionGuard(inviteTimeout: LiveKitCallConfig.inviteTimeout); final callGuard =
CallSessionGuard(inviteTimeout: LiveKitCallConfig.inviteTimeout);
@override @override
void onClose() { void onClose() {
@@ -36,7 +35,8 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
if (token == null || token.isEmpty) { if (token == null || token.isEmpty) {
return Future.error(StateError('登录状态已失效,请重新登录')); return Future.error(StateError('登录状态已失效,请重新登录'));
} }
return _rtcTokenApi.getToken(room: roomID, identity: userID, authToken: token); return _rtcTokenApi.getToken(
room: roomID, identity: userID, authToken: token);
} }
@override @override
@@ -54,11 +54,13 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
if (!FeatureFlags.livekitCall) return; if (!FeatureFlags.livekitCall) return;
final invitation = info.invitation; final invitation = info.invitation;
final roomID = invitation?.roomID ?? ''; final roomID = invitation?.roomID ?? '';
if (invitation?.mediaType != 'audio' || invitation?.sessionType != ConversationType.single) { if (invitation?.mediaType != 'audio' ||
invitation?.sessionType != ConversationType.single) {
onTapReject(info); onTapReject(info);
return; return;
} }
if (!callGuard.acceptSignaling(customType: CustomMessageType.callingInvite, roomID: roomID)) { if (!callGuard.acceptSignaling(
customType: CustomMessageType.callingInvite, roomID: roomID)) {
return; return;
} }
if (isBusy || callGuard.isBusy) { if (isBusy || callGuard.isBusy) {
@@ -149,7 +151,8 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
); );
OpenIM.iMManager OpenIM.iMManager
..setUploadLogsListener(OnUploadLogsListener(onUploadProgress: uploadLogsProgress)) ..setUploadLogsListener(
OnUploadLogsListener(onUploadProgress: uploadLogsProgress))
..userManager.setUserListener(OnUserListener( ..userManager.setUserListener(OnUserListener(
onSelfInfoUpdated: (u) { onSelfInfoUpdated: (u) {
selfInfoUpdated(u); selfInfoUpdated(u);
@@ -180,11 +183,13 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
customType == CustomMessageType.callingCancel || customType == CustomMessageType.callingCancel ||
customType == CustomMessageType.callingHungup) { customType == CustomMessageType.callingHungup) {
if (!FeatureFlags.livekitCall) return; if (!FeatureFlags.livekitCall) return;
final signaling = SignalingInfo(invitation: InvitationInfo.fromJson(map['data'])); final signaling = SignalingInfo(
invitation: InvitationInfo.fromJson(map['data']));
signaling.userID = signaling.invitation?.inviterUserID; signaling.userID = signaling.invitation?.inviterUserID;
final roomID = signaling.invitation?.roomID ?? ''; final roomID = signaling.invitation?.roomID ?? '';
if (customType != CustomMessageType.callingInvite && if (customType != CustomMessageType.callingInvite &&
!callGuard.acceptSignaling(customType: customType as int, roomID: roomID)) { !callGuard.acceptSignaling(
customType: customType as int, roomID: roomID)) {
return; return;
} }
@@ -236,9 +241,6 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
}, },
onSyncServerFinish: (reInstall) { onSyncServerFinish: (reInstall) {
imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false); imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false);
if (Platform.isAndroid) {
Permissions.request([Permission.systemAlertWindow]);
}
}, },
onSyncServerStart: (reInstall) { onSyncServerStart: (reInstall) {
imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false); imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false);
+127 -87
View File
@@ -126,10 +126,13 @@ class ChatLogic extends SuperController {
String get memberStr => isSingleChat ? "" : "($memberCount)"; String get memberStr => isSingleChat ? "" : "($memberCount)";
String? get senderName => isSingleChat ? OpenIM.iMManager.userInfo.nickname : groupMembersInfo?.nickname; String? get senderName => isSingleChat
? OpenIM.iMManager.userInfo.nickname
: groupMembersInfo?.nickname;
bool get isAdminOrOwner => bool get isAdminOrOwner =>
groupMemberRoleLevel.value == GroupRoleLevel.admin || groupMemberRoleLevel.value == GroupRoleLevel.owner; groupMemberRoleLevel.value == GroupRoleLevel.admin ||
groupMemberRoleLevel.value == GroupRoleLevel.owner;
final directionalUsers = <GroupMembersInfo>[].obs; final directionalUsers = <GroupMembersInfo>[].obs;
@@ -140,8 +143,10 @@ class ChatLogic extends SuperController {
var isCurSingleChat = message.isSingleChat && var isCurSingleChat = message.isSingleChat &&
isSingleChat && isSingleChat &&
(senderId == userID || senderId == OpenIM.iMManager.userID && receiverId == userID); (senderId == userID ||
var isCurGroupChat = message.isGroupChat && isGroupChat && groupID == groupId; senderId == OpenIM.iMManager.userID && receiverId == userID);
var isCurGroupChat =
message.isGroupChat && isGroupChat && groupID == groupId;
return isCurSingleChat || isCurGroupChat; return isCurSingleChat || isCurGroupChat;
} }
@@ -152,11 +157,14 @@ class ChatLogic extends SuperController {
} }
Future<List<Message>> searchMediaMessage() async { Future<List<Message>> searchMediaMessage() async {
final messageList = await OpenIM.iMManager.messageManager.searchLocalMessages( final messageList = await OpenIM.iMManager.messageManager
conversationID: conversationInfo.conversationID, .searchLocalMessages(
messageTypeList: [MessageType.picture, MessageType.video], conversationID: conversationInfo.conversationID,
count: 500); messageTypeList: [MessageType.picture, MessageType.video],
return messageList.searchResultItems?.first.messageList?.reversed.toList() ?? []; count: 500);
return messageList.searchResultItems?.first.messageList?.reversed
.toList() ??
[];
} }
@override @override
@@ -185,7 +193,8 @@ class ChatLogic extends SuperController {
_setSdkSyncDataListener(); _setSdkSyncDataListener();
conversationSub = imLogic.conversationChangedSubject.listen((value) { conversationSub = imLogic.conversationChangedSubject.listen((value) {
final obj = value.firstWhereOrNull((e) => e.conversationID == conversationInfo.conversationID); final obj = value.firstWhereOrNull(
(e) => e.conversationID == conversationInfo.conversationID);
if (obj != null) { if (obj != null) {
conversationInfo = obj; conversationInfo = obj;
@@ -196,7 +205,8 @@ class ChatLogic extends SuperController {
if (isCurrentChat(message)) { if (isCurrentChat(message)) {
if (message.contentType == MessageType.typing) { if (message.contentType == MessageType.typing) {
} else { } else {
if (!messageList.contains(message) && !scrollingCacheMessageList.contains(message)) { if (!messageList.contains(message) &&
!scrollingCacheMessageList.contains(message)) {
_isReceivedMessageWhenSyncing = true; _isReceivedMessageWhenSyncing = true;
if (isShowPopMenu.value || scrollController.offset != 0) { if (isShowPopMenu.value || scrollController.offset != 0) {
scrollingCacheMessageList.add(message); scrollingCacheMessageList.add(message);
@@ -210,7 +220,8 @@ class ChatLogic extends SuperController {
}; };
imLogic.onRecvMessageRevoked = (RevokedInfo info) { imLogic.onRecvMessageRevoked = (RevokedInfo info) {
var message = messageList.firstWhereOrNull((e) => e.clientMsgID == info.clientMsgID); var message = messageList
.firstWhereOrNull((e) => e.clientMsgID == info.clientMsgID);
message?.notificationElem = NotificationElem(detail: jsonEncode(info)); message?.notificationElem = NotificationElem(detail: jsonEncode(info));
message?.contentType = MessageType.revokeMessageNotification; message?.contentType = MessageType.revokeMessageNotification;
@@ -273,12 +284,14 @@ class ChatLogic extends SuperController {
} }
_putMemberInfo([info]); _putMemberInfo([info]);
final index = ownerAndAdmin.indexWhere((element) => element.userID == info.userID); final index = ownerAndAdmin
.indexWhere((element) => element.userID == info.userID);
if (info.roleLevel == GroupRoleLevel.member) { if (info.roleLevel == GroupRoleLevel.member) {
if (index > -1) { if (index > -1) {
ownerAndAdmin.removeAt(index); ownerAndAdmin.removeAt(index);
} }
} else if (info.roleLevel == GroupRoleLevel.admin || info.roleLevel == GroupRoleLevel.owner) { } else if (info.roleLevel == GroupRoleLevel.admin ||
info.roleLevel == GroupRoleLevel.owner) {
if (index == -1) { if (index == -1) {
ownerAndAdmin.add(info); ownerAndAdmin.add(info);
} else { } else {
@@ -364,7 +377,8 @@ class ChatLogic extends SuperController {
}; };
imLogic.inputStateChangedSubject.listen((value) { imLogic.inputStateChangedSubject.listen((value) {
if (value.conversationID == conversationInfo.conversationID && value.userID == userID) { if (value.conversationID == conversationInfo.conversationID &&
value.userID == userID) {
typing.value = value.platformIDs?.isNotEmpty == true; typing.value = value.platformIDs?.isNotEmpty == true;
} }
}); });
@@ -396,7 +410,8 @@ class ChatLogic extends SuperController {
Future sendPicture({required String path, bool sendNow = true}) async { Future sendPicture({required String path, bool sendNow = true}) async {
final file = await IMUtils.compressImageAndGetFile(File(path)); final file = await IMUtils.compressImageAndGetFile(File(path));
var message = await OpenIM.iMManager.messageManager.createImageMessageFromFullPath( var message =
await OpenIM.iMManager.messageManager.createImageMessageFromFullPath(
imagePath: file!.path, imagePath: file!.path,
); );
@@ -409,7 +424,8 @@ class ChatLogic extends SuperController {
} }
void sendVoice(int duration, String path) async { void sendVoice(int duration, String path) async {
var message = await OpenIM.iMManager.messageManager.createSoundMessageFromFullPath( var message =
await OpenIM.iMManager.messageManager.createSoundMessageFromFullPath(
soundPath: path, soundPath: path,
duration: duration, duration: duration,
); );
@@ -423,7 +439,8 @@ class ChatLogic extends SuperController {
required String thumbnailPath, required String thumbnailPath,
bool sendNow = true}) async { bool sendNow = true}) async {
var d = duration > 1000.0 ? duration / 1000.0 : duration; var d = duration > 1000.0 ? duration / 1000.0 : duration;
var message = await OpenIM.iMManager.messageManager.createVideoMessageFromFullPath( var message =
await OpenIM.iMManager.messageManager.createVideoMessageFromFullPath(
videoPath: videoPath, videoPath: videoPath,
videoType: mimeType, videoType: mimeType,
duration: d.toInt(), duration: d.toInt(),
@@ -439,24 +456,14 @@ class ChatLogic extends SuperController {
} }
void sendFile({required String filePath, required String fileName}) async { void sendFile({required String filePath, required String fileName}) async {
var message = await OpenIM.iMManager.messageManager.createFileMessageFromFullPath( var message =
await OpenIM.iMManager.messageManager.createFileMessageFromFullPath(
filePath: filePath, filePath: filePath,
fileName: fileName, fileName: fileName,
); );
_sendMessage(message); _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( sendForwardRemarkMsg(
String content, { String content, {
String? userId, String? userId,
@@ -481,8 +488,8 @@ class ChatLogic extends SuperController {
void sendTypingMsg({bool focus = false}) async { void sendTypingMsg({bool focus = false}) async {
if (isSingleChat) { if (isSingleChat) {
OpenIM.iMManager.conversationManager OpenIM.iMManager.conversationManager.changeInputStates(
.changeInputStates(conversationID: conversationInfo.conversationID, focus: focus); conversationID: conversationInfo.conversationID, focus: focus);
} }
} }
@@ -544,7 +551,8 @@ class ChatLogic extends SuperController {
offlinePushInfo: Config.offlinePushInfo, offlinePushInfo: Config.offlinePushInfo,
) )
.then((value) => _sendSucceeded(message, value)) .then((value) => _sendSucceeded(message, value))
.catchError((error, _) => _senFailed(message, groupId, userId, error, _)) .catchError(
(error, _) => _senFailed(message, groupId, userId, error, _))
.whenComplete(() => _completed()); .whenComplete(() => _completed());
} }
@@ -557,8 +565,10 @@ class ChatLogic extends SuperController {
)); ));
} }
void _senFailed(Message message, String? groupId, String? userId, error, stack) async { void _senFailed(
Logger.print('message send failed userID: $userId groupId:$groupId, catch error :$error $stack'); 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; message.status = MessageStatus.failed;
sendStatusSub.addSafely(MsgStreamEv<bool>( sendStatusSub.addSafely(MsgStreamEv<bool>(
id: message.clientMsgID!, id: message.clientMsgID!,
@@ -574,7 +584,8 @@ class ChatLogic extends SuperController {
customType = CustomMessageType.deletedByFriend; customType = CustomMessageType.deletedByFriend;
} }
if (null != customType) { if (null != customType) {
final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage(type: customType)) final hintMessage = (await OpenIM.iMManager.messageManager
.createFailedHintMessage(type: customType))
..status = 2 ..status = 2
..isRead = true; ..isRead = true;
if (userId != null) { if (userId != null) {
@@ -591,10 +602,15 @@ class ChatLogic extends SuperController {
); );
} }
} else { } else {
if ((code == SDKErrorCode.userIsNotInGroup || code == SDKErrorCode.groupDisbanded) && null == groupId) { if ((code == SDKErrorCode.userIsNotInGroup ||
code == SDKErrorCode.groupDisbanded) &&
null == groupId) {
final status = groupInfo?.status; final status = groupInfo?.status;
final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage( final hintMessage = (await OpenIM.iMManager.messageManager
type: status == 2 ? CustomMessageType.groupDisbanded : CustomMessageType.removedFromGroup)) .createFailedHintMessage(
type: status == 2
? CustomMessageType.groupDisbanded
: CustomMessageType.removedFromGroup))
..status = 2 ..status = 2
..isRead = true; ..isRead = true;
messageList.add(hintMessage); messageList.add(hintMessage);
@@ -662,7 +678,9 @@ class ChatLogic extends SuperController {
void markMessageAsRead(Message message, bool visible) async { void markMessageAsRead(Message message, bool visible) async {
Logger.print('markMessageAsRead: ${message.textElem?.content}, $visible'); Logger.print('markMessageAsRead: ${message.textElem?.content}, $visible');
if (visible && message.contentType! < 1000 && message.contentType! != MessageType.voice) { if (visible &&
message.contentType! < 1000 &&
message.contentType! != MessageType.voice) {
var data = IMUtils.parseCustomMessage(message); var data = IMUtils.parseCustomMessage(message);
if (null != data && data['viewType'] == CustomMessageType.call) { if (null != data && data['viewType'] == CustomMessageType.call) {
Logger.print('markMessageAsRead: call message $data'); Logger.print('markMessageAsRead: call message $data');
@@ -675,11 +693,14 @@ class ChatLogic extends SuperController {
_markMessageAsRead(Message message) async { _markMessageAsRead(Message message) async {
if (!message.isRead! && message.sendID != OpenIM.iMManager.userID) { if (!message.isRead! && message.sendID != OpenIM.iMManager.userID) {
try { try {
Logger.print('mark conversation message as read${message.clientMsgID!} ${message.isRead}'); Logger.print(
'mark conversation message as read${message.clientMsgID!} ${message.isRead}');
await OpenIM.iMManager.conversationManager await OpenIM.iMManager.conversationManager
.markConversationMessageAsRead(conversationID: conversationInfo.conversationID); .markConversationMessageAsRead(
conversationID: conversationInfo.conversationID);
} catch (e) { } catch (e) {
Logger.print('failed to send group message read receipt ${message.clientMsgID} ${message.isRead}'); Logger.print(
'failed to send group message read receipt ${message.clientMsgID} ${message.isRead}');
} finally { } finally {
message.isRead = true; message.isRead = true;
message.hasReadTime = _timestamp; message.hasReadTime = _timestamp;
@@ -690,23 +711,23 @@ class ChatLogic extends SuperController {
_clearUnreadCount() { _clearUnreadCount() {
if (conversationInfo.unreadCount > 0) { if (conversationInfo.unreadCount > 0) {
OpenIM.iMManager.conversationManager OpenIM.iMManager.conversationManager.markConversationMessageAsRead(
.markConversationMessageAsRead(conversationID: conversationInfo.conversationID); conversationID: conversationInfo.conversationID);
} }
} }
void _getInputState() async { void _getInputState() async {
if (conversationInfo.isSingleChat) { if (conversationInfo.isSingleChat) {
final result = final result = await OpenIM.iMManager.conversationManager
await OpenIM.iMManager.conversationManager.getInputStates(conversationInfo.conversationID, userID!); .getInputStates(conversationInfo.conversationID, userID!);
typing.value = result?.isNotEmpty == true; typing.value = result?.isNotEmpty == true;
} }
} }
void _changeInputStatus(bool focus) async { void _changeInputStatus(bool focus) async {
if (conversationInfo.isSingleChat) { if (conversationInfo.isSingleChat) {
await OpenIM.iMManager.conversationManager await OpenIM.iMManager.conversationManager.changeInputStates(
.changeInputStates(conversationID: conversationInfo.conversationID, focus: focus); conversationID: conversationInfo.conversationID, focus: focus);
} }
} }
@@ -714,18 +735,6 @@ class ChatLogic extends SuperController {
forceCloseToolbox.addSafely(true); 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 { void onTapAlbum() async {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!, final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!,
pickerConfig: AssetPickerConfig( pickerConfig: AssetPickerConfig(
@@ -743,7 +752,8 @@ class ChatLogic extends SuperController {
} }
if (entity.videoDuration > const Duration(seconds: 5 * 60)) { if (entity.videoDuration > const Duration(seconds: 5 * 60)) {
IMViews.showToast(sprintf(StrRes.selectVideoLimit, [5]) + StrRes.minute); IMViews.showToast(
sprintf(StrRes.selectVideoLimit, [5]) + StrRes.minute);
return false; return false;
} }
return true; return true;
@@ -837,10 +847,13 @@ class ChatLogic extends SuperController {
Future _handleAssets(AssetEntity? asset, {bool sendNow = true}) async { Future _handleAssets(AssetEntity? asset, {bool sendNow = true}) async {
if (null != asset) { if (null != asset) {
Logger.print('--------assets type-----${asset.type} create time: ${asset.createDateTime}'); Logger.print(
'--------assets type-----${asset.type} create time: ${asset.createDateTime}');
final originalFile = await asset.file; final originalFile = await asset.file;
final originalPath = originalFile!.path; final originalPath = originalFile!.path;
var path = originalPath.toLowerCase().endsWith('.gif') ? originalPath : originalFile.path; var path = originalPath.toLowerCase().endsWith('.gif')
? originalPath
: originalFile.path;
Logger.print('--------assets path-----$path'); Logger.print('--------assets path-----$path');
switch (asset.type) { switch (asset.type) {
case AssetType.image: case AssetType.image:
@@ -1041,14 +1054,16 @@ class ChatLogic extends SuperController {
} }
void copy(Message message) { void copy(Message message) {
final content = copyTextMap[message.clientMsgID] ?? message.textElem?.content; final content =
copyTextMap[message.clientMsgID] ?? message.textElem?.content;
if (null != content) { if (null != content) {
IMUtils.copy(text: content.replaceAll('\u200B', '')); IMUtils.copy(text: content.replaceAll('\u200B', ''));
} }
} }
Message indexOfMessage(int index, {bool calculate = true}) => IMUtils.calChatTimeInterval( Message indexOfMessage(int index, {bool calculate = true}) =>
IMUtils.calChatTimeInterval(
messageList, messageList,
calculate: calculate, calculate: calculate,
).reversed.elementAt(index); ).reversed.elementAt(index);
@@ -1111,7 +1126,11 @@ class ChatLogic extends SuperController {
void onDeleteEmoji() { void onDeleteEmoji() {
final input = inputCtrl.text; final input = inputCtrl.text;
final regexEmoji = emojiFaces.keys.toList().join('|').replaceAll('[', '\\[').replaceAll(']', '\\]'); final regexEmoji = emojiFaces.keys
.toList()
.join('|')
.replaceAll('[', '\\[')
.replaceAll(']', '\\]');
final list = [regexEmoji]; final list = [regexEmoji];
final pattern = '(${list.toList().join('|')})'; final pattern = '(${list.toList().join('|')})';
final emojiReg = RegExp(regexEmoji); final emojiReg = RegExp(regexEmoji);
@@ -1186,7 +1205,8 @@ class ChatLogic extends SuperController {
void sendFavoritePic(int index, String url) async { void sendFavoritePic(int index, String url) async {
var emoji = cacheLogic.favoriteList.elementAt(index); var emoji = cacheLogic.favoriteList.elementAt(index);
var message = await OpenIM.iMManager.messageManager.createFaceMessage( var message = await OpenIM.iMManager.messageManager.createFaceMessage(
data: json.encode({'url': emoji.url, 'width': emoji.width, 'height': emoji.height}), data: json.encode(
{'url': emoji.url, 'width': emoji.width, 'height': emoji.height}),
); );
_sendMessage(message); _sendMessage(message);
} }
@@ -1227,7 +1247,8 @@ class ChatLogic extends SuperController {
var diff = (end - _timestamp) ~/ 1000; var diff = (end - _timestamp) ~/ 1000;
if (diff > 0) { if (diff > 0) {
privateMessageList.addIf(() => !privateMessageList.contains(message), message); privateMessageList.addIf(
() => !privateMessageList.contains(message), message);
} }
return diff < 0 ? 0 : diff; return diff < 0 ? 0 : diff;
} }
@@ -1255,7 +1276,8 @@ class ChatLogic extends SuperController {
userIDList: [OpenIM.iMManager.userID], userIDList: [OpenIM.iMManager.userID],
); );
groupMembersInfo = list.firstOrNull; groupMembersInfo = list.firstOrNull;
groupMemberRoleLevel.value = groupMembersInfo?.roleLevel ?? GroupRoleLevel.member; groupMemberRoleLevel.value =
groupMembersInfo?.roleLevel ?? GroupRoleLevel.member;
muteEndTime.value = groupMembersInfo?.muteEndTime ?? 0; muteEndTime.value = groupMembersInfo?.muteEndTime ?? 0;
if (null != groupMembersInfo) { if (null != groupMembersInfo) {
memberUpdateInfoMap[OpenIM.iMManager.userID] = groupMembersInfo!; memberUpdateInfoMap[OpenIM.iMManager.userID] = groupMembersInfo!;
@@ -1266,7 +1288,8 @@ class ChatLogic extends SuperController {
Future _queryOwnerAndAdmin() async { Future _queryOwnerAndAdmin() async {
if (isGroupChat) { if (isGroupChat) {
ownerAndAdmin = await OpenIM.iMManager.groupManager.getGroupMemberList(groupID: groupID!, filter: 5, count: 20); ownerAndAdmin = await OpenIM.iMManager.groupManager
.getGroupMemberList(groupID: groupID!, filter: 5, count: 20);
} }
return; return;
} }
@@ -1303,7 +1326,9 @@ class ChatLogic extends SuperController {
bool get havePermissionMute => bool get havePermissionMute =>
isGroupChat && isGroupChat &&
(groupInfo?.ownerUserID == OpenIM.iMManager.userID /*|| (groupInfo?.ownerUserID ==
OpenIM.iMManager
.userID /*||
groupMembersInfo?.roleLevel == 2*/ groupMembersInfo?.roleLevel == 2*/
); );
@@ -1315,8 +1340,10 @@ class ChatLogic extends SuperController {
void _queryUserOnlineStatus() { void _queryUserOnlineStatus() {
if (isSingleChat) { if (isSingleChat) {
OpenIM.iMManager.userManager.subscribeUsersStatus([userID!]).then((value) { OpenIM.iMManager.userManager
final status = value.firstWhereOrNull((element) => element.userID == userID); .subscribeUsersStatus([userID!]).then((value) {
final status =
value.firstWhereOrNull((element) => element.userID == userID);
_configUserStatusChanged(status); _configUserStatusChanged(status);
}); });
userStatusChangedSub = imLogic.userStatusChangedSubject.listen((value) { userStatusChangedSub = imLogic.userStatusChangedSubject.listen((value) {
@@ -1336,8 +1363,9 @@ class ChatLogic extends SuperController {
void _configUserStatusChanged(UserStatusInfo? status) { void _configUserStatusChanged(UserStatusInfo? status) {
if (status != null) { if (status != null) {
onlineStatus.value = status.status == 1; onlineStatus.value = status.status == 1;
onlineStatusDesc.value = onlineStatusDesc.value = status.status == 0
status.status == 0 ? StrRes.offline : _onlineStatusDes(status.platformIDs!) + StrRes.online; ? StrRes.offline
: _onlineStatusDes(status.platformIDs!) + StrRes.online;
} }
} }
@@ -1496,10 +1524,12 @@ class ChatLogic extends SuperController {
if (message.sendID == OpenIM.iMManager.userID) { if (message.sendID == OpenIM.iMManager.userID) {
canRevoke = true; canRevoke = true;
} else { } else {
var list = await LoadingView.singleton var list = await LoadingView.singleton.wrap(
.wrap(asyncFunction: () => OpenIM.iMManager.groupManager.getGroupOwnerAndAdmin(groupID: groupID!)); asyncFunction: () => OpenIM.iMManager.groupManager
.getGroupOwnerAndAdmin(groupID: groupID!));
var sender = list.firstWhereOrNull((e) => e.userID == message.sendID); var sender = list.firstWhereOrNull((e) => e.userID == message.sendID);
var revoker = list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID); var revoker =
list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID);
if (revoker != null && sender == null) { if (revoker != null && sender == null) {
canRevoke = true; canRevoke = true;
@@ -1531,7 +1561,8 @@ class ChatLogic extends SuperController {
), ),
); );
message.contentType = MessageType.revokeMessageNotification; message.contentType = MessageType.revokeMessageNotification;
message.notificationElem = NotificationElem(detail: jsonEncode(_buildRevokeInfo(message))); message.notificationElem =
NotificationElem(detail: jsonEncode(_buildRevokeInfo(message)));
messageList.refresh(); messageList.refresh();
} catch (e) { } catch (e) {
IMViews.showToast(e.toString()); IMViews.showToast(e.toString());
@@ -1595,12 +1626,15 @@ class ChatLogic extends SuperController {
if (isGroupChat) { if (isGroupChat) {
if (groupMemberRoleLevel.value == GroupRoleLevel.owner || if (groupMemberRoleLevel.value == GroupRoleLevel.owner ||
(groupMemberRoleLevel.value == GroupRoleLevel.admin && (groupMemberRoleLevel.value == GroupRoleLevel.admin &&
ownerAndAdmin.firstWhereOrNull((element) => element.userID == message.sendID) == null)) { ownerAndAdmin.firstWhereOrNull(
(element) => element.userID == message.sendID) ==
null)) {
return true; return true;
} }
} }
if (message.sendID == OpenIM.iMManager.userID) { if (message.sendID == OpenIM.iMManager.userID) {
if (DateTime.now().millisecondsSinceEpoch - (message.sendTime ??= 0) < (1000 * 60 * 5)) { if (DateTime.now().millisecondsSinceEpoch - (message.sendTime ??= 0) <
(1000 * 60 * 5)) {
return true; return true;
} }
} }
@@ -1611,7 +1645,8 @@ class ChatLogic extends SuperController {
if (message.status != MessageStatus.succeeded) { if (message.status != MessageStatus.succeeded) {
return false; return false;
} }
return message.contentType == MessageType.picture || message.contentType == MessageType.customFace; return message.contentType == MessageType.picture ||
message.contentType == MessageType.customFace;
} }
WillPopCallback? willPop() { WillPopCallback? willPop() {
@@ -1658,12 +1693,14 @@ class ChatLogic extends SuperController {
var data = message.customElem!.data; var data = message.customElem!.data;
var map = json.decode(data!); var map = json.decode(data!);
var customType = map['customType']; var customType = map['customType'];
return customType == CustomMessageType.deletedByFriend || customType == CustomMessageType.blockedByFriend; return customType == CustomMessageType.deletedByFriend ||
customType == CustomMessageType.blockedByFriend;
} }
return false; return false;
} }
void sendFriendVerification() => AppNavigator.startSendVerificationApplication(userID: userID); void sendFriendVerification() =>
AppNavigator.startSendVerificationApplication(userID: userID);
void _setSdkSyncDataListener() { void _setSdkSyncDataListener() {
connectionSub = imLogic.imSdkStatusPublishSubject.listen((value) { connectionSub = imLogic.imSdkStatusPublishSubject.listen((value) {
@@ -1699,7 +1736,9 @@ class ChatLogic extends SuperController {
} }
bool showBubbleBg(Message message) { bool showBubbleBg(Message message) {
return !isNotificationType(message) && !isFailedHintMessage(message) && !isRevokeMessage(message); return !isNotificationType(message) &&
!isFailedHintMessage(message) &&
!isRevokeMessage(message);
} }
bool isRevokeMessage(Message message) { bool isRevokeMessage(Message message) {
@@ -1748,7 +1787,8 @@ class ChatLogic extends SuperController {
} }
Future<void> _loadHistoryForSyncEnd() async { Future<void> _loadHistoryForSyncEnd() async {
final result = await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList( final result =
await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList(
conversationID: conversationInfo.conversationID, conversationID: conversationInfo.conversationID,
count: messageList.length < _pageSize ? _pageSize : messageList.length, count: messageList.length < _pageSize ? _pageSize : messageList.length,
startMsg: null, startMsg: null,
+20 -7
View File
@@ -86,13 +86,23 @@ class ChatPage extends StatelessWidget {
onTapUserProfile: handleUserProfileTap, onTapUserProfile: handleUserProfileTap,
); );
void handleUserProfileTap(({String userID, String name, String? faceURL, String? groupID}) userProfile) { void handleUserProfileTap(
final userInfo = UserInfo(userID: userProfile.userID, nickname: userProfile.name, faceURL: userProfile.faceURL); ({
String userID,
String name,
String? faceURL,
String? groupID
}) userProfile) {
final userInfo = UserInfo(
userID: userProfile.userID,
nickname: userProfile.name,
faceURL: userProfile.faceURL);
logic.viewUserInfo(userInfo); logic.viewUserInfo(userInfo);
} }
Widget? _buildMediaItem(BuildContext context, Message message) { Widget? _buildMediaItem(BuildContext context, Message message) {
if (message.contentType != MessageType.picture && message.contentType != MessageType.video) { if (message.contentType != MessageType.picture &&
message.contentType != MessageType.video) {
return null; return null;
} }
@@ -125,7 +135,8 @@ class ChatPage extends StatelessWidget {
child: Hero( child: Hero(
tag: message.clientMsgID!, tag: message.clientMsgID!,
child: _buildMediaContent(message), child: _buildMediaContent(message),
placeholderBuilder: (BuildContext context, Size heroSize, Widget child) => child, placeholderBuilder:
(BuildContext context, Size heroSize, Widget child) => child,
), ),
); );
} }
@@ -155,7 +166,8 @@ class ChatPage extends StatelessWidget {
final content = data['content']; final content = data['content'];
final view = ChatCallItemView(type: type, content: content); final view = ChatCallItemView(type: type, content: content);
return CustomTypeInfo(view); return CustomTypeInfo(view);
} else if (viewType == CustomMessageType.deletedByFriend || viewType == CustomMessageType.blockedByFriend) { } else if (viewType == CustomMessageType.deletedByFriend ||
viewType == CustomMessageType.blockedByFriend) {
final view = ChatFriendRelationshipAbnormalHintView( final view = ChatFriendRelationshipAbnormalHintView(
name: logic.nickname.value, name: logic.nickname.value,
onTap: logic.sendFriendVerification, onTap: logic.sendFriendVerification,
@@ -244,10 +256,11 @@ class ChatPage extends StatelessWidget {
toolbox: ChatToolBox( toolbox: ChatToolBox(
onTapAlbum: logic.onTapAlbum, onTapAlbum: logic.onTapAlbum,
onTapCamera: logic.onTapCamera, onTapCamera: logic.onTapCamera,
onTapCall: FeatureFlags.livekitCall && logic.isSingleChat ? logic.call : null, onTapCall: FeatureFlags.livekitCall && logic.isSingleChat
? logic.call
: null,
onTapCard: logic.onTapCarte, onTapCard: logic.onTapCarte,
onTapFile: logic.onTapFile, onTapFile: logic.onTapFile,
onTapLocation: logic.onTapLocation,
), ),
voiceRecordBar: bar, voiceRecordBar: bar,
emojiView: ChatEmojiView( emojiView: ChatEmojiView(
@@ -1,4 +1,3 @@
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:openim/pages/login/login_logic.dart'; import 'package:openim/pages/login/login_logic.dart';
@@ -26,8 +25,6 @@ class MyInfoLogic extends GetxController {
attr: EditAttr.mobile, attr: EditAttr.mobile,
); );
void editEmail() => AppNavigator.startEditMyInfo(attr: EditAttr.email, maxLength: 30);
void openPhotoSheet() { void openPhotoSheet() {
IMViews.openPhotoSheet( IMViews.openPhotoSheet(
onData: (path, url) async { onData: (path, url) async {
@@ -43,62 +40,6 @@ class MyInfoLogic extends GetxController {
quality: 15); quality: 15);
} }
void openDatePicker() {
var appLocale = Get.locale;
var isZh = appLocale!.languageCode.toLowerCase().contains("zh");
DatePicker.showDatePicker(
Get.context!,
locale: isZh ? LocaleType.zh : LocaleType.en,
maxTime: DateTime.now(),
currentTime: DateTime.fromMillisecondsSinceEpoch(imLogic.userInfo.value.birth ?? 0),
theme: DatePickerTheme(
cancelStyle: Styles.ts_0C1C33_17sp,
doneStyle: Styles.ts_0089FF_17sp,
itemStyle: Styles.ts_0C1C33_17sp,
),
onConfirm: (dateTime) {
_updateBirthday(dateTime.millisecondsSinceEpoch ~/ 1000);
},
);
}
void selectGender() {
Get.bottomSheet(
BottomSheetView(
items: [
SheetItem(
label: StrRes.man,
onTap: () => _updateGender(1),
),
SheetItem(
label: StrRes.woman,
onTap: () => _updateGender(2),
),
],
),
);
}
void _updateGender(int gender) {
LoadingView.singleton.wrap(
asyncFunction: () => Apis.updateUserInfo(userID: OpenIM.iMManager.userID, gender: gender)
.then((value) => imLogic.userInfo.update((val) {
val?.gender = gender;
})),
);
}
void _updateBirthday(int birthday) {
LoadingView.singleton.wrap(
asyncFunction: () => Apis.updateUserInfo(
userID: OpenIM.iMManager.userID,
birth: birthday * 1000,
).then((value) => imLogic.userInfo.update((val) {
val?.birth = birthday * 1000;
})),
);
}
@override @override
void onReady() { void onReady() {
_queryMyFullIno(); _queryMyFullIno();
@@ -42,16 +42,18 @@ class MyInfoPage extends StatelessWidget {
), ),
_buildItemView( _buildItemView(
label: StrRes.gender, label: StrRes.gender,
value: imLogic.userInfo.value.isMale ? StrRes.man : StrRes.woman, value: _genderText(imLogic.userInfo.value.gender),
onTap: logic.selectGender, showRightArrow: false,
), ),
_buildItemView( _buildItemView(
label: StrRes.birthDay, label: StrRes.birthDay,
value: DateUtil.formatDateMs( value: imLogic.userInfo.value.birth == null
imLogic.userInfo.value.birth ?? 0, ? ''
format: IMUtils.getTimeFormat1(), : DateUtil.formatDateMs(
), imLogic.userInfo.value.birth!,
onTap: logic.openDatePicker, format: IMUtils.getTimeFormat1(),
),
showRightArrow: false,
), ),
], ],
), ),
@@ -68,7 +70,7 @@ class MyInfoPage extends StatelessWidget {
_buildItemView( _buildItemView(
label: StrRes.email, label: StrRes.email,
value: imLogic.userInfo.value.email, value: imLogic.userInfo.value.email,
onTap: logic.editEmail, showRightArrow: false,
), ),
], ],
), ),
@@ -93,6 +95,12 @@ class MyInfoPage extends StatelessWidget {
child: Column(children: children), child: Column(children: children),
); );
String _genderText(int? gender) {
if (gender == 1) return StrRes.man;
if (gender == 2) return StrRes.woman;
return '';
}
Widget _buildItemView({ Widget _buildItemView({
required String label, required String label,
String? value, String? value,
@@ -33,6 +33,7 @@ export 'src/utils/sp_util.dart';
export 'src/utils/utils.dart'; export 'src/utils/utils.dart';
export 'src/utils/voice_record.dart'; export 'src/utils/voice_record.dart';
export 'src/utils/api_service.dart'; export 'src/utils/api_service.dart';
export 'src/utils/user_full_info_mapper.dart';
export 'src/widgets/avatar_view.dart'; export 'src/widgets/avatar_view.dart';
export 'src/widgets/azlist_view.dart'; export 'src/widgets/azlist_view.dart';
export 'src/widgets/bottom_bar.dart'; export 'src/widgets/bottom_bar.dart';
@@ -75,7 +76,6 @@ export 'src/widgets/chat/chat_voice_record_bar.dart';
export 'src/widgets/chat/chat_voice_record_layout.dart'; export 'src/widgets/chat/chat_voice_record_layout.dart';
export 'src/widgets/chat/chat_voice_record_view.dart'; export 'src/widgets/chat/chat_voice_record_view.dart';
export 'src/widgets/chat/chat_voice_view.dart'; export 'src/widgets/chat/chat_voice_view.dart';
export 'src/widgets/chat/chat_webview_map.dart';
export 'src/widgets/chat/new_message_indicator.dart'; export 'src/widgets/chat/new_message_indicator.dart';
export 'src/widgets/chat/water_mark_view.dart'; export 'src/widgets/chat/water_mark_view.dart';
export 'src/widgets/custom_pop_up_menu.dart'; export 'src/widgets/custom_pop_up_menu.dart';
+43 -84
View File
@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart'; import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -11,14 +10,19 @@ import 'package:sprintf/sprintf.dart';
import 'utils/api_service.dart'; import 'utils/api_service.dart';
class Apis { class Apis {
static Options get imTokenOptions => Options(headers: {'token': DataSp.imToken}); static Options get imTokenOptions =>
Options(headers: {'token': DataSp.imToken});
static Options get chatTokenOptions => Options(headers: {'token': DataSp.chatToken}); static Options get chatTokenOptions =>
Options(headers: {'token': DataSp.chatToken});
static StreamController kickoffController = StreamController<int>.broadcast(); static StreamController kickoffController = StreamController<int>.broadcast();
static void _kickoff(int? errCode) { static void _kickoff(int? errCode) {
if (errCode == 1501 || errCode == 1503 || errCode == 1504 || errCode == 1505) { if (errCode == 1501 ||
errCode == 1503 ||
errCode == 1504 ||
errCode == 1505) {
kickoffController.sink.add(errCode); kickoffController.sink.add(errCode);
} }
} }
@@ -197,43 +201,16 @@ class Apis {
int? allowBeep, int? allowBeep,
int? allowVibration, int? allowVibration,
}) async { }) async {
try { // 原 chat `/user/update` 已随 :10008 下线。OpenIM 只同步昵称/头像;
Map<String, dynamic> param = {'userID': userID}; // 邮箱、性别、生日等业务字段由调用方更新本地状态,不打账号服务。
void put(String key, dynamic value) { if (userID.isEmpty) return {};
if (null != value) { if (nickname != null || faceURL != null) {
param[key] = value; await OpenIM.iMManager.userManager.setSelfInfo(
} nickname: nickname,
} faceURL: faceURL,
put('account', account);
put('phoneNumber', phoneNumber);
put('areaCode', areaCode);
put('email', email);
put('nickname', nickname);
put('faceURL', faceURL);
put('gender', gender);
put('gender', gender);
put('level', level);
put('birth', birth);
put('allowAddFriend', allowAddFriend);
put('allowBeep', allowBeep);
put('allowVibration', allowVibration);
return HttpUtil.post(
Urls.updateUserInfo,
data: {
...param,
'platform': IMUtils.getPlatform(),
},
options: chatTokenOptions,
); );
} catch (e, s) {
final t = e as (int, String?);
final errCode = t.$1;
final errMsg = t.$2;
_kickoff(errCode);
Logger.print('e:$errCode s:$errMsg');
} }
return {};
} }
static Future<List<FriendInfo>> searchFriendInfo( static Future<List<FriendInfo>> searchFriendInfo(
@@ -251,7 +228,9 @@ class Apis {
options: chatTokenOptions, options: chatTokenOptions,
); );
if (data['users'] is List) { if (data['users'] is List) {
return (data['users'] as List).map((e) => FriendInfo.fromJson(e)).toList(); return (data['users'] as List)
.map((e) => FriendInfo.fromJson(e))
.toList();
} }
return []; return [];
} catch (e, s) { } catch (e, s) {
@@ -269,26 +248,16 @@ class Apis {
int showNumber = 10, int showNumber = 10,
required List<String> userIDList, required List<String> userIDList,
}) async { }) async {
if (userIDList.isEmpty) return [];
try { try {
final data = await HttpUtil.post( final users = await OpenIM.iMManager.userManager.getUsersInfo(
Urls.getUsersFullInfo, userIDList: userIDList,
data: {
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
'userIDs': userIDList,
'platform': IMUtils.getPlatform(),
},
options: chatTokenOptions,
); );
if (data['users'] is List) { return users
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList(); .map((e) => UserFullInfoMapper.fromImJson(e.toJson()))
} .toList();
return null;
} catch (e, s) { } catch (e, s) {
final t = e as (int, String?); Logger.print('getUserFullInfo e:$e s:$s');
final errCode = t.$1;
final errMsg = t.$2;
_kickoff(errCode);
Logger.print('e:$errCode s:$errMsg');
return []; return [];
} }
} }
@@ -298,34 +267,20 @@ class Apis {
int pageNumber = 1, int pageNumber = 1,
int showNumber = 10, int showNumber = 10,
}) async { }) async {
try { final keyword = content.trim();
final data = await HttpUtil.post( if (keyword.isEmpty) return [];
Urls.searchUserFullInfo, if (pageNumber > 1) return [];
data: { return getUserFullInfo(userIDList: [keyword], showNumber: showNumber);
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
'keyword': content,
},
options: chatTokenOptions,
);
if (data['users'] is List) {
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
}
return null;
} catch (e, s) {
final t = e as (int, String?);
final errCode = t.$1;
final errMsg = t.$2;
_kickoff(errCode);
Logger.print('e:$errCode s:$errMsg');
return [];
}
} }
static Future<UserFullInfo?> queryMyFullInfo() async { static Future<UserFullInfo?> queryMyFullInfo() async {
final list = await Apis.getUserFullInfo( try {
userIDList: [OpenIM.iMManager.userID], final user = await OpenIM.iMManager.userManager.getSelfUserInfo();
); return UserFullInfoMapper.fromImJson(user.toJson());
return list?.firstOrNull; } catch (e, s) {
Logger.print('queryMyFullInfo e:$e s:$s');
return null;
}
} }
static Future<bool> requestVerificationCode({ static Future<bool> requestVerificationCode({
@@ -353,7 +308,8 @@ class Apis {
}); });
} }
static Future<SignalingCertificate> getTokenForRTC(String roomID, String userID) async { static Future<SignalingCertificate> getTokenForRTC(
String roomID, String userID) async {
return HttpUtil.post( return HttpUtil.post(
Urls.getTokenForRTC, Urls.getTokenForRTC,
data: { data: {
@@ -410,7 +366,10 @@ class Apis {
} }
static Future<Map<String, dynamic>> getClientConfig() async { static Future<Map<String, dynamic>> getClientConfig() async {
return {'discoverPageURL': Config.discoverPageURL, 'allowSendMsgNotFriend': Config.allowSendMsgNotFriend}; return {
'discoverPageURL': Config.discoverPageURL,
'allowSendMsgNotFriend': Config.allowSendMsgNotFriend
};
} }
static void _catchError(Object e, StackTrace s, {bool forceBack = true}) { static void _catchError(Object e, StackTrace s, {bool forceBack = true}) {
@@ -71,6 +71,7 @@ const Map<String, String> en_US = {
"video": "Video", "video": "Video",
"voice": "Voice", "voice": "Voice",
"location": "Location", "location": "Location",
"locationMessage": "Location",
"file": "File", "file": "File",
"carte": "Card", "carte": "Card",
"emoji": "Custom Emoji", "emoji": "Custom Emoji",
@@ -120,7 +121,7 @@ const Map<String, String> en_US = {
"cancel": "Cancel", "cancel": "Cancel",
"determine": "OK", "determine": "OK",
"toolboxAlbum": "Album", "toolboxAlbum": "Album",
"toolboxCall": "Video Call", "toolboxCall": "Voice Call",
"toolboxCamera": "Camera", "toolboxCamera": "Camera",
"toolboxCard": "Card", "toolboxCard": "Card",
"toolboxFile": "File", "toolboxFile": "File",
@@ -224,7 +225,7 @@ const Map<String, String> en_US = {
'position': 'Position', 'position': 'Position',
'personalInfo': 'Personal Info', 'personalInfo': 'Personal Info',
'viewDynamics': 'View Dynamics', 'viewDynamics': 'View Dynamics',
'audioAndVideoCall': 'Call', 'audioAndVideoCall': 'Voice Call',
'sendMessage': 'Message', 'sendMessage': 'Message',
'avatar': 'Avatar', 'avatar': 'Avatar',
'name': 'Name', 'name': 'Name',
@@ -71,6 +71,7 @@ const Map<String, String> zh_CN = {
"video": "视频", "video": "视频",
"voice": "语音", "voice": "语音",
"location": "位置", "location": "位置",
"locationMessage": "位置消息",
"file": "文件", "file": "文件",
"carte": "名片", "carte": "名片",
"emoji": "自定义表情", "emoji": "自定义表情",
@@ -120,7 +121,7 @@ const Map<String, String> zh_CN = {
"cancel": "取消", "cancel": "取消",
"determine": "确定", "determine": "确定",
"toolboxAlbum": "相册", "toolboxAlbum": "相册",
"toolboxCall": "视频通话", "toolboxCall": "语音通话",
"toolboxCamera": "拍摄", "toolboxCamera": "拍摄",
"toolboxCard": "名片", "toolboxCard": "名片",
"toolboxFile": "文件", "toolboxFile": "文件",
@@ -224,7 +225,7 @@ const Map<String, String> zh_CN = {
'position': '职位', 'position': '职位',
'personalInfo': '个人资料', 'personalInfo': '个人资料',
'viewDynamics': '查看动态', 'viewDynamics': '查看动态',
'audioAndVideoCall': '视频通话', 'audioAndVideoCall': '音通话',
'sendMessage': '发消息', 'sendMessage': '发消息',
'avatar': '头像', 'avatar': '头像',
'name': '姓名', 'name': '姓名',
@@ -58,7 +58,8 @@ class StrRes {
static String get resendVerificationCode => 'resendVerificationCode'.tr; static String get resendVerificationCode => 'resendVerificationCode'.tr;
static String get verificationCodeTimingReminder => 'verificationCodeTimingReminder'.tr; static String get verificationCodeTimingReminder =>
'verificationCodeTimingReminder'.tr;
static String get defaultVerificationCode => 'defaultVerificationCode'.tr; static String get defaultVerificationCode => 'defaultVerificationCode'.tr;
@@ -160,6 +161,8 @@ class StrRes {
static String get location => 'location'.tr; static String get location => 'location'.tr;
static String get locationMessage => 'locationMessage'.tr;
static String get file => 'file'.tr; static String get file => 'file'.tr;
static String get carte => 'carte'.tr; static String get carte => 'carte'.tr;
@@ -276,7 +279,8 @@ class StrRes {
static String get releaseToSend => 'releaseToSend'.tr; static String get releaseToSend => 'releaseToSend'.tr;
static String get releaseToSendSwipeUpToCancel => 'releaseToSendSwipeUpToCancel'.tr; static String get releaseToSendSwipeUpToCancel =>
'releaseToSendSwipeUpToCancel'.tr;
static String get liftFingerToCancelSend => 'liftFingerToCancelSend'.tr; static String get liftFingerToCancelSend => 'liftFingerToCancelSend'.tr;
@@ -704,7 +708,8 @@ class StrRes {
static String get confirm => 'confirm'.tr; static String get confirm => 'confirm'.tr;
static String get confirmTransferGroupToUser => 'confirmTransferGroupToUser'.tr; static String get confirmTransferGroupToUser =>
'confirmTransferGroupToUser'.tr;
static String get removeGroupMember => 'removeGroupMember'.tr; static String get removeGroupMember => 'removeGroupMember'.tr;
@@ -970,7 +975,8 @@ class StrRes {
static String get confirmTheChanges => 'confirmTheChanges'.tr; static String get confirmTheChanges => 'confirmTheChanges'.tr;
static String get invitesYouToVideoConference => 'invitesYouToVideoConference'.tr; static String get invitesYouToVideoConference =>
'invitesYouToVideoConference'.tr;
static String get over => 'over'.tr; static String get over => 'over'.tr;
@@ -1074,7 +1080,8 @@ class StrRes {
static String get sendAnother => 'sendAnother'.tr; static String get sendAnother => 'sendAnother'.tr;
static String get confirmDelTagNotificationHint => 'confirmDelTagNotificationHint'.tr; static String get confirmDelTagNotificationHint =>
'confirmDelTagNotificationHint'.tr;
static String get contentNotBlank => 'contentNotBlank'.tr; static String get contentNotBlank => 'contentNotBlank'.tr;
@@ -1086,11 +1093,13 @@ class StrRes {
static String get groupRequestHandled => 'groupRequestHandled'.tr; static String get groupRequestHandled => 'groupRequestHandled'.tr;
static String get burnAfterReadingDescription => 'burnAfterReadingDescription'.tr; static String get burnAfterReadingDescription =>
'burnAfterReadingDescription'.tr;
static String get periodicallyDeleteMessage => 'periodicallyDeleteMessage'.tr; static String get periodicallyDeleteMessage => 'periodicallyDeleteMessage'.tr;
static String get periodicallyDeleteMessageDescription => 'periodicallyDeleteMessageDescription'.tr; static String get periodicallyDeleteMessageDescription =>
'periodicallyDeleteMessageDescription'.tr;
static String get nDay => 'nDay'.tr; static String get nDay => 'nDay'.tr;
@@ -5,6 +5,8 @@ class Urls {
"${Config.imApiUrl}/manager/get_users_online_status"; "${Config.imApiUrl}/manager/get_users_online_status";
static String get queryAllUsers => static String get queryAllUsers =>
"${Config.imApiUrl}/manager/get_all_users_uid"; "${Config.imApiUrl}/manager/get_all_users_uid";
// 以下 chat 路径仅作历史对照。账号服务 :10010 没有这些路由;
// 用户资料读写已改走 OpenIM SDK,见 Apis.getUserFullInfo / updateUserInfo。
static String get updateUserInfo => "${Config.appAuthUrl}/user/update"; static String get updateUserInfo => "${Config.appAuthUrl}/user/update";
static String get searchFriendInfo => "${Config.appAuthUrl}/friend/search"; static String get searchFriendInfo => "${Config.appAuthUrl}/friend/search";
static String get getUsersFullInfo => "${Config.appAuthUrl}/user/find/full"; static String get getUsersFullInfo => "${Config.appAuthUrl}/user/find/full";
@@ -10,10 +10,6 @@ import 'package:sprintf/sprintf.dart';
class Permissions { class Permissions {
Permissions._(); Permissions._();
static Future<bool> checkSystemAlertWindow() async {
return Permission.systemAlertWindow.isGranted;
}
static Future<bool> checkStorage() async { static Future<bool> checkStorage() async {
return await Permission.storage.isGranted; return await Permission.storage.isGranted;
} }
@@ -22,7 +18,8 @@ class Permissions {
if (await Permission.camera.request().isGranted) { if (await Permission.camera.request().isGranted) {
onGranted?.call(); onGranted?.call();
} }
if (await Permission.camera.isPermanentlyDenied || await Permission.camera.isDenied) { if (await Permission.camera.isPermanentlyDenied ||
await Permission.camera.isDenied) {
_showPermissionDeniedDialog(Permission.camera.title); _showPermissionDeniedDialog(Permission.camera.title);
} }
} }
@@ -52,7 +49,8 @@ class Permissions {
if (await Permission.manageExternalStorage.request().isGranted) { if (await Permission.manageExternalStorage.request().isGranted) {
onGranted?.call(); onGranted?.call();
} }
if (await Permission.storage.isPermanentlyDenied || await Permission.storage.isDenied) { if (await Permission.storage.isPermanentlyDenied ||
await Permission.storage.isDenied) {
_showPermissionDeniedDialog(Permission.storage.title); _showPermissionDeniedDialog(Permission.storage.title);
} }
} }
@@ -61,25 +59,18 @@ class Permissions {
if (await Permission.microphone.request().isGranted) { if (await Permission.microphone.request().isGranted) {
onGranted?.call(); onGranted?.call();
} }
if (await Permission.microphone.isPermanentlyDenied || await Permission.microphone.isDenied) { if (await Permission.microphone.isPermanentlyDenied ||
await Permission.microphone.isDenied) {
_showPermissionDeniedDialog(Permission.microphone.title); _showPermissionDeniedDialog(Permission.microphone.title);
} }
} }
static void location(Function()? onGranted) async {
if (await Permission.location.request().isGranted) {
onGranted?.call();
}
if (await Permission.location.isPermanentlyDenied || await Permission.location.isDenied) {
_showPermissionDeniedDialog(Permission.location.title);
}
}
static void speech(Function()? onGranted) async { static void speech(Function()? onGranted) async {
if (await Permission.speech.request().isGranted) { if (await Permission.speech.request().isGranted) {
onGranted?.call(); onGranted?.call();
} }
if (await Permission.speech.isPermanentlyDenied || await Permission.speech.isDenied) { if (await Permission.speech.isPermanentlyDenied ||
await Permission.speech.isDenied) {
_showPermissionDeniedDialog(Permission.speech.title); _showPermissionDeniedDialog(Permission.speech.title);
} }
} }
@@ -93,7 +84,8 @@ class Permissions {
if (await Permission.photos.request().isGranted) { if (await Permission.photos.request().isGranted) {
onGranted?.call(); onGranted?.call();
} }
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) { if (await Permission.photos.isPermanentlyDenied ||
await Permission.photos.isDenied) {
_showPermissionDeniedDialog(Permission.photos.title); _showPermissionDeniedDialog(Permission.photos.title);
} }
} }
@@ -101,7 +93,8 @@ class Permissions {
if (await Permission.photos.request().isGranted) { if (await Permission.photos.request().isGranted) {
onGranted?.call(); onGranted?.call();
} }
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) { if (await Permission.photos.isPermanentlyDenied ||
await Permission.photos.isDenied) {
_showPermissionDeniedDialog(Permission.photos.title); _showPermissionDeniedDialog(Permission.photos.title);
} }
} }
@@ -111,20 +104,14 @@ class Permissions {
if (await Permission.notification.request().isGranted) { if (await Permission.notification.request().isGranted) {
return true; return true;
} }
if (await Permission.notification.isPermanentlyDenied || await Permission.notification.isDenied) { if (await Permission.notification.isPermanentlyDenied ||
await Permission.notification.isDenied) {
_showPermissionDeniedDialog(Permission.notification.title); _showPermissionDeniedDialog(Permission.notification.title);
} }
return false; return false;
} }
static void ignoreBatteryOptimizations(Function()? onGranted) async {
if (await Permission.ignoreBatteryOptimizations.request().isGranted) {
onGranted?.call();
}
if (await Permission.ignoreBatteryOptimizations.isPermanentlyDenied) {}
}
static void cameraAndMicrophone(Function()? onGranted) async { static void cameraAndMicrophone(Function()? onGranted) async {
final permissions = [ final permissions = [
Permission.camera, Permission.camera,
@@ -214,7 +201,8 @@ class Permissions {
} }
} }
static Future<Map<Permission, PermissionStatus>> request(List<Permission> permissions) async { static Future<Map<Permission, PermissionStatus>> request(
List<Permission> permissions) async {
Map<Permission, PermissionStatus> statuses = await permissions.request(); Map<Permission, PermissionStatus> statuses = await permissions.request();
return statuses; return statuses;
} }
@@ -0,0 +1,24 @@
import '../models/user_full_info.dart';
/// 把 OpenIM 公开资料收成原 chat 服务 `UserFullInfo` 形状。
///
/// 官方 `open-im-chat`:10008 `/user/find/full`)已下线,账号服务 :10010
/// 没有这条路由。SDK 只保证 userID / nickname / faceURL / ex / 勿扰。
class UserFullInfoMapper {
UserFullInfoMapper._();
static UserFullInfo fromImJson(Map<String, dynamic> json) {
return UserFullInfo.fromJson({
'userID': json['userID'],
'nickname': json['nickname'],
'faceURL': json['faceURL'],
'ex': json['ex'],
'remark': json['remark'],
'globalRecvMsgOpt': json['globalRecvMsgOpt'],
});
}
static List<UserFullInfo> fromImList(Iterable<Map<String, dynamic>> items) {
return items.map(fromImJson).toList();
}
}
@@ -45,7 +45,8 @@ class IntervalDo {
void run({required Function() fuc, int milliseconds = 0}) { void run({required Function() fuc, int milliseconds = 0}) {
DateTime now = DateTime.now(); DateTime now = DateTime.now();
if (null == last || now.difference(last ?? now).inMilliseconds > milliseconds) { if (null == last ||
now.difference(last ?? now).inMilliseconds > milliseconds) {
last = now; last = now;
fuc(); fuc();
} }
@@ -153,12 +154,14 @@ class IMUtils {
} }
} }
static String? emptyStrToNull(String? str) => (null != str && str.trim().isEmpty) ? null : str; static String? emptyStrToNull(String? str) =>
(null != str && str.trim().isEmpty) ? null : str;
static bool isNotNullEmptyStr(String? str) => null != str && "" != str.trim(); static bool isNotNullEmptyStr(String? str) => null != str && "" != str.trim();
static bool isChinaMobile(String mobile) { static bool isChinaMobile(String mobile) {
RegExp exp = RegExp(r'^((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(17[0-9])|(18[0-9])|(19[0-9]))\d{8}$'); RegExp exp = RegExp(
r'^((13[0-9])|(14[0-9])|(15[0-9])|(16[0-9])|(17[0-9])|(18[0-9])|(19[0-9]))\d{8}$');
return exp.hasMatch(mobile); return exp.hasMatch(mobile);
} }
@@ -172,14 +175,17 @@ class IMUtils {
final directory = await createTempDir(dir: 'video'); final directory = await createTempDir(dir: 'video');
final targetPath = '$directory/$name'; final targetPath = '$directory/$name';
final String ffmpegCommand = '-i $path -ss 0 -vframes 1 -q:v 15 -y $targetPath'; final String ffmpegCommand =
'-i $path -ss 0 -vframes 1 -q:v 15 -y $targetPath';
final session = await FFmpegKit.execute(ffmpegCommand); final session = await FFmpegKit.execute(ffmpegCommand);
final state = FFmpegKitConfig.sessionStateToString(await session.getState()); final state =
FFmpegKitConfig.sessionStateToString(await session.getState());
final returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
if (state == SessionState.failed || !ReturnCode.isSuccess(returnCode)) { if (state == SessionState.failed || !ReturnCode.isSuccess(returnCode)) {
Logger().printError(info: "Command failed. Please check output for the details."); Logger().printError(
info: "Command failed. Please check output for the details.");
} }
session.cancel(); session.cancel();
@@ -195,11 +201,14 @@ class IMUtils {
final output = await FFprobeKit.getMediaInformation(path); final output = await FFprobeKit.getMediaInformation(path);
final streams = output.getMediaInformation()?.getStreams(); final streams = output.getMediaInformation()?.getStreams();
final isH264 = streams?.any((element) => element.getCodec()?.contains('h264') == true) ?? false; final isH264 = streams
?.any((element) => element.getCodec()?.contains('h264') == true) ??
false;
final size = output.getMediaInformation()?.getSize() ?? '0'; final size = output.getMediaInformation()?.getSize() ?? '0';
output.cancel(); output.cancel();
final audioStream = streams?.firstWhereOrNull((e) => e.getType()?.contains('audio') == true); final audioStream = streams
?.firstWhereOrNull((e) => e.getType()?.contains('audio') == true);
final isAAC = audioStream?.getCodec()?.toLowerCase() != 'aac'; final isAAC = audioStream?.getCodec()?.toLowerCase() != 'aac';
String ffmpegCommand = String ffmpegCommand =
@@ -210,7 +219,8 @@ class IMUtils {
if (isAAC) { if (isAAC) {
return File(targetPath); return File(targetPath);
} else { } else {
ffmpegCommand = '-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath'; ffmpegCommand =
'-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath';
} }
} }
@@ -220,7 +230,8 @@ class IMUtils {
return File(targetPath); return File(targetPath);
} else { } else {
ffmpegCommand = '-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath'; ffmpegCommand =
'-i $path -c:v copy -c:a aac -q:a 2 -threads 4 $targetPath';
} }
} }
@@ -230,7 +241,8 @@ class IMUtils {
final returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
if (state == SessionState.failed || !ReturnCode.isSuccess(returnCode)) { if (state == SessionState.failed || !ReturnCode.isSuccess(returnCode)) {
Logger().printError(info: "Command failed. Please check output for the details."); Logger().printError(
info: "Command failed. Please check output for the details.");
file.copySync(targetPath); file.copySync(targetPath);
return File(targetPath); return File(targetPath);
@@ -241,7 +253,8 @@ class IMUtils {
return File(targetPath); return File(targetPath);
} }
static Future<File?> compressImageAndGetFile(File file, {int quality = 80}) async { static Future<File?> compressImageAndGetFile(File file,
{int quality = 80}) async {
var path = file.path; var path = file.path;
var name = path.substring(path.lastIndexOf("/") + 1).toLowerCase(); var name = path.substring(path.lastIndexOf("/") + 1).toLowerCase();
@@ -360,14 +373,16 @@ class IMUtils {
String? externalStorageDirPath; String? externalStorageDirPath;
if (Platform.isAndroid) { if (Platform.isAndroid) {
try { try {
externalStorageDirPath = await PathProviderPlatform.instance.getDownloadsPath(); externalStorageDirPath =
await PathProviderPlatform.instance.getDownloadsPath();
} catch (err, st) { } catch (err, st) {
Logger.print('failed to get downloads path: $err, $st'); Logger.print('failed to get downloads path: $err, $st');
final directory = await getExternalStorageDirectory(); final directory = await getExternalStorageDirectory();
externalStorageDirPath = directory?.path; externalStorageDirPath = directory?.path;
} }
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
externalStorageDirPath = (await getApplicationDocumentsDirectory()).absolute.path; externalStorageDirPath =
(await getApplicationDocumentsDirectory()).absolute.path;
} }
return externalStorageDirPath!; return externalStorageDirPath!;
} }
@@ -384,7 +399,8 @@ class IMUtils {
return path; return path;
} }
static List<Message> calChatTimeInterval(List<Message> list, {bool calculate = true}) { static List<Message> calChatTimeInterval(List<Message> list,
{bool calculate = true}) {
if (!calculate) return list; if (!calculate) return list;
var milliseconds = list.firstOrNull?.sendTime; var milliseconds = list.firstOrNull?.sendTime;
if (null == milliseconds) return list; if (null == milliseconds) return list;
@@ -421,7 +437,9 @@ class IMUtils {
final yesterday = now.subtract(Duration(days: 1)); final yesterday = now.subtract(Duration(days: 1));
if (isSameDay(dateTime, yesterday)) { if (isSameDay(dateTime, yesterday)) {
return isChinese ? '昨天 ${formatter.format(dateTime)}' : 'Yesterday ${formatter.format(dateTime)}'; return isChinese
? '昨天 ${formatter.format(dateTime)}'
: 'Yesterday ${formatter.format(dateTime)}';
} }
if (isSameWeek(dateTime, now)) { if (isSameWeek(dateTime, now)) {
@@ -448,13 +466,16 @@ class IMUtils {
} }
static bool isSameDay(DateTime date1, DateTime date2) { static bool isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day; return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
} }
static bool isSameWeek(DateTime date1, DateTime date2) { static bool isSameWeek(DateTime date1, DateTime date2) {
final weekStart = date2.subtract(Duration(days: date2.weekday - 1)); final weekStart = date2.subtract(Duration(days: date2.weekday - 1));
final weekEnd = weekStart.add(Duration(days: 6)); final weekEnd = weekStart.add(Duration(days: 6));
return date1.isAfter(weekStart.subtract(Duration(days: 1))) && date1.isBefore(weekEnd.add(Duration(days: 1))); return date1.isAfter(weekStart.subtract(Duration(days: 1))) &&
date1.isBefore(weekEnd.add(Duration(days: 1)));
} }
static String getCallTimeline(int milliseconds) { static String getCallTimeline(int milliseconds) {
@@ -530,7 +551,8 @@ class IMUtils {
return "${_combTime(days, StrRes.day)}${_combTime(hours, StrRes.hours)}${_combTime(minutes, StrRes.minute)}${_combTime(seconds, StrRes.seconds)}"; return "${_combTime(days, StrRes.day)}${_combTime(hours, StrRes.hours)}${_combTime(minutes, StrRes.minute)}${_combTime(seconds, StrRes.seconds)}";
} }
static String _combTime(int value, String unit) => value > 0 ? '$value$unit' : ''; static String _combTime(int value, String unit) =>
value > 0 ? '$value$unit' : '';
static String calContent({ static String calContent({
required String content, required String content,
@@ -566,9 +588,11 @@ class IMUtils {
int maxLines = 1, int maxLines = 1,
double maxWidth = double.infinity, double maxWidth = double.infinity,
}) { }) {
final TextPainter textPainter = final TextPainter textPainter = TextPainter(
TextPainter(text: TextSpan(text: text, style: style), maxLines: maxLines, textDirection: TextDirection.ltr) text: TextSpan(text: text, style: style),
..layout(minWidth: 0, maxWidth: maxWidth); maxLines: maxLines,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: maxWidth);
return textPainter.size; return textPainter.size;
} }
@@ -578,7 +602,10 @@ class IMUtils {
int maxLines = 1, int maxLines = 1,
double maxWidth = double.infinity, double maxWidth = double.infinity,
}) => }) =>
TextPainter(text: TextSpan(text: text, style: style), maxLines: maxLines, textDirection: TextDirection.ltr) TextPainter(
text: TextSpan(text: text, style: style),
maxLines: maxLines,
textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: maxWidth); ..layout(minWidth: 0, maxWidth: maxWidth);
static bool isUrlValid(String? url) { static bool isUrlValid(String? url) {
@@ -600,11 +627,16 @@ class IMUtils {
} }
static String getGroupMemberShowName(GroupMembersInfo membersInfo) { static String getGroupMemberShowName(GroupMembersInfo membersInfo) {
return membersInfo.userID == OpenIM.iMManager.userID ? StrRes.you : membersInfo.nickname!; return membersInfo.userID == OpenIM.iMManager.userID
? StrRes.you
: membersInfo.nickname!;
} }
static String getShowName(String? userID, String? nickname) { static String getShowName(String? userID, String? nickname) {
return (userID == OpenIM.iMManager.userID ? OpenIM.iMManager.userInfo.nickname : nickname) ?? ''; return (userID == OpenIM.iMManager.userID
? OpenIM.iMManager.userInfo.nickname
: nickname) ??
'';
} }
static String? parseNtf( static String? parseNtf(
@@ -628,7 +660,8 @@ class IMUtils {
case MessageType.groupInfoSetNotification: case MessageType.groupInfoSetNotification:
{ {
final ntf = GroupNotification.fromJson(map); final ntf = GroupNotification.fromJson(map);
if (ntf.group?.notification != null && ntf.group!.notification!.isNotEmpty) { if (ntf.group?.notification != null &&
ntf.group!.notification!.isNotEmpty) {
return isConversation ? ntf.group!.notification! : null; return isConversation ? ntf.group!.notification! : null;
} }
@@ -649,8 +682,12 @@ class IMUtils {
final ntf = InvitedJoinGroupNotification.fromJson(map); final ntf = InvitedJoinGroupNotification.fromJson(map);
final label = StrRes.invitedJoinGroupNtf; final label = StrRes.invitedJoinGroupNtf;
final b = ntf.invitedUserList?.map((e) => getGroupMemberShowName(e)).toList().join(''); final b = ntf.invitedUserList
text = sprintf(label, [getGroupMemberShowName(ntf.opUser!), b ?? '']); ?.map((e) => getGroupMemberShowName(e))
.toList()
.join('');
text = sprintf(
label, [getGroupMemberShowName(ntf.opUser!), b ?? '']);
} }
break; break;
case MessageType.memberKickedNotification: case MessageType.memberKickedNotification:
@@ -658,7 +695,10 @@ class IMUtils {
final ntf = KickedGroupMemeberNotification.fromJson(map); final ntf = KickedGroupMemeberNotification.fromJson(map);
final label = StrRes.kickedGroupNtf; final label = StrRes.kickedGroupNtf;
final b = ntf.kickedUserList!.map((e) => getGroupMemberShowName(e)).toList().join(''); final b = ntf.kickedUserList!
.map((e) => getGroupMemberShowName(e))
.toList()
.join('');
text = sprintf(label, [b, getGroupMemberShowName(ntf.opUser!)]); text = sprintf(label, [b, getGroupMemberShowName(ntf.opUser!)]);
} }
break; break;
@@ -683,7 +723,10 @@ class IMUtils {
final ntf = GroupRightsTransferNoticication.fromJson(map); final ntf = GroupRightsTransferNoticication.fromJson(map);
final label = StrRes.transferredGroupNtf; final label = StrRes.transferredGroupNtf;
text = sprintf(label, [getGroupMemberShowName(ntf.opUser!), getGroupMemberShowName(ntf.newGroupOwner!)]); text = sprintf(label, [
getGroupMemberShowName(ntf.opUser!),
getGroupMemberShowName(ntf.newGroupOwner!)
]);
} }
break; break;
case MessageType.groupMemberMutedNotification: case MessageType.groupMemberMutedNotification:
@@ -692,8 +735,11 @@ class IMUtils {
final label = StrRes.muteMemberNtf; final label = StrRes.muteMemberNtf;
final c = ntf.mutedSeconds; final c = ntf.mutedSeconds;
text = sprintf( text = sprintf(label, [
label, [getGroupMemberShowName(ntf.mutedUser!), getGroupMemberShowName(ntf.opUser!), mutedTime(c!)]); getGroupMemberShowName(ntf.mutedUser!),
getGroupMemberShowName(ntf.opUser!),
mutedTime(c!)
]);
} }
break; break;
case MessageType.groupMemberCancelMutedNotification: case MessageType.groupMemberCancelMutedNotification:
@@ -701,7 +747,10 @@ class IMUtils {
final ntf = MuteMemberNotification.fromJson(map); final ntf = MuteMemberNotification.fromJson(map);
final label = StrRes.muteCancelMemberNtf; final label = StrRes.muteCancelMemberNtf;
text = sprintf(label, [getGroupMemberShowName(ntf.mutedUser!), getGroupMemberShowName(ntf.opUser!)]); text = sprintf(label, [
getGroupMemberShowName(ntf.mutedUser!),
getGroupMemberShowName(ntf.opUser!)
]);
} }
break; break;
case MessageType.groupMutedNotification: case MessageType.groupMutedNotification:
@@ -737,7 +786,8 @@ class IMUtils {
break; break;
case MessageType.groupMemberInfoChangedNotification: case MessageType.groupMemberInfoChangedNotification:
final ntf = GroupMemberInfoChangedNotification.fromJson(map); final ntf = GroupMemberInfoChangedNotification.fromJson(map);
text = sprintf(StrRes.memberInfoChangedNtf, [getGroupMemberShowName(ntf.opUser!)]); text = sprintf(StrRes.memberInfoChangedNtf,
[getGroupMemberShowName(ntf.opUser!)]);
break; break;
case MessageType.groupInfoSetAnnouncementNotification: case MessageType.groupInfoSetAnnouncementNotification:
if (isConversation) { if (isConversation) {
@@ -747,7 +797,8 @@ class IMUtils {
break; break;
case MessageType.groupInfoSetNameNotification: case MessageType.groupInfoSetNameNotification:
final ntf = GroupNotification.fromJson(map); final ntf = GroupNotification.fromJson(map);
text = sprintf(StrRes.whoModifyGroupName, [getGroupMemberShowName(ntf.opUser!), ntf.group?.groupName]); text = sprintf(StrRes.whoModifyGroupName,
[getGroupMemberShowName(ntf.opUser!), ntf.group?.groupName]);
break; break;
} }
} }
@@ -851,7 +902,8 @@ class IMUtils {
switch (customType) { switch (customType) {
case CustomMessageType.call: case CustomMessageType.call:
var type = map['data']['type']; var type = map['data']['type'];
content = '[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]'; content =
'[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]';
break; break;
case CustomMessageType.emoji: case CustomMessageType.emoji:
content = '[${StrRes.emoji}]'; content = '[${StrRes.emoji}]';
@@ -924,7 +976,8 @@ class IMUtils {
switch (state) { switch (state) {
case 'beHangup': case 'beHangup':
case 'hangup': case 'hangup':
content = sprintf(StrRes.callDuration, [seconds2HMS(duration)]); content =
sprintf(StrRes.callDuration, [seconds2HMS(duration)]);
break; break;
case 'cancel': case 'cancel':
content = StrRes.cancelled; content = StrRes.cancelled;
@@ -991,8 +1044,11 @@ class IMUtils {
final atUserInfos = message.atTextElem!.atUsersInfo!; final atUserInfos = message.atTextElem!.atUsersInfo!;
for (final userID in atUserIDs) { for (final userID in atUserIDs) {
final groupNickname = final groupNickname = (newMapping[userID] ??
(newMapping[userID] ?? atUserInfos.firstWhere((e) => e.atUserID == userID).groupNickname) ?? userID; atUserInfos
.firstWhere((e) => e.atUserID == userID)
.groupNickname) ??
userID;
mapping[userID] = getAtNickname(userID, groupNickname); mapping[userID] = getAtNickname(userID, groupNickname);
} }
} }
@@ -1065,20 +1121,27 @@ class IMUtils {
previewUrlPicture( previewUrlPicture(
[ [
MediaSource( MediaSource(
url: message.pictureElem!.sourcePicture!.url!, thumbnail: message.pictureElem!.snapshotPicture!.url!) url: message.pictureElem!.sourcePicture!.url!,
thumbnail: message.pictureElem!.snapshotPicture!.url!)
], ],
currentIndex: 0, currentIndex: 0,
); );
} else { } else {
final picList = allList final picList = allList
.where((element) => element.contentType == MessageType.picture || element.contentType == MessageType.video) .where((element) =>
element.contentType == MessageType.picture ||
element.contentType == MessageType.video)
.toList(); .toList();
final index = picList.indexOf(message); final index = picList.indexOf(message);
final urls = picList.map((e) { final urls = picList.map((e) {
if (e.contentType == MessageType.picture) { if (e.contentType == MessageType.picture) {
return MediaSource(url: e.pictureElem!.sourcePicture!.url!, thumbnail: e.pictureElem!.snapshotPicture!.url!); return MediaSource(
url: e.pictureElem!.sourcePicture!.url!,
thumbnail: e.pictureElem!.snapshotPicture!.url!);
} else { } else {
return MediaSource(url: e.videoElem!.videoUrl!, thumbnail: e.videoElem!.snapshotUrl!); return MediaSource(
url: e.videoElem!.videoUrl!,
thumbnail: e.videoElem!.snapshotUrl!);
} }
}).toList(); }).toList();
previewUrlPicture(urls, currentIndex: index == -1 ? 0 : index); previewUrlPicture(urls, currentIndex: index == -1 ? 0 : index);
@@ -1106,7 +1169,8 @@ class IMUtils {
const begin = Offset(0.0, 1.0); const begin = Offset(0.0, 1.0);
const end = Offset.zero; const end = Offset.zero;
const curve = Curves.easeOut; const curve = Curves.easeOut;
final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); final tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
final offsetAnimation = animation.drive(tween); final offsetAnimation = animation.drive(tween);
return SlideTransition( return SlideTransition(
@@ -1136,7 +1200,8 @@ class IMUtils {
final isExitCachePath = await isExitFile(cachePath); final isExitCachePath = await isExitFile(cachePath);
Logger.print('isExitSourcePath:$isExitSourcePath, isExitCachePath:$isExitCachePath, cachePath:$cachePath'); Logger.print(
'isExitSourcePath:$isExitSourcePath, isExitCachePath:$isExitCachePath, cachePath:$cachePath');
final isExitNetwork = isUrlValid(url); final isExitNetwork = isUrlValid(url);
String? availablePath; String? availablePath;
@@ -1145,9 +1210,11 @@ class IMUtils {
} else if (isExitCachePath) { } else if (isExitCachePath) {
availablePath = cachePath; availablePath = cachePath;
} }
final isAvailableFileSize = final isAvailableFileSize = isExitSourcePath || isExitCachePath
isExitSourcePath || isExitCachePath ? (await File(availablePath!).length() == fileSize) : false; ? (await File(availablePath!).length() == fileSize)
Logger.print('previewFile isAvailableFileSize: $isAvailableFileSize isExitNetwork: $isExitNetwork'); : false;
Logger.print(
'previewFile isAvailableFileSize: $isAvailableFileSize isExitNetwork: $isExitNetwork');
if (isAvailableFileSize) { if (isAvailableFileSize) {
String? mimeType = lookupMimeType(fileName ?? ''); String? mimeType = lookupMimeType(fileName ?? '');
if (null != mimeType && allowVideoType(mimeType)) { if (null != mimeType && allowVideoType(mimeType)) {
@@ -1159,7 +1226,9 @@ class IMUtils {
previewPicture(Message() previewPicture(Message()
..clientMsgID = message.clientMsgID ..clientMsgID = message.clientMsgID
..contentType = MessageType.picture ..contentType = MessageType.picture
..pictureElem = PictureElem(sourcePath: availablePath, sourcePicture: PictureInfo(url: url))); ..pictureElem = PictureElem(
sourcePath: availablePath,
sourcePicture: PictureInfo(url: url)));
} else { } else {
openFileByOtherApp(availablePath); openFileByOtherApp(availablePath);
} }
@@ -1183,7 +1252,8 @@ class IMUtils {
bool onlySave = false, bool onlySave = false,
ValueChanged<OperateType>? onOperate}) { ValueChanged<OperateType>? onOperate}) {
void saveVideo(BuildContext ctx, String url, {int? length}) async { void saveVideo(BuildContext ctx, String url, {int? length}) async {
final cachedVideoControllerService = CachedVideoControllerService(DefaultCacheManager()); final cachedVideoControllerService =
CachedVideoControllerService(DefaultCacheManager());
final cached = await cachedVideoControllerService.getCacheFile(url); final cached = await cachedVideoControllerService.getCacheFile(url);
if (cached != null) { if (cached != null) {
@@ -1196,7 +1266,8 @@ class IMUtils {
} else { } else {
LoadingView.singleton.show(); LoadingView.singleton.show();
final downloader = MultiThreadDownloader(url: url, fileName: url.split('/').last, length: length); final downloader = MultiThreadDownloader(
url: url, fileName: url.split('/').last, length: length);
callback(EasyLoadingStatus status) { callback(EasyLoadingStatus status) {
if (status == EasyLoadingStatus.dismiss) { if (status == EasyLoadingStatus.dismiss) {
@@ -1235,7 +1306,8 @@ class IMUtils {
switch (type) { switch (type) {
case OperateType.save: case OperateType.save:
if (msg.videoElem != null) { if (msg.videoElem != null) {
saveVideo(context, msg.videoElem!.videoUrl!, length: msg.videoElem!.videoSize); saveVideo(context, msg.videoElem!.videoUrl!,
length: msg.videoElem!.videoSize);
} else { } else {
final url = msg.pictureElem?.sourcePicture?.url; final url = msg.pictureElem?.sourcePicture?.url;
if (url?.isNotEmpty == true) { if (url?.isNotEmpty == true) {
@@ -1254,14 +1326,18 @@ class IMUtils {
final sources = message.isVideoType final sources = message.isVideoType
? MediaSource( ? MediaSource(
url: message.videoElem?.videoUrl, url: message.videoElem?.videoUrl,
thumbnail: message.videoElem!.snapshotUrl?.adjustThumbnailAbsoluteString(960) ?? '', thumbnail: message.videoElem!.snapshotUrl
?.adjustThumbnailAbsoluteString(960) ??
'',
file: File(message.videoElem!.videoPath!), file: File(message.videoElem!.videoPath!),
tag: message.clientMsgID, tag: message.clientMsgID,
isVideo: true, isVideo: true,
) )
: MediaSource( : MediaSource(
url: message.pictureElem?.sourcePicture?.url, url: message.pictureElem?.sourcePicture?.url,
thumbnail: message.pictureElem!.snapshotPicture?.url?.adjustThumbnailAbsoluteString(960) ?? '', thumbnail: message.pictureElem!.snapshotPicture?.url
?.adjustThumbnailAbsoluteString(960) ??
'',
file: File(message.pictureElem!.sourcePath!), file: File(message.pictureElem!.sourcePath!),
tag: message.clientMsgID, tag: message.clientMsgID,
); );
@@ -1326,19 +1402,36 @@ class IMUtils {
} }
static void previewLocation(Message message) { static void previewLocation(Message message) {
var location = message.locationElem; try {
Map detail = json.decode(location!.description!); final location = message.locationElem;
Logger.print('previewLocation ${location.latitude} ${location.longitude}'); if (location == null ||
Get.to( location.latitude == null ||
() => MapView( location.longitude == null) {
IMViews.showToast(StrRes.locationMessage);
return;
}
final data = LocationBubbleData.parse(
description: location.description ?? '',
latitude: location.latitude!, latitude: location.latitude!,
longitude: location.longitude!, longitude: location.longitude!,
address1: detail['name'], fallbackTitle: StrRes.locationMessage,
address2: detail['addr'], );
), Logger.print(
transition: Transition.cupertino, 'previewLocation ${location.latitude} ${location.longitude}');
popGesture: true, Get.to(
); () => MapView(
latitude: location.latitude!,
longitude: location.longitude!,
address1: data.title,
address2: data.address,
),
transition: Transition.cupertino,
popGesture: true,
);
} catch (e, s) {
Logger.print('previewLocation failed: $e $s');
IMViews.showToast(StrRes.locationMessage);
}
} }
static void previewCarteMessage( static void previewCarteMessage(
@@ -1353,7 +1446,8 @@ class IMUtils {
Function(Message msg)? meetingItemClick, Function(Message msg)? meetingItemClick,
VoidCallback? onForward, VoidCallback? onForward,
}) async { }) async {
if (message.contentType == MessageType.picture || message.contentType == MessageType.video) { if (message.contentType == MessageType.picture ||
message.contentType == MessageType.video) {
previewMediaFile( previewMediaFile(
context: Get.context!, context: Get.context!,
message: message, message: message,
@@ -1464,15 +1558,18 @@ class IMUtils {
if (mimeType == 'application/pdf') { if (mimeType == 'application/pdf') {
return ImageRes.filePdf; return ImageRes.filePdf;
} else if (mimeType == 'application/msword' || } else if (mimeType == 'application/msword' ||
mimeType == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { mimeType ==
'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
return ImageRes.fileWord; return ImageRes.fileWord;
} else if (mimeType == 'application/vnd.ms-excel' || } else if (mimeType == 'application/vnd.ms-excel' ||
mimeType == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { mimeType ==
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
return ImageRes.fileExcel; return ImageRes.fileExcel;
} else if (mimeType == 'application/vnd.ms-powerpoint') { } else if (mimeType == 'application/vnd.ms-powerpoint') {
return ImageRes.filePpt; return ImageRes.filePpt;
} else if (mimeType.startsWith('audio/')) { } else if (mimeType.startsWith('audio/')) {
} else if (mimeType == 'application/zip' || mimeType == 'application/x-rar-compressed') { } else if (mimeType == 'application/zip' ||
mimeType == 'application/x-rar-compressed') {
return ImageRes.fileZip; return ImageRes.fileZip;
} }
/*else if (mimeType.startsWith('audio/')) { /*else if (mimeType.startsWith('audio/')) {
@@ -1516,7 +1613,10 @@ class IMUtils {
final checkedList = <String>[]; final checkedList = <String>[];
final values = result.values; final values = result.values;
for (final value in values) { for (final value in values) {
if (value is UserInfo || value is FriendInfo || value is UserFullInfo || value is ISUserInfo) { if (value is UserInfo ||
value is FriendInfo ||
value is UserFullInfo ||
value is ISUserInfo) {
checkedList.add(value.userID!); checkedList.add(value.userID!);
} }
} }
@@ -1531,7 +1631,10 @@ class IMUtils {
for (var item in checkedList) { for (var item in checkedList) {
if (item is ConversationInfo) { if (item is ConversationInfo) {
checkedMap[item.isSingleChat ? item.userID! : item.groupID!] = item; checkedMap[item.isSingleChat ? item.userID! : item.groupID!] = item;
} else if (item is UserInfo || item is UserFullInfo || item is ISUserInfo || item is FriendInfo) { } else if (item is UserInfo ||
item is UserFullInfo ||
item is ISUserInfo ||
item is FriendInfo) {
checkedMap[item.userID!] = item; checkedMap[item.userID!] = item;
} else if (item is GroupInfo) { } else if (item is GroupInfo) {
checkedMap[item.groupID] = item; checkedMap[item.groupID] = item;
@@ -1540,10 +1643,14 @@ class IMUtils {
return checkedMap; return checkedMap;
} }
static List<Map<String, String?>> convertCheckedListToForwardObj(List<dynamic> checkedList) { static List<Map<String, String?>> convertCheckedListToForwardObj(
List<dynamic> checkedList) {
final map = <Map<String, String?>>[]; final map = <Map<String, String?>>[];
for (var item in checkedList) { for (var item in checkedList) {
if (item is UserInfo || item is UserFullInfo || item is ISUserInfo || item is FriendInfo) { if (item is UserInfo ||
item is UserFullInfo ||
item is ISUserInfo ||
item is FriendInfo) {
map.add({'nickname': item.nickname, 'faceURL': item.faceURL}); map.add({'nickname': item.nickname, 'faceURL': item.faceURL});
} else if (item is GroupInfo) { } else if (item is GroupInfo) {
map.add({'nickname': item.groupName, 'faceURL': item.faceURL}); map.add({'nickname': item.groupName, 'faceURL': item.faceURL});
@@ -1555,7 +1662,10 @@ class IMUtils {
} }
static String? convertCheckedToUserID(dynamic info) { static String? convertCheckedToUserID(dynamic info) {
if (info is UserInfo || info is UserFullInfo || info is ISUserInfo || info is FriendInfo) { if (info is UserInfo ||
info is UserFullInfo ||
info is ISUserInfo ||
info is FriendInfo) {
return info.userID; return info.userID;
} else if (info is ConversationInfo) { } else if (info is ConversationInfo) {
return info.userID; return info.userID;
@@ -1570,14 +1680,18 @@ class IMUtils {
} else if (info is ConversationInfo) { } else if (info is ConversationInfo) {
return info.groupID; return info.groupID;
} }
return null; return null;
} }
static List<Map<String, String?>> convertCheckedListToShare(Iterable<dynamic> checkedList) { static List<Map<String, String?>> convertCheckedListToShare(
Iterable<dynamic> checkedList) {
final map = <Map<String, String?>>[]; final map = <Map<String, String?>>[];
for (var item in checkedList) { for (var item in checkedList) {
if (item is UserInfo || item is UserFullInfo || item is ISUserInfo || item is FriendInfo) { if (item is UserInfo ||
item is UserFullInfo ||
item is ISUserInfo ||
item is FriendInfo) {
map.add({'userID': item.userID, 'groupID': null}); map.add({'userID': item.userID, 'groupID': null});
} else if (item is GroupInfo) { } else if (item is GroupInfo) {
map.add({'userID': null, 'groupID': item.groupID}); map.add({'userID': null, 'groupID': item.groupID});
@@ -1612,8 +1726,10 @@ class IMUtils {
return formatDateMs(ms, format: isZH ? 'yyyy年MM月dd' : 'yyyy/MM/dd'); return formatDateMs(ms, format: isZH ? 'yyyy年MM月dd' : 'yyyy/MM/dd');
} }
static Future<bool> checkingBiometric(LocalAuthentication auth) => auth.authenticate( static Future<bool> checkingBiometric(LocalAuthentication auth) =>
localizedReason: 'Scan your fingerprint (or face or other) to authenticate.', auth.authenticate(
localizedReason:
'Scan your fingerprint (or face or other) to authenticate.',
options: const AuthenticationOptions( options: const AuthenticationOptions(
biometricOnly: true, biometricOnly: true,
), ),
@@ -1636,7 +1752,8 @@ class IMUtils {
goToSettingsButton: 'Go to settings', goToSettingsButton: 'Go to settings',
goToSettingsDescription: goToSettingsDescription:
'No biometric authentication is set up on your device. Please enable Touch ID or Face ID on your phone.', 'No biometric authentication is set up on your device. Please enable Touch ID or Face ID on your phone.',
lockOut: 'Biometric authentication is disabled. Please lock and unlock your screen to enable it.', lockOut:
'Biometric authentication is disabled. Please lock and unlock your screen to enable it.',
), ),
], ],
); );
@@ -1664,11 +1781,15 @@ class IMUtils {
r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\S]{6,20}$', r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\S]{6,20}$',
).hasMatch(password); ).hasMatch(password);
static TextInputFormatter getPasswordFormatter() => FilteringTextInputFormatter.allow( static TextInputFormatter getPasswordFormatter() =>
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9\S]'), RegExp(r'[a-zA-Z0-9\S]'),
); );
static Future requestBackgroundPermission({required String title, required String text, bool isRetry = false}) async { static Future requestBackgroundPermission(
{required String title,
required String text,
bool isRetry = false}) async {
if (!Platform.isAndroid) { if (!Platform.isAndroid) {
return; return;
} }
@@ -1680,7 +1801,8 @@ class IMUtils {
notificationTitle: title, notificationTitle: title,
notificationText: text, notificationText: text,
notificationImportance: AndroidNotificationImportance.normal, notificationImportance: AndroidNotificationImportance.normal,
notificationIcon: const AndroidResource(name: 'ic_launcher', defType: 'mipmap'), notificationIcon: const AndroidResource(
name: 'ic_launcher', defType: 'mipmap'),
shouldRequestBatteryOptimizationsOff: false)); shouldRequestBatteryOptimizationsOff: false));
} }
if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) { if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) {
@@ -1689,7 +1811,9 @@ class IMUtils {
} catch (e) { } catch (e) {
if (!isRetry) { if (!isRetry) {
return await Future<void>.delayed( return await Future<void>.delayed(
const Duration(seconds: 1), () => requestBackgroundPermission(title: title, text: text, isRetry: true)); const Duration(seconds: 1),
() => requestBackgroundPermission(
title: title, text: text, isRetry: true));
} }
} }
} }
@@ -1698,5 +1822,6 @@ class IMUtils {
extension PlatformExt on Platform { extension PlatformExt on Platform {
static bool get isMobile => Platform.isIOS || Platform.isAndroid; static bool get isMobile => Platform.isIOS || Platform.isAndroid;
static bool get isDesktop => Platform.isLinux || Platform.isMacOS || Platform.isWindows; static bool get isDesktop =>
Platform.isLinux || Platform.isMacOS || Platform.isWindows;
} }
@@ -38,7 +38,9 @@ class Button extends StatelessWidget {
child: Ink( child: Ink(
height: height ?? 44.h, height: height ?? 44.h,
decoration: BoxDecoration( decoration: BoxDecoration(
color: enabled ? enabledColor ?? Styles.c_0089FF : disabledColor ?? Styles.c_0089FF_opacity50, color: enabled
? enabledColor ?? Styles.c_0089FF
: disabledColor ?? Styles.c_0089FF_opacity50,
borderRadius: BorderRadius.circular(radius ?? 4.r), borderRadius: BorderRadius.circular(radius ?? 4.r),
), ),
child: InkWell( child: InkWell(
@@ -78,7 +80,7 @@ class ImageTextButton extends StatelessWidget {
final Function()? onTap; final Function()? onTap;
ImageTextButton.call({super.key, this.onTap}) ImageTextButton.call({super.key, this.onTap})
: icon = ImageRes.audioAndVideoCall, : icon = ImageRes.callVoice,
text = StrRes.audioAndVideoCall, text = StrRes.audioAndVideoCall,
color = Styles.c_FFFFFF, color = Styles.c_FFFFFF,
textStyle = null, textStyle = null,
@@ -4,6 +4,48 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:openim_common/openim_common.dart'; import 'package:openim_common/openim_common.dart';
class LocationBubbleData {
const LocationBubbleData({
required this.title,
required this.address,
this.thumbnailUrl,
});
final String title;
final String address;
final String? thumbnailUrl;
static LocationBubbleData parse({
required String description,
required double latitude,
required double longitude,
String fallbackTitle = '位置消息',
}) {
Map<String, dynamic>? map;
try {
final decoded = json.decode(description);
if (decoded is Map) {
map = decoded.map((key, value) => MapEntry(key.toString(), value));
}
} catch (_) {}
final name = map?['name']?.toString().trim() ?? '';
final addr = map?['addr']?.toString().trim() ?? '';
final url = map?['url']?.toString().trim() ?? '';
final coord =
'${latitude.toStringAsFixed(5)}, ${longitude.toStringAsFixed(5)}';
final raw = description.trim();
return LocationBubbleData(
title: name.isNotEmpty ? name : fallbackTitle,
address: addr.isNotEmpty
? addr
: (map == null && raw.isNotEmpty ? raw : coord),
thumbnailUrl: url.isNotEmpty ? url : null,
);
}
}
class ChatLocationView extends StatelessWidget { class ChatLocationView extends StatelessWidget {
const ChatLocationView({ const ChatLocationView({
Key? key, Key? key,
@@ -14,53 +56,62 @@ class ChatLocationView extends StatelessWidget {
final String description; final String description;
final double latitude; final double latitude;
final double longitude; final double longitude;
final _decoder = const JsonDecoder();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
try { final data = LocationBubbleData.parse(
final map = _decoder.convert(description); description: description,
String url = map['url'] ?? ''; latitude: latitude,
String name = map['name'] ?? ''; longitude: longitude,
String addr = map['addr'] ?? ''; fallbackTitle: StrRes.locationMessage,
return Container( );
width: locationWidth, return Container(
height: 130.h, width: locationWidth,
decoration: BoxDecoration( height: 130.h,
color: Styles.c_FFFFFF, decoration: BoxDecoration(
border: Border.all(color: Styles.c_E8EAEF, width: 1), color: Styles.c_FFFFFF,
borderRadius: BorderRadius.circular(6.r), border: Border.all(color: Styles.c_E8EAEF, width: 1),
), borderRadius: BorderRadius.circular(6.r),
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
4.verticalSpace, children: [
Padding( 4.verticalSpace,
padding: EdgeInsets.symmetric(horizontal: 4.w), Padding(
child: name.toText padding: EdgeInsets.symmetric(horizontal: 4.w),
..style = Styles.ts_0C1C33_14sp child: data.title.toText
..maxLines = 1 ..style = Styles.ts_0C1C33_14sp
..overflow = TextOverflow.ellipsis, ..maxLines = 1
), ..overflow = TextOverflow.ellipsis,
Padding( ),
padding: EdgeInsets.symmetric(horizontal: 4.w), Padding(
child: addr.toText padding: EdgeInsets.symmetric(horizontal: 4.w),
..style = Styles.ts_8E9AB0_12sp child: data.address.toText
..maxLines = 1 ..style = Styles.ts_8E9AB0_12sp
..overflow = TextOverflow.ellipsis, ..maxLines = 1
), ..overflow = TextOverflow.ellipsis,
2.verticalSpace, ),
Expanded( 2.verticalSpace,
child: ImageUtil.networkImage( Expanded(
url: url, child: data.thumbnailUrl == null
width: locationWidth, ? ColoredBox(
fit: BoxFit.cover, color: Styles.c_F0F2F6,
), child: Center(
), child: Icon(
], Icons.location_on_outlined,
), color: Styles.c_8E9AB0,
); size: 28.w,
} catch (e) {} ),
return Container(); ),
)
: ImageUtil.networkImage(
url: data.thumbnailUrl!,
width: locationWidth,
fit: BoxFit.cover,
),
),
],
),
);
} }
} }
@@ -10,7 +10,6 @@ class ChatToolBox extends StatelessWidget {
this.onTapCamera, this.onTapCamera,
this.onTapCard, this.onTapCard,
this.onTapFile, this.onTapFile,
this.onTapLocation,
this.onTapDirectionalMessage, this.onTapDirectionalMessage,
}); });
final Function()? onTapAlbum; final Function()? onTapAlbum;
@@ -18,7 +17,6 @@ class ChatToolBox extends StatelessWidget {
final Function()? onTapCall; final Function()? onTapCall;
final Function()? onTapFile; final Function()? onTapFile;
final Function()? onTapCard; final Function()? onTapCard;
final Function()? onTapLocation;
final VoidCallback? onTapDirectionalMessage; final VoidCallback? onTapDirectionalMessage;
@override @override
@@ -50,11 +48,6 @@ class ChatToolBox extends StatelessWidget {
icon: ImageRes.toolboxCard, icon: ImageRes.toolboxCard,
onTap: onTapCard, onTap: onTapCard,
), ),
ToolboxItemInfo(
text: StrRes.toolboxLocation,
icon: ImageRes.toolboxLocation,
onTap: () => Permissions.location(onTapLocation),
),
if (onTapDirectionalMessage != null) if (onTapDirectionalMessage != null)
ToolboxItemInfo( ToolboxItemInfo(
text: StrRes.toolboxDirectionalMessage, text: StrRes.toolboxDirectionalMessage,
@@ -63,9 +56,11 @@ class ChatToolBox extends StatelessWidget {
), ),
]; ];
final rowCount = (items.length / 4).ceil().clamp(1, 2);
return Container( return Container(
color: Styles.c_F0F2F6, color: Styles.c_F0F2F6,
height: 224.h, height: rowCount <= 1 ? 118.h : 224.h,
child: GridView.builder( child: GridView.builder(
itemCount: items.length, itemCount: items.length,
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -1,269 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:openim_common/openim_common.dart';
import 'package:geolocator/geolocator.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class ChatWebViewMap extends StatefulWidget {
const ChatWebViewMap({
super.key,
required this.host,
required this.webKey,
required this.webServerKey,
this.mapThumbnailSize = "1200*600",
this.mapBackUrl = "http://callback",
this.latitude,
this.longitude,
});
final String host;
final String webKey;
final String webServerKey;
final String mapThumbnailSize;
final String mapBackUrl;
final double? latitude;
final double? longitude;
@override
State<ChatWebViewMap> createState() => _ChatWebViewMapState();
}
class _ChatWebViewMapState extends State<ChatWebViewMap> {
WebViewController? _controller;
String url = "";
double progress = 0;
double? latitude;
double? longitude;
String? description;
late String locationUrl;
late String thumbnailUrl;
late String previewLocationUrl;
late String webKey;
late String webServerKey;
late String host;
void _configUrl() {
locationUrl = "$host?key=$webKey&serverKey=$webServerKey#/";
previewLocationUrl = "$host?key=$webKey&serverKey=$webServerKey&location=$longitude,$latitude#/";
}
String getStaticMapURL(double longitude, double latitude) {
final url =
'https://restapi.amap.com/v3/staticmap?location=$longitude,$latitude&zoom=13&size=200*200&markers=mid,,A:$longitude,$latitude&key=$webServerKey';
return url;
}
bool get isPreview => widget.longitude != null && widget.latitude != null;
@override
void initState() {
super.initState();
host = widget.host;
webKey = widget.webKey;
webServerKey = widget.webServerKey;
longitude = widget.longitude;
latitude = widget.latitude;
_determinePosition().then((position) {
longitude = position.longitude;
latitude = position.latitude;
_configUrl();
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
final WebViewController controller = WebViewController.fromPlatformCreationParams(params);
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
setState(() {
this.progress = progress / 100;
});
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onWebResourceError: (WebResourceError error) {
debugPrint(
'Page resource error: code: ${error.errorCode} description: ${error.description} errorType: ${error.errorType} isForMainFrame: ${error.isForMainFrame}');
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
debugPrint('blocking navigation to ${request.url}');
return NavigationDecision.prevent;
}
debugPrint('allowing navigation to ${request.url}');
return NavigationDecision.navigate;
},
onHttpError: (HttpResponseError error) {
debugPrint('Error occurred on page: ${error.response?.statusCode}');
},
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
onHttpAuthRequest: (HttpAuthRequest request) {},
),
)
..addJavaScriptChannel(
'getLocaltion',
onMessageReceived: (JavaScriptMessage message) {
final value = message.message;
final params = jsonDecode(value);
final locationStr = params['location'] as String;
final locations = locationStr.split(',');
longitude = double.parse(locations[0]);
latitude = double.parse(locations[1]);
final address = params['address'];
final url = getStaticMapURL(longitude!, latitude!);
final result = {
'longitude': longitude,
'latitude': latitude,
'url': url,
'addr': address,
'name': '',
};
description = jsonEncode(result);
Logger.print('$result');
_confirm();
},
)
..loadRequest(Uri.parse(previewLocationUrl));
print('previewLocationUrl: $previewLocationUrl');
if (!Platform.isMacOS) {
controller.setBackgroundColor(const Color(0x80000000));
}
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController).setMediaPlaybackRequiresUserGesture(false);
}
setState(() {
_controller = controller;
});
});
}
Future<Position> _determinePosition() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('Location permissions are permanently denied, we cannot request permissions.');
}
return await Geolocator.getCurrentPosition();
}
@override
void dispose() {
super.dispose();
}
void _confirm() async {
if (null == latitude || null == longitude) {
await showDialog(
context: context,
builder: (_) => AlertDialog(
title: StrRes.plsSelectLocation.toText..style = Styles.ts_0C1C33_17sp_semibold,
actions: [
GestureDetector(
onTap: () => Navigator.pop(context),
behavior: HitTestBehavior.translucent,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
child: StrRes.determine.toText..style = Styles.ts_0089FF_17sp_semibold,
),
),
],
),
);
return;
}
Navigator.pop(context, {
'latitude': latitude,
'longitude': longitude,
'description': description,
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: TitleBar.back(
onTap: () async {
Get.back();
},
title: StrRes.location,
),
body: SafeArea(
child: Stack(
children: [
_controller == null
? const Align(
child: CupertinoActivityIndicator(),
)
: WebViewWidget(controller: _controller!),
progress < 1.0
? LinearProgressIndicator(
value: progress,
color: Colors.blue,
)
: const SizedBox(),
],
),
),
);
}
}
@@ -34,7 +34,8 @@ class MapView extends StatelessWidget {
), ),
children: [ children: [
TileLayer( TileLayer(
urlTemplate: 'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', urlTemplate:
'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
userAgentPackageName: '', userAgentPackageName: '',
), ),
MarkerLayer( MarkerLayer(
@@ -88,19 +89,27 @@ class MapView extends StatelessWidget {
} }
_openMapSheet() async { _openMapSheet() async {
final availableMaps = await ml.MapLauncher.installedMaps; try {
Get.bottomSheet( final availableMaps = await ml.MapLauncher.installedMaps;
BottomSheetView( if (availableMaps.isEmpty) {
items: availableMaps IMViews.showToast(StrRes.locationMessage);
.map((e) => SheetItem( return;
label: _mapLabel(e), }
onTap: () async { Get.bottomSheet(
_launcherMap(e); BottomSheetView(
}, items: availableMaps
)) .map((e) => SheetItem(
.toList(), label: _mapLabel(e),
), onTap: () async {
); _launcherMap(e);
},
))
.toList(),
),
);
} catch (_) {
IMViews.showToast(StrRes.locationMessage);
}
} }
String _mapLabel(ml.AvailableMap map) { String _mapLabel(ml.AvailableMap map) {
@@ -119,11 +128,15 @@ class MapView extends StatelessWidget {
} }
_launcherMap(ml.AvailableMap map) async { _launcherMap(ml.AvailableMap map) async {
await ml.MapLauncher.showMarker( try {
mapType: map.mapType, await ml.MapLauncher.showMarker(
coords: ml.Coords(latitude, longitude), mapType: map.mapType,
title: address1, coords: ml.Coords(latitude, longitude),
description: address2, title: address1,
); description: address2,
);
} catch (_) {
IMViews.showToast(StrRes.locationMessage);
}
} }
} }
@@ -14,11 +14,9 @@ class RichTextInputBox extends StatefulWidget {
this.showCameraIcon = true, this.showCameraIcon = true,
this.showCardIcon = true, this.showCardIcon = true,
this.showFileIcon = true, this.showFileIcon = true,
this.showLocationIcon = true,
this.onTapAlbum, this.onTapAlbum,
this.onTapCard, this.onTapCard,
this.onTapFile, this.onTapFile,
this.onTapLocation,
this.onSend, this.onSend,
}) : super(key: key); }) : super(key: key);
final TextEditingController? controller; final TextEditingController? controller;
@@ -29,12 +27,10 @@ class RichTextInputBox extends StatefulWidget {
final bool showCameraIcon; final bool showCameraIcon;
final bool showFileIcon; final bool showFileIcon;
final bool showCardIcon; final bool showCardIcon;
final bool showLocationIcon;
final Function()? onTapAlbum; final Function()? onTapAlbum;
final Function()? onTapCamera; final Function()? onTapCamera;
final Function()? onTapFile; final Function()? onTapFile;
final Function()? onTapCard; final Function()? onTapCard;
final Function()? onTapLocation;
final Function()? onSend; final Function()? onSend;
@override @override
@@ -113,19 +109,12 @@ class _RichTextInputBoxState extends State<RichTextInputBox> {
..height = 22.h ..height = 22.h
..opacity = _opacity ..opacity = _opacity
..onTap = widget.onTapCard, ..onTap = widget.onTapCard,
if (widget.showLocationIcon)
ImageRes.toolboxLocation1.toImage
..width = 16.w
..height = 22.h
..opacity = _opacity
..onTap = widget.onTapLocation,
], ],
), ),
if (widget.showAlbumIcon || if (widget.showAlbumIcon ||
widget.showCameraIcon || widget.showCameraIcon ||
widget.showCardIcon || widget.showCardIcon ||
widget.showFileIcon || widget.showFileIcon)
widget.showLocationIcon)
15.verticalSpace, 15.verticalSpace,
Row( Row(
children: [ children: [
@@ -72,12 +72,6 @@ class IMViews {
alignment: MainAxisAlignment.start, alignment: MainAxisAlignment.start,
onTap: () => onTapSheetItem.call(0), onTap: () => onTapSheetItem.call(0),
), ),
SheetItem(
label: StrRes.callVideo,
icon: ImageRes.callVideo,
alignment: MainAxisAlignment.start,
onTap: () => onTapSheetItem.call(1),
),
], ],
), ),
); );
@@ -96,11 +90,6 @@ class IMViews {
icon: ImageRes.callVoice, icon: ImageRes.callVoice,
onTap: () => onTapSheetItem.call(0), onTap: () => onTapSheetItem.call(0),
), ),
SheetItem(
label: StrRes.callVideo,
icon: ImageRes.callVideo,
onTap: () => onTapSheetItem.call(1),
),
], ],
), ),
); );
@@ -115,7 +104,8 @@ class IMViews {
List<SheetItem> items = const [], List<SheetItem> items = const [],
int quality = 80}) { int quality = 80}) {
bool allowSendImageTypeHelper(String? mimeType) { bool allowSendImageTypeHelper(String? mimeType) {
final result = mimeType?.contains('png') == true || mimeType?.contains('jpeg') == true; final result = mimeType?.contains('png') == true ||
mimeType?.contains('jpeg') == true;
return result; return result;
} }
@@ -134,23 +124,25 @@ class IMViews {
SheetItem( SheetItem(
label: StrRes.toolboxAlbum, label: StrRes.toolboxAlbum,
onTap: () async { onTap: () async {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!, final List<AssetEntity>? assets =
pickerConfig: AssetPickerConfig( await AssetPicker.pickAssets(Get.context!,
requestType: RequestType.image, pickerConfig: AssetPickerConfig(
maxAssets: 1, requestType: RequestType.image,
selectPredicate: (_, entity, isSelected) async { maxAssets: 1,
if (await allowSendImageType(entity)) { selectPredicate: (_, entity, isSelected) async {
return true; if (await allowSendImageType(entity)) {
} return true;
}
IMViews.showToast(StrRes.supportsTypeHint); IMViews.showToast(StrRes.supportsTypeHint);
return false; return false;
})); }));
final file = await assets?.firstOrNull?.file; final file = await assets?.firstOrNull?.file;
if (file?.path != null) { if (file?.path != null) {
final map = await uCropPic(file!.path, crop: crop, toUrl: toUrl, quality: quality); final map = await uCropPic(file!.path,
crop: crop, toUrl: toUrl, quality: quality);
onData?.call(map['path'], map['url']); onData?.call(map['path'], map['url']);
} }
}, },
@@ -176,7 +168,8 @@ class IMViews {
final file = await entity?.file; final file = await entity?.file;
if (file?.path != null) { if (file?.path != null) {
final map = await uCropPic(file!.path, crop: crop, toUrl: toUrl, quality: quality); final map = await uCropPic(file!.path,
crop: crop, toUrl: toUrl, quality: quality);
onData?.call(map['path'], map['url']); onData?.call(map['path'], map['url']);
} }
}, },
@@ -206,7 +199,9 @@ class IMViews {
if (null != cropFile) { if (null != cropFile) {
Logger.print('-----------crop path: ${cropFile.path}'); Logger.print('-----------crop path: ${cropFile.path}');
result = await LoadingView.singleton.wrap(asyncFunction: () async { result = await LoadingView.singleton.wrap(asyncFunction: () async {
final image = await IMUtils.compressImageAndGetFile(File(cropFile!.path), quality: quality); final image = await IMUtils.compressImageAndGetFile(
File(cropFile!.path),
quality: quality);
return OpenIM.iMManager.uploadFile( return OpenIM.iMManager.uploadFile(
id: putID, id: putID,
@@ -217,7 +212,8 @@ class IMViews {
} else { } else {
Logger.print('-----------source path: $path'); Logger.print('-----------source path: $path');
result = await LoadingView.singleton.wrap(asyncFunction: () async { result = await LoadingView.singleton.wrap(asyncFunction: () async {
final image = await IMUtils.compressImageAndGetFile(File(path), quality: quality); final image = await IMUtils.compressImageAndGetFile(File(path),
quality: quality);
return OpenIM.iMManager.uploadFile( return OpenIM.iMManager.uploadFile(
id: putID, id: putID,
-1
View File
@@ -79,7 +79,6 @@ dependencies:
flutter_map: ^6.0.1 flutter_map: ^6.0.1
ffmpeg_kit_flutter_full_gpl: 6.0.3 ffmpeg_kit_flutter_full_gpl: 6.0.3
pull_to_refresh_new: ^2.0.5 pull_to_refresh_new: ^2.0.5
geolocator: ^12.0.0
fixnum: ^1.1.0 fixnum: ^1.1.0
protobuf: ^3.0.0 protobuf: ^3.0.0
intl: ^0.19.0 intl: ^0.19.0
@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -88,7 +87,8 @@ mixin OpenIMLive {
}); });
} }
Stream<CallEvent> get _stream => signalingSubject.stream /*.where((event) => LiveClient.dispatchSignaling(event))*/; Stream<CallEvent> get _stream => signalingSubject
.stream /*.where((event) => LiveClient.dispatchSignaling(event))*/;
_signalingListener() => _stream.listen( _signalingListener() => _stream.listen(
(event) async { (event) async {
@@ -97,15 +97,13 @@ mixin OpenIMLive {
_playSound(vibrate: true); _playSound(vibrate: true);
final mediaType = event.data.invitation!.mediaType; final mediaType = event.data.invitation!.mediaType;
final sessionType = event.data.invitation!.sessionType; final sessionType = event.data.invitation!.sessionType;
final callType = mediaType == 'audio' ? CallType.audio : CallType.video; final callType =
final callObj = sessionType == ConversationType.single ? CallObj.single : CallObj.group; mediaType == 'audio' ? CallType.audio : CallType.video;
final callObj = sessionType == ConversationType.single
? CallObj.single
: CallObj.group;
if (Platform.isAndroid && _isRunningBackground) { // 不再申请/依赖悬浮窗:后台来电也走应用内通话页,切回前台可继续。
_beCalledEvent = event;
if (await Permissions.checkSystemAlertWindow()) {
return;
}
}
_beCalledEvent = null; _beCalledEvent = null;
OpenIMLiveClient().start( OpenIMLiveClient().start(
Get.overlayContext!, Get.overlayContext!,
@@ -151,7 +149,8 @@ mixin OpenIMLive {
_stopSound(); _stopSound();
} else if (event.state == CallState.beAccepted) { } else if (event.state == CallState.beAccepted) {
_stopSound(); _stopSound();
} else if (event.state == CallState.otherReject || event.state == CallState.otherAccepted) { } else if (event.state == CallState.otherReject ||
event.state == CallState.otherAccepted) {
_stopSound(); _stopSound();
} else if (event.state == CallState.timeout) { } else if (event.state == CallState.timeout) {
insertSignalingMessageSubject.add(event); insertSignalingMessageSubject.add(event);
@@ -257,15 +256,19 @@ mixin OpenIMLive {
onRoomDisconnected(SignalingInfo signalingInfo) {} onRoomDisconnected(SignalingInfo signalingInfo) {}
Future<SignalingCertificate> onDialSingle(SignalingInfo signaling) async { Future<SignalingCertificate> onDialSingle(SignalingInfo signaling) async {
final data = {'customType': CustomMessageType.callingInvite, 'data': signaling.invitation!.toJson()}; final data = {
final message = await OpenIM.iMManager.messageManager 'customType': CustomMessageType.callingInvite,
.createCustomMessage(data: jsonEncode(data), extension: '', description: ''); 'data': signaling.invitation!.toJson()
};
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
data: jsonEncode(data), extension: '', description: '');
OpenIM.iMManager.messageManager.sendMessage( OpenIM.iMManager.messageManager.sendMessage(
message: message, message: message,
offlinePushInfo: OfflinePushInfo(), offlinePushInfo: OfflinePushInfo(),
userID: signaling.invitation!.inviteeUserIDList!.first, userID: signaling.invitation!.inviteeUserIDList!.first,
isOnlineOnly: true); isOnlineOnly: true);
final certificate = await getRtcCertificate(signaling.invitation!.roomID!, OpenIM.iMManager.userID); final certificate = await getRtcCertificate(
signaling.invitation!.roomID!, OpenIM.iMManager.userID);
return certificate; return certificate;
} }
@@ -283,15 +286,19 @@ mixin OpenIMLive {
_beCalledEvent = null; // ios bug _beCalledEvent = null; // ios bug
_autoPickup = false; _autoPickup = false;
_stopSound(); _stopSound();
final data = {'customType': CustomMessageType.callingAccept, 'data': signaling.invitation!.toJson()}; final data = {
final message = await OpenIM.iMManager.messageManager 'customType': CustomMessageType.callingAccept,
.createCustomMessage(data: jsonEncode(data), extension: '', description: ''); 'data': signaling.invitation!.toJson()
};
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
data: jsonEncode(data), extension: '', description: '');
OpenIM.iMManager.messageManager.sendMessage( OpenIM.iMManager.messageManager.sendMessage(
message: message, message: message,
offlinePushInfo: OfflinePushInfo(), offlinePushInfo: OfflinePushInfo(),
userID: signaling.invitation!.inviterUserID, userID: signaling.invitation!.inviterUserID,
isOnlineOnly: true); isOnlineOnly: true);
final certificate = await getRtcCertificate(signaling.invitation!.roomID!, OpenIM.iMManager.userID); final certificate = await getRtcCertificate(
signaling.invitation!.roomID!, OpenIM.iMManager.userID);
return certificate; return certificate;
} }
@@ -300,35 +307,52 @@ mixin OpenIMLive {
_stopSound(); _stopSound();
insertSignalingMessageSubject.add(CallEvent(CallState.reject, signaling)); insertSignalingMessageSubject.add(CallEvent(CallState.reject, signaling));
final data = {'customType': CustomMessageType.callingReject, 'data': signaling.invitation!.toJson()}; final data = {
final message = await OpenIM.iMManager.messageManager 'customType': CustomMessageType.callingReject,
.createCustomMessage(data: jsonEncode(data), extension: '', description: ''); 'data': signaling.invitation!.toJson()
final recvUserID = signaling.invitation!.inviterUserID == OpenIM.iMManager.userID };
? signaling.invitation!.inviteeUserIDList!.first final message = await OpenIM.iMManager.messageManager.createCustomMessage(
: signaling.invitation!.inviterUserID; data: jsonEncode(data), extension: '', description: '');
return OpenIM.iMManager.messageManager final recvUserID =
.sendMessage(message: message, offlinePushInfo: OfflinePushInfo(), userID: recvUserID, isOnlineOnly: true); signaling.invitation!.inviterUserID == OpenIM.iMManager.userID
? signaling.invitation!.inviteeUserIDList!.first
: signaling.invitation!.inviterUserID;
return OpenIM.iMManager.messageManager.sendMessage(
message: message,
offlinePushInfo: OfflinePushInfo(),
userID: recvUserID,
isOnlineOnly: true);
} }
onTapCancel(SignalingInfo signaling) async { onTapCancel(SignalingInfo signaling) async {
_stopSound(); _stopSound();
insertSignalingMessageSubject.add(CallEvent(CallState.cancel, signaling)); insertSignalingMessageSubject.add(CallEvent(CallState.cancel, signaling));
final data = {'customType': CustomMessageType.callingCancel, 'data': signaling.invitation!.toJson()}; final data = {
final message = await OpenIM.iMManager.messageManager 'customType': CustomMessageType.callingCancel,
.createCustomMessage(data: jsonEncode(data), extension: '', description: ''); 'data': signaling.invitation!.toJson()
final recvUserID = signaling.invitation!.inviterUserID == OpenIM.iMManager.userID };
? signaling.invitation!.inviteeUserIDList!.first final message = await OpenIM.iMManager.messageManager.createCustomMessage(
: signaling.invitation!.inviterUserID; data: jsonEncode(data), extension: '', description: '');
OpenIM.iMManager.messageManager final recvUserID =
.sendMessage(message: message, offlinePushInfo: OfflinePushInfo(), userID: recvUserID, isOnlineOnly: true); signaling.invitation!.inviterUserID == OpenIM.iMManager.userID
? signaling.invitation!.inviteeUserIDList!.first
: signaling.invitation!.inviterUserID;
OpenIM.iMManager.messageManager.sendMessage(
message: message,
offlinePushInfo: OfflinePushInfo(),
userID: recvUserID,
isOnlineOnly: true);
return true; return true;
} }
onTimeoutCancelled(SignalingInfo signaling) async { onTimeoutCancelled(SignalingInfo signaling) async {
final data = {'customType': CustomMessageType.callingCancel, 'data': signaling.invitation!.toJson()}; final data = {
final message = await OpenIM.iMManager.messageManager 'customType': CustomMessageType.callingCancel,
.createCustomMessage(data: jsonEncode(data), extension: '', description: ''); 'data': signaling.invitation!.toJson()
};
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
data: jsonEncode(data), extension: '', description: '');
OpenIM.iMManager.messageManager.sendMessage( OpenIM.iMManager.messageManager.sendMessage(
message: message, message: message,
@@ -349,14 +373,21 @@ mixin OpenIMLive {
onTapHangup(SignalingInfo signaling, int duration, bool isPositive) async { onTapHangup(SignalingInfo signaling, int duration, bool isPositive) async {
if (isPositive) { if (isPositive) {
final data = {'customType': CustomMessageType.callingHungup, 'data': signaling.invitation!.toJson()}; final data = {
final message = await OpenIM.iMManager.messageManager 'customType': CustomMessageType.callingHungup,
.createCustomMessage(data: jsonEncode(data), extension: '', description: ''); 'data': signaling.invitation!.toJson()
final recvUserID = signaling.invitation!.inviterUserID == OpenIM.iMManager.userID };
? signaling.invitation!.inviteeUserIDList!.first final message = await OpenIM.iMManager.messageManager.createCustomMessage(
: signaling.invitation!.inviterUserID; data: jsonEncode(data), extension: '', description: '');
OpenIM.iMManager.messageManager final recvUserID =
.sendMessage(message: message, offlinePushInfo: OfflinePushInfo(), userID: recvUserID, isOnlineOnly: true); signaling.invitation!.inviterUserID == OpenIM.iMManager.userID
? signaling.invitation!.inviteeUserIDList!.first
: signaling.invitation!.inviterUserID;
OpenIM.iMManager.messageManager.sendMessage(
message: message,
offlinePushInfo: OfflinePushInfo(),
userID: recvUserID,
isOnlineOnly: true);
} }
_stopSound(); _stopSound();
@@ -389,7 +420,8 @@ mixin OpenIMLive {
return list.firstOrNull; return list.firstOrNull;
} }
Future<List<GroupMembersInfo>> onSyncGroupMemberInfo(groupID, userIDList) async { Future<List<GroupMembersInfo>> onSyncGroupMemberInfo(
groupID, userIDList) async {
var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo( var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo(
groupID: groupID, groupID: groupID,
userIDList: userIDList, userIDList: userIDList,
@@ -412,7 +444,8 @@ mixin OpenIMLive {
void _startIncomingVibrate() { void _startIncomingVibrate() {
_stopIncomingVibrate(); _stopIncomingVibrate();
_pulseVibrate(); _pulseVibrate();
_incomingVibrateTimer = Timer.periodic(Styles.callVibrate, (_) => _pulseVibrate()); _incomingVibrateTimer =
Timer.periodic(Styles.callVibrate, (_) => _pulseVibrate());
} }
void _stopIncomingVibrate() { void _stopIncomingVibrate() {
@@ -467,7 +500,8 @@ mixin OpenIMLive {
receiverID = inviteeUserID; receiverID = inviteeUserID;
} }
var msg = await OpenIM.iMManager.messageManager.insertSingleMessageToLocalStorage( var msg = await OpenIM.iMManager.messageManager
.insertSingleMessageToLocalStorage(
receiverID: inviteeUserID, receiverID: inviteeUserID,
senderID: inviterUserID, senderID: inviterUserID,
message: message message: message
@@ -495,7 +529,9 @@ class SignalingMessageEvent {
bool get isSingleChat => sessionType == ConversationType.single; bool get isSingleChat => sessionType == ConversationType.single;
bool get isGroupChat => sessionType == ConversationType.group || sessionType == ConversationType.superGroup; bool get isGroupChat =>
sessionType == ConversationType.group ||
sessionType == ConversationType.superGroup;
} }
extension MessageMangerExt on MessageManager { extension MessageMangerExt on MessageManager {
-48
View File
@@ -930,54 +930,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
geolocator:
dependency: transitive
description:
name: geolocator
sha256: "149876cc5207a0f5daf4fdd3bfcf0a0f27258b3fe95108fa084f527ad0568f1b"
url: "https://pub.dev"
source: hosted
version: "12.0.0"
geolocator_android:
dependency: transitive
description:
name: geolocator_android
sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47"
url: "https://pub.dev"
source: hosted
version: "4.6.1"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
sha256: c4ecead17985ede9634f21500072edfcb3dba0ef7b97f8d7bc556d2d722b3ba3
url: "https://pub.dev"
source: hosted
version: "2.3.9"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
geolocator_web:
dependency: transitive
description:
name: geolocator_web
sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
geolocator_windows:
dependency: transitive
description:
name: geolocator_windows
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
gesture_password_widget: gesture_password_widget:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -0,0 +1,105 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:openim_common/openim_common.dart';
import 'package:openim_common/src/res/lang/en_US.dart';
import 'package:openim_common/src/res/lang/zh_CN.dart';
void main() {
test('通话入口文案统一为语音通话,不改相册/拍摄视频能力文案', () {
expect(zh_CN['toolboxCall'], '语音通话');
expect(zh_CN['audioAndVideoCall'], '语音通话');
expect(zh_CN['callVoice'], '语音通话');
expect(zh_CN['invitedVoiceCallHint'], '邀请你语音通话');
expect(zh_CN['toolboxCamera'], '拍摄');
expect(zh_CN['video'], '视频');
expect(en_US['toolboxCall'], 'Voice Call');
expect(en_US['audioAndVideoCall'], 'Voice Call');
});
test('资料页通话按钮使用麦克风语义图标', () {
expect(ImageRes.callVoice, contains('ic_call_voice'));
});
test('历史位置消息解析失败时回退到标题和坐标,不抛异常', () {
final valid = LocationBubbleData.parse(
description:
'{"name":"公司门口","addr":"示范路 1 号","url":"https://example.com/map.png"}',
latitude: 31.23,
longitude: 121.47,
);
expect(valid.title, '公司门口');
expect(valid.address, '示范路 1 号');
expect(valid.thumbnailUrl, 'https://example.com/map.png');
final broken = LocationBubbleData.parse(
description: 'not-json',
latitude: 31.23001,
longitude: 121.47002,
);
expect(broken.title, '位置消息');
expect(broken.address, 'not-json');
expect(broken.thumbnailUrl, isNull);
final emptyJson = LocationBubbleData.parse(
description: '{}',
latitude: 1.5,
longitude: 2.5,
);
expect(emptyJson.title, '位置消息');
expect(emptyJson.address, '1.50000, 2.50000');
expect(emptyJson.thumbnailUrl, isNull);
});
test('Android 清单移除悬浮窗、定位和旧浮窗服务', () {
final xml =
File('android/app/src/main/AndroidManifest.xml').readAsStringSync();
expect(xml.contains('SYSTEM_ALERT_WINDOW'), isTrue);
expect(xml.contains('tools:node="remove"'), isTrue);
expect(
RegExp(r'<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"\s*/>')
.hasMatch(xml),
isFalse);
expect(
RegExp(r'<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"\s*/>')
.hasMatch(xml),
isFalse);
expect(
RegExp(r'<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"\s*/>')
.hasMatch(xml),
isFalse);
expect(
RegExp(r'<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"\s*/>')
.hasMatch(xml),
isFalse);
expect(xml.contains('flutter_openim_live_alert.services.CallService'),
isFalse);
expect(xml.contains('REQUEST_IGNORE_BATTERY_OPTIMIZATIONS'), isFalse);
});
test('IM 同步完成不再申请悬浮窗,来电不再检查 overlay', () {
final im =
File('lib/core/controller/im_controller.dart').readAsStringSync();
expect(im.contains('systemAlertWindow'), isFalse);
final live =
File('openim_live/lib/src/live_controller.dart').readAsStringSync();
expect(live.contains('checkSystemAlertWindow'), isFalse);
expect(live.contains('systemAlertWindow'), isFalse);
});
test('工具箱和选点发送链路已删除', () {
final toolbox = File('openim_common/lib/src/widgets/chat/chat_toolbox.dart')
.readAsStringSync();
expect(toolbox.contains('onTapLocation'), isFalse);
expect(toolbox.contains('toolboxLocation'), isFalse);
final chatLogic = File('lib/pages/chat/chat_logic.dart').readAsStringSync();
expect(chatLogic.contains('onTapLocation'), isFalse);
expect(chatLogic.contains('sendLocation'), isFalse);
expect(chatLogic.contains('createLocationMessage'), isFalse);
expect(chatLogic.contains('ChatWebViewMap'), isFalse);
expect(
File('openim_common/lib/src/widgets/chat/chat_webview_map.dart')
.existsSync(),
isFalse);
});
}
@@ -0,0 +1,37 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:openim_common/openim_common.dart';
void main() {
test('把 OpenIM 公开资料映射成我的信息可用字段', () {
final info = UserFullInfoMapper.fromImJson({
'userID': '10001',
'nickname': '张三',
'faceURL': 'http://example/a.png',
'ex': '',
'globalRecvMsgOpt': 0,
});
expect(info.userID, '10001');
expect(info.nickname, '张三');
expect(info.faceURL, 'http://example/a.png');
expect(info.globalRecvMsgOpt, 0);
});
test('不把 chat 业务字段当成仍可从账号服务拿到', () {
final info = UserFullInfoMapper.fromImJson({
'userID': '10001',
'nickname': '张三',
'phoneNumber': '13800000000',
'email': 'a@b.c',
'gender': 1,
});
expect(info.userID, '10001');
expect(info.nickname, '张三');
expect(info.phoneNumber, isNull);
expect(info.email, isNull);
expect(info.gender, isNull);
});
test('空列表保持为空', () {
expect(UserFullInfoMapper.fromImList(const []), isEmpty);
});
}