Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
613d38dd06 | ||
|
|
1774e596ba | ||
|
|
694891c393 | ||
|
|
092039bc1a | ||
|
|
7bda2f280a | ||
|
|
9001688c9e |
@@ -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-225:Flutter 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-261:Flutter 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.CHANGE_WIFI_MULTICAST_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.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 android:name="android.permission.CALL_PHONE" />
|
||||
@@ -26,14 +26,15 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
|
||||
<!-- B-273: 位置发送已移除,合并清单禁止再声明定位权限 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
|
||||
<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.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<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.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
|
||||
@@ -50,9 +51,6 @@
|
||||
android:usesPermissionFlags="neverForLocation" />
|
||||
<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_BIOMETRIC" />
|
||||
@@ -130,10 +128,6 @@
|
||||
android:exported="false"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<service
|
||||
android:name="io.openim.live.alert.flutter_openim_live_alert.services.CallService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:openim_common/openim_common.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:openim_live/openim_live.dart';
|
||||
|
||||
import '../../company/feature_flags.dart';
|
||||
@@ -20,7 +18,8 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
||||
late Rx<UserFullInfo> userInfo;
|
||||
late String atAllTag;
|
||||
final _rtcTokenApi = RtcTokenApi();
|
||||
final callGuard = CallSessionGuard(inviteTimeout: LiveKitCallConfig.inviteTimeout);
|
||||
final callGuard =
|
||||
CallSessionGuard(inviteTimeout: LiveKitCallConfig.inviteTimeout);
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
@@ -36,7 +35,8 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
||||
if (token == null || token.isEmpty) {
|
||||
return Future.error(StateError('登录状态已失效,请重新登录'));
|
||||
}
|
||||
return _rtcTokenApi.getToken(room: roomID, identity: userID, authToken: token);
|
||||
return _rtcTokenApi.getToken(
|
||||
room: roomID, identity: userID, authToken: token);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,11 +54,13 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
||||
if (!FeatureFlags.livekitCall) return;
|
||||
final invitation = info.invitation;
|
||||
final roomID = invitation?.roomID ?? '';
|
||||
if (invitation?.mediaType != 'audio' || invitation?.sessionType != ConversationType.single) {
|
||||
if (invitation?.mediaType != 'audio' ||
|
||||
invitation?.sessionType != ConversationType.single) {
|
||||
onTapReject(info);
|
||||
return;
|
||||
}
|
||||
if (!callGuard.acceptSignaling(customType: CustomMessageType.callingInvite, roomID: roomID)) {
|
||||
if (!callGuard.acceptSignaling(
|
||||
customType: CustomMessageType.callingInvite, roomID: roomID)) {
|
||||
return;
|
||||
}
|
||||
if (isBusy || callGuard.isBusy) {
|
||||
@@ -149,7 +151,8 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
||||
);
|
||||
|
||||
OpenIM.iMManager
|
||||
..setUploadLogsListener(OnUploadLogsListener(onUploadProgress: uploadLogsProgress))
|
||||
..setUploadLogsListener(
|
||||
OnUploadLogsListener(onUploadProgress: uploadLogsProgress))
|
||||
..userManager.setUserListener(OnUserListener(
|
||||
onSelfInfoUpdated: (u) {
|
||||
selfInfoUpdated(u);
|
||||
@@ -180,11 +183,13 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
||||
customType == CustomMessageType.callingCancel ||
|
||||
customType == CustomMessageType.callingHungup) {
|
||||
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;
|
||||
final roomID = signaling.invitation?.roomID ?? '';
|
||||
if (customType != CustomMessageType.callingInvite &&
|
||||
!callGuard.acceptSignaling(customType: customType as int, roomID: roomID)) {
|
||||
!callGuard.acceptSignaling(
|
||||
customType: customType as int, roomID: roomID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,9 +241,6 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
||||
},
|
||||
onSyncServerFinish: (reInstall) {
|
||||
imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false);
|
||||
if (Platform.isAndroid) {
|
||||
Permissions.request([Permission.systemAlertWindow]);
|
||||
}
|
||||
},
|
||||
onSyncServerStart: (reInstall) {
|
||||
imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false);
|
||||
|
||||
@@ -126,10 +126,13 @@ class ChatLogic extends SuperController {
|
||||
|
||||
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 =>
|
||||
groupMemberRoleLevel.value == GroupRoleLevel.admin || groupMemberRoleLevel.value == GroupRoleLevel.owner;
|
||||
groupMemberRoleLevel.value == GroupRoleLevel.admin ||
|
||||
groupMemberRoleLevel.value == GroupRoleLevel.owner;
|
||||
|
||||
final directionalUsers = <GroupMembersInfo>[].obs;
|
||||
|
||||
@@ -140,8 +143,10 @@ class ChatLogic extends SuperController {
|
||||
|
||||
var isCurSingleChat = message.isSingleChat &&
|
||||
isSingleChat &&
|
||||
(senderId == userID || senderId == OpenIM.iMManager.userID && receiverId == userID);
|
||||
var isCurGroupChat = message.isGroupChat && isGroupChat && groupID == groupId;
|
||||
(senderId == userID ||
|
||||
senderId == OpenIM.iMManager.userID && receiverId == userID);
|
||||
var isCurGroupChat =
|
||||
message.isGroupChat && isGroupChat && groupID == groupId;
|
||||
return isCurSingleChat || isCurGroupChat;
|
||||
}
|
||||
|
||||
@@ -152,11 +157,14 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
Future<List<Message>> searchMediaMessage() async {
|
||||
final messageList = await OpenIM.iMManager.messageManager.searchLocalMessages(
|
||||
conversationID: conversationInfo.conversationID,
|
||||
messageTypeList: [MessageType.picture, MessageType.video],
|
||||
count: 500);
|
||||
return messageList.searchResultItems?.first.messageList?.reversed.toList() ?? [];
|
||||
final messageList = await OpenIM.iMManager.messageManager
|
||||
.searchLocalMessages(
|
||||
conversationID: conversationInfo.conversationID,
|
||||
messageTypeList: [MessageType.picture, MessageType.video],
|
||||
count: 500);
|
||||
return messageList.searchResultItems?.first.messageList?.reversed
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -185,7 +193,8 @@ class ChatLogic extends SuperController {
|
||||
_setSdkSyncDataListener();
|
||||
|
||||
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) {
|
||||
conversationInfo = obj;
|
||||
@@ -196,7 +205,8 @@ class ChatLogic extends SuperController {
|
||||
if (isCurrentChat(message)) {
|
||||
if (message.contentType == MessageType.typing) {
|
||||
} else {
|
||||
if (!messageList.contains(message) && !scrollingCacheMessageList.contains(message)) {
|
||||
if (!messageList.contains(message) &&
|
||||
!scrollingCacheMessageList.contains(message)) {
|
||||
_isReceivedMessageWhenSyncing = true;
|
||||
if (isShowPopMenu.value || scrollController.offset != 0) {
|
||||
scrollingCacheMessageList.add(message);
|
||||
@@ -210,7 +220,8 @@ class ChatLogic extends SuperController {
|
||||
};
|
||||
|
||||
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?.contentType = MessageType.revokeMessageNotification;
|
||||
|
||||
@@ -273,12 +284,14 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
_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 (index > -1) {
|
||||
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) {
|
||||
ownerAndAdmin.add(info);
|
||||
} else {
|
||||
@@ -364,7 +377,8 @@ class ChatLogic extends SuperController {
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -396,7 +410,8 @@ class ChatLogic extends SuperController {
|
||||
Future sendPicture({required String path, bool sendNow = true}) async {
|
||||
final file = await IMUtils.compressImageAndGetFile(File(path));
|
||||
|
||||
var message = await OpenIM.iMManager.messageManager.createImageMessageFromFullPath(
|
||||
var message =
|
||||
await OpenIM.iMManager.messageManager.createImageMessageFromFullPath(
|
||||
imagePath: file!.path,
|
||||
);
|
||||
|
||||
@@ -409,7 +424,8 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
void sendVoice(int duration, String path) async {
|
||||
var message = await OpenIM.iMManager.messageManager.createSoundMessageFromFullPath(
|
||||
var message =
|
||||
await OpenIM.iMManager.messageManager.createSoundMessageFromFullPath(
|
||||
soundPath: path,
|
||||
duration: duration,
|
||||
);
|
||||
@@ -423,7 +439,8 @@ class ChatLogic extends SuperController {
|
||||
required String thumbnailPath,
|
||||
bool sendNow = true}) async {
|
||||
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,
|
||||
videoType: mimeType,
|
||||
duration: d.toInt(),
|
||||
@@ -439,24 +456,14 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
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,
|
||||
fileName: fileName,
|
||||
);
|
||||
_sendMessage(message);
|
||||
}
|
||||
|
||||
void sendLocation({
|
||||
required dynamic location,
|
||||
}) async {
|
||||
var message = await OpenIM.iMManager.messageManager.createLocationMessage(
|
||||
latitude: location['latitude'],
|
||||
longitude: location['longitude'],
|
||||
description: location['description'],
|
||||
);
|
||||
_sendMessage(message);
|
||||
}
|
||||
|
||||
sendForwardRemarkMsg(
|
||||
String content, {
|
||||
String? userId,
|
||||
@@ -481,8 +488,8 @@ class ChatLogic extends SuperController {
|
||||
|
||||
void sendTypingMsg({bool focus = false}) async {
|
||||
if (isSingleChat) {
|
||||
OpenIM.iMManager.conversationManager
|
||||
.changeInputStates(conversationID: conversationInfo.conversationID, focus: focus);
|
||||
OpenIM.iMManager.conversationManager.changeInputStates(
|
||||
conversationID: conversationInfo.conversationID, focus: focus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +551,8 @@ class ChatLogic extends SuperController {
|
||||
offlinePushInfo: Config.offlinePushInfo,
|
||||
)
|
||||
.then((value) => _sendSucceeded(message, value))
|
||||
.catchError((error, _) => _senFailed(message, groupId, userId, error, _))
|
||||
.catchError(
|
||||
(error, _) => _senFailed(message, groupId, userId, error, _))
|
||||
.whenComplete(() => _completed());
|
||||
}
|
||||
|
||||
@@ -557,8 +565,10 @@ class ChatLogic extends SuperController {
|
||||
));
|
||||
}
|
||||
|
||||
void _senFailed(Message message, String? groupId, String? userId, error, stack) async {
|
||||
Logger.print('message send failed userID: $userId groupId:$groupId, catch error :$error $stack');
|
||||
void _senFailed(
|
||||
Message message, String? groupId, String? userId, error, stack) async {
|
||||
Logger.print(
|
||||
'message send failed userID: $userId groupId:$groupId, catch error :$error $stack');
|
||||
message.status = MessageStatus.failed;
|
||||
sendStatusSub.addSafely(MsgStreamEv<bool>(
|
||||
id: message.clientMsgID!,
|
||||
@@ -574,7 +584,8 @@ class ChatLogic extends SuperController {
|
||||
customType = CustomMessageType.deletedByFriend;
|
||||
}
|
||||
if (null != customType) {
|
||||
final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage(type: customType))
|
||||
final hintMessage = (await OpenIM.iMManager.messageManager
|
||||
.createFailedHintMessage(type: customType))
|
||||
..status = 2
|
||||
..isRead = true;
|
||||
if (userId != null) {
|
||||
@@ -591,10 +602,15 @@ class ChatLogic extends SuperController {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ((code == SDKErrorCode.userIsNotInGroup || code == SDKErrorCode.groupDisbanded) && null == groupId) {
|
||||
if ((code == SDKErrorCode.userIsNotInGroup ||
|
||||
code == SDKErrorCode.groupDisbanded) &&
|
||||
null == groupId) {
|
||||
final status = groupInfo?.status;
|
||||
final hintMessage = (await OpenIM.iMManager.messageManager.createFailedHintMessage(
|
||||
type: status == 2 ? CustomMessageType.groupDisbanded : CustomMessageType.removedFromGroup))
|
||||
final hintMessage = (await OpenIM.iMManager.messageManager
|
||||
.createFailedHintMessage(
|
||||
type: status == 2
|
||||
? CustomMessageType.groupDisbanded
|
||||
: CustomMessageType.removedFromGroup))
|
||||
..status = 2
|
||||
..isRead = true;
|
||||
messageList.add(hintMessage);
|
||||
@@ -662,7 +678,9 @@ class ChatLogic extends SuperController {
|
||||
|
||||
void markMessageAsRead(Message message, bool visible) async {
|
||||
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);
|
||||
if (null != data && data['viewType'] == CustomMessageType.call) {
|
||||
Logger.print('markMessageAsRead: call message $data');
|
||||
@@ -675,11 +693,14 @@ class ChatLogic extends SuperController {
|
||||
_markMessageAsRead(Message message) async {
|
||||
if (!message.isRead! && message.sendID != OpenIM.iMManager.userID) {
|
||||
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
|
||||
.markConversationMessageAsRead(conversationID: conversationInfo.conversationID);
|
||||
.markConversationMessageAsRead(
|
||||
conversationID: conversationInfo.conversationID);
|
||||
} 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 {
|
||||
message.isRead = true;
|
||||
message.hasReadTime = _timestamp;
|
||||
@@ -690,23 +711,23 @@ class ChatLogic extends SuperController {
|
||||
|
||||
_clearUnreadCount() {
|
||||
if (conversationInfo.unreadCount > 0) {
|
||||
OpenIM.iMManager.conversationManager
|
||||
.markConversationMessageAsRead(conversationID: conversationInfo.conversationID);
|
||||
OpenIM.iMManager.conversationManager.markConversationMessageAsRead(
|
||||
conversationID: conversationInfo.conversationID);
|
||||
}
|
||||
}
|
||||
|
||||
void _getInputState() async {
|
||||
if (conversationInfo.isSingleChat) {
|
||||
final result =
|
||||
await OpenIM.iMManager.conversationManager.getInputStates(conversationInfo.conversationID, userID!);
|
||||
final result = await OpenIM.iMManager.conversationManager
|
||||
.getInputStates(conversationInfo.conversationID, userID!);
|
||||
typing.value = result?.isNotEmpty == true;
|
||||
}
|
||||
}
|
||||
|
||||
void _changeInputStatus(bool focus) async {
|
||||
if (conversationInfo.isSingleChat) {
|
||||
await OpenIM.iMManager.conversationManager
|
||||
.changeInputStates(conversationID: conversationInfo.conversationID, focus: focus);
|
||||
await OpenIM.iMManager.conversationManager.changeInputStates(
|
||||
conversationID: conversationInfo.conversationID, focus: focus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,18 +735,6 @@ class ChatLogic extends SuperController {
|
||||
forceCloseToolbox.addSafely(true);
|
||||
}
|
||||
|
||||
void onTapLocation() async {
|
||||
var location = await Get.to(
|
||||
const ChatWebViewMap(host: Config.locationHost, webKey: Config.webKey, webServerKey: Config.webServerKey),
|
||||
transition: Transition.cupertino,
|
||||
popGesture: true,
|
||||
);
|
||||
if (null != location) {
|
||||
Logger.print(location);
|
||||
sendLocation(location: location);
|
||||
}
|
||||
}
|
||||
|
||||
void onTapAlbum() async {
|
||||
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
@@ -743,7 +752,8 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
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 true;
|
||||
@@ -837,10 +847,13 @@ class ChatLogic extends SuperController {
|
||||
|
||||
Future _handleAssets(AssetEntity? asset, {bool sendNow = true}) async {
|
||||
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 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');
|
||||
switch (asset.type) {
|
||||
case AssetType.image:
|
||||
@@ -1041,14 +1054,16 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
void copy(Message message) {
|
||||
final content = copyTextMap[message.clientMsgID] ?? message.textElem?.content;
|
||||
final content =
|
||||
copyTextMap[message.clientMsgID] ?? message.textElem?.content;
|
||||
|
||||
if (null != content) {
|
||||
IMUtils.copy(text: content.replaceAll('\u200B', ''));
|
||||
}
|
||||
}
|
||||
|
||||
Message indexOfMessage(int index, {bool calculate = true}) => IMUtils.calChatTimeInterval(
|
||||
Message indexOfMessage(int index, {bool calculate = true}) =>
|
||||
IMUtils.calChatTimeInterval(
|
||||
messageList,
|
||||
calculate: calculate,
|
||||
).reversed.elementAt(index);
|
||||
@@ -1111,7 +1126,11 @@ class ChatLogic extends SuperController {
|
||||
|
||||
void onDeleteEmoji() {
|
||||
final input = inputCtrl.text;
|
||||
final regexEmoji = emojiFaces.keys.toList().join('|').replaceAll('[', '\\[').replaceAll(']', '\\]');
|
||||
final regexEmoji = emojiFaces.keys
|
||||
.toList()
|
||||
.join('|')
|
||||
.replaceAll('[', '\\[')
|
||||
.replaceAll(']', '\\]');
|
||||
final list = [regexEmoji];
|
||||
final pattern = '(${list.toList().join('|')})';
|
||||
final emojiReg = RegExp(regexEmoji);
|
||||
@@ -1186,7 +1205,8 @@ class ChatLogic extends SuperController {
|
||||
void sendFavoritePic(int index, String url) async {
|
||||
var emoji = cacheLogic.favoriteList.elementAt(index);
|
||||
var message = await OpenIM.iMManager.messageManager.createFaceMessage(
|
||||
data: json.encode({'url': emoji.url, 'width': emoji.width, 'height': emoji.height}),
|
||||
data: json.encode(
|
||||
{'url': emoji.url, 'width': emoji.width, 'height': emoji.height}),
|
||||
);
|
||||
_sendMessage(message);
|
||||
}
|
||||
@@ -1227,7 +1247,8 @@ class ChatLogic extends SuperController {
|
||||
var diff = (end - _timestamp) ~/ 1000;
|
||||
|
||||
if (diff > 0) {
|
||||
privateMessageList.addIf(() => !privateMessageList.contains(message), message);
|
||||
privateMessageList.addIf(
|
||||
() => !privateMessageList.contains(message), message);
|
||||
}
|
||||
return diff < 0 ? 0 : diff;
|
||||
}
|
||||
@@ -1255,7 +1276,8 @@ class ChatLogic extends SuperController {
|
||||
userIDList: [OpenIM.iMManager.userID],
|
||||
);
|
||||
groupMembersInfo = list.firstOrNull;
|
||||
groupMemberRoleLevel.value = groupMembersInfo?.roleLevel ?? GroupRoleLevel.member;
|
||||
groupMemberRoleLevel.value =
|
||||
groupMembersInfo?.roleLevel ?? GroupRoleLevel.member;
|
||||
muteEndTime.value = groupMembersInfo?.muteEndTime ?? 0;
|
||||
if (null != groupMembersInfo) {
|
||||
memberUpdateInfoMap[OpenIM.iMManager.userID] = groupMembersInfo!;
|
||||
@@ -1266,7 +1288,8 @@ class ChatLogic extends SuperController {
|
||||
|
||||
Future _queryOwnerAndAdmin() async {
|
||||
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;
|
||||
}
|
||||
@@ -1303,7 +1326,9 @@ class ChatLogic extends SuperController {
|
||||
|
||||
bool get havePermissionMute =>
|
||||
isGroupChat &&
|
||||
(groupInfo?.ownerUserID == OpenIM.iMManager.userID /*||
|
||||
(groupInfo?.ownerUserID ==
|
||||
OpenIM.iMManager
|
||||
.userID /*||
|
||||
groupMembersInfo?.roleLevel == 2*/
|
||||
);
|
||||
|
||||
@@ -1315,8 +1340,10 @@ class ChatLogic extends SuperController {
|
||||
|
||||
void _queryUserOnlineStatus() {
|
||||
if (isSingleChat) {
|
||||
OpenIM.iMManager.userManager.subscribeUsersStatus([userID!]).then((value) {
|
||||
final status = value.firstWhereOrNull((element) => element.userID == userID);
|
||||
OpenIM.iMManager.userManager
|
||||
.subscribeUsersStatus([userID!]).then((value) {
|
||||
final status =
|
||||
value.firstWhereOrNull((element) => element.userID == userID);
|
||||
_configUserStatusChanged(status);
|
||||
});
|
||||
userStatusChangedSub = imLogic.userStatusChangedSubject.listen((value) {
|
||||
@@ -1336,8 +1363,9 @@ class ChatLogic extends SuperController {
|
||||
void _configUserStatusChanged(UserStatusInfo? status) {
|
||||
if (status != null) {
|
||||
onlineStatus.value = status.status == 1;
|
||||
onlineStatusDesc.value =
|
||||
status.status == 0 ? StrRes.offline : _onlineStatusDes(status.platformIDs!) + StrRes.online;
|
||||
onlineStatusDesc.value = status.status == 0
|
||||
? StrRes.offline
|
||||
: _onlineStatusDes(status.platformIDs!) + StrRes.online;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1496,10 +1524,12 @@ class ChatLogic extends SuperController {
|
||||
if (message.sendID == OpenIM.iMManager.userID) {
|
||||
canRevoke = true;
|
||||
} else {
|
||||
var list = await LoadingView.singleton
|
||||
.wrap(asyncFunction: () => OpenIM.iMManager.groupManager.getGroupOwnerAndAdmin(groupID: groupID!));
|
||||
var list = await LoadingView.singleton.wrap(
|
||||
asyncFunction: () => OpenIM.iMManager.groupManager
|
||||
.getGroupOwnerAndAdmin(groupID: groupID!));
|
||||
var sender = list.firstWhereOrNull((e) => e.userID == message.sendID);
|
||||
var revoker = list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID);
|
||||
var revoker =
|
||||
list.firstWhereOrNull((e) => e.userID == OpenIM.iMManager.userID);
|
||||
|
||||
if (revoker != null && sender == null) {
|
||||
canRevoke = true;
|
||||
@@ -1531,7 +1561,8 @@ class ChatLogic extends SuperController {
|
||||
),
|
||||
);
|
||||
message.contentType = MessageType.revokeMessageNotification;
|
||||
message.notificationElem = NotificationElem(detail: jsonEncode(_buildRevokeInfo(message)));
|
||||
message.notificationElem =
|
||||
NotificationElem(detail: jsonEncode(_buildRevokeInfo(message)));
|
||||
messageList.refresh();
|
||||
} catch (e) {
|
||||
IMViews.showToast(e.toString());
|
||||
@@ -1595,12 +1626,15 @@ class ChatLogic extends SuperController {
|
||||
if (isGroupChat) {
|
||||
if (groupMemberRoleLevel.value == GroupRoleLevel.owner ||
|
||||
(groupMemberRoleLevel.value == GroupRoleLevel.admin &&
|
||||
ownerAndAdmin.firstWhereOrNull((element) => element.userID == message.sendID) == null)) {
|
||||
ownerAndAdmin.firstWhereOrNull(
|
||||
(element) => element.userID == message.sendID) ==
|
||||
null)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (message.sendID == OpenIM.iMManager.userID) {
|
||||
if (DateTime.now().millisecondsSinceEpoch - (message.sendTime ??= 0) < (1000 * 60 * 5)) {
|
||||
if (DateTime.now().millisecondsSinceEpoch - (message.sendTime ??= 0) <
|
||||
(1000 * 60 * 5)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1611,7 +1645,8 @@ class ChatLogic extends SuperController {
|
||||
if (message.status != MessageStatus.succeeded) {
|
||||
return false;
|
||||
}
|
||||
return message.contentType == MessageType.picture || message.contentType == MessageType.customFace;
|
||||
return message.contentType == MessageType.picture ||
|
||||
message.contentType == MessageType.customFace;
|
||||
}
|
||||
|
||||
WillPopCallback? willPop() {
|
||||
@@ -1658,12 +1693,14 @@ class ChatLogic extends SuperController {
|
||||
var data = message.customElem!.data;
|
||||
var map = json.decode(data!);
|
||||
var customType = map['customType'];
|
||||
return customType == CustomMessageType.deletedByFriend || customType == CustomMessageType.blockedByFriend;
|
||||
return customType == CustomMessageType.deletedByFriend ||
|
||||
customType == CustomMessageType.blockedByFriend;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void sendFriendVerification() => AppNavigator.startSendVerificationApplication(userID: userID);
|
||||
void sendFriendVerification() =>
|
||||
AppNavigator.startSendVerificationApplication(userID: userID);
|
||||
|
||||
void _setSdkSyncDataListener() {
|
||||
connectionSub = imLogic.imSdkStatusPublishSubject.listen((value) {
|
||||
@@ -1699,7 +1736,9 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
bool showBubbleBg(Message message) {
|
||||
return !isNotificationType(message) && !isFailedHintMessage(message) && !isRevokeMessage(message);
|
||||
return !isNotificationType(message) &&
|
||||
!isFailedHintMessage(message) &&
|
||||
!isRevokeMessage(message);
|
||||
}
|
||||
|
||||
bool isRevokeMessage(Message message) {
|
||||
@@ -1748,7 +1787,8 @@ class ChatLogic extends SuperController {
|
||||
}
|
||||
|
||||
Future<void> _loadHistoryForSyncEnd() async {
|
||||
final result = await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList(
|
||||
final result =
|
||||
await OpenIM.iMManager.messageManager.getAdvancedHistoryMessageList(
|
||||
conversationID: conversationInfo.conversationID,
|
||||
count: messageList.length < _pageSize ? _pageSize : messageList.length,
|
||||
startMsg: null,
|
||||
|
||||
@@ -86,13 +86,23 @@ class ChatPage extends StatelessWidget {
|
||||
onTapUserProfile: handleUserProfileTap,
|
||||
);
|
||||
|
||||
void handleUserProfileTap(({String userID, String name, String? faceURL, String? groupID}) userProfile) {
|
||||
final userInfo = UserInfo(userID: userProfile.userID, nickname: userProfile.name, faceURL: userProfile.faceURL);
|
||||
void handleUserProfileTap(
|
||||
({
|
||||
String userID,
|
||||
String name,
|
||||
String? faceURL,
|
||||
String? groupID
|
||||
}) userProfile) {
|
||||
final userInfo = UserInfo(
|
||||
userID: userProfile.userID,
|
||||
nickname: userProfile.name,
|
||||
faceURL: userProfile.faceURL);
|
||||
logic.viewUserInfo(userInfo);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -125,7 +135,8 @@ class ChatPage extends StatelessWidget {
|
||||
child: Hero(
|
||||
tag: message.clientMsgID!,
|
||||
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 view = ChatCallItemView(type: type, content: content);
|
||||
return CustomTypeInfo(view);
|
||||
} else if (viewType == CustomMessageType.deletedByFriend || viewType == CustomMessageType.blockedByFriend) {
|
||||
} else if (viewType == CustomMessageType.deletedByFriend ||
|
||||
viewType == CustomMessageType.blockedByFriend) {
|
||||
final view = ChatFriendRelationshipAbnormalHintView(
|
||||
name: logic.nickname.value,
|
||||
onTap: logic.sendFriendVerification,
|
||||
@@ -244,10 +256,11 @@ class ChatPage extends StatelessWidget {
|
||||
toolbox: ChatToolBox(
|
||||
onTapAlbum: logic.onTapAlbum,
|
||||
onTapCamera: logic.onTapCamera,
|
||||
onTapCall: FeatureFlags.livekitCall && logic.isSingleChat ? logic.call : null,
|
||||
onTapCall: FeatureFlags.livekitCall && logic.isSingleChat
|
||||
? logic.call
|
||||
: null,
|
||||
onTapCard: logic.onTapCarte,
|
||||
onTapFile: logic.onTapFile,
|
||||
onTapLocation: logic.onTapLocation,
|
||||
),
|
||||
voiceRecordBar: bar,
|
||||
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:get/get.dart';
|
||||
import 'package:openim/pages/login/login_logic.dart';
|
||||
@@ -26,8 +25,6 @@ class MyInfoLogic extends GetxController {
|
||||
attr: EditAttr.mobile,
|
||||
);
|
||||
|
||||
void editEmail() => AppNavigator.startEditMyInfo(attr: EditAttr.email, maxLength: 30);
|
||||
|
||||
void openPhotoSheet() {
|
||||
IMViews.openPhotoSheet(
|
||||
onData: (path, url) async {
|
||||
@@ -43,62 +40,6 @@ class MyInfoLogic extends GetxController {
|
||||
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
|
||||
void onReady() {
|
||||
_queryMyFullIno();
|
||||
|
||||
@@ -42,16 +42,18 @@ class MyInfoPage extends StatelessWidget {
|
||||
),
|
||||
_buildItemView(
|
||||
label: StrRes.gender,
|
||||
value: imLogic.userInfo.value.isMale ? StrRes.man : StrRes.woman,
|
||||
onTap: logic.selectGender,
|
||||
value: _genderText(imLogic.userInfo.value.gender),
|
||||
showRightArrow: false,
|
||||
),
|
||||
_buildItemView(
|
||||
label: StrRes.birthDay,
|
||||
value: DateUtil.formatDateMs(
|
||||
imLogic.userInfo.value.birth ?? 0,
|
||||
format: IMUtils.getTimeFormat1(),
|
||||
),
|
||||
onTap: logic.openDatePicker,
|
||||
value: imLogic.userInfo.value.birth == null
|
||||
? ''
|
||||
: DateUtil.formatDateMs(
|
||||
imLogic.userInfo.value.birth!,
|
||||
format: IMUtils.getTimeFormat1(),
|
||||
),
|
||||
showRightArrow: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -68,7 +70,7 @@ class MyInfoPage extends StatelessWidget {
|
||||
_buildItemView(
|
||||
label: StrRes.email,
|
||||
value: imLogic.userInfo.value.email,
|
||||
onTap: logic.editEmail,
|
||||
showRightArrow: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -93,6 +95,12 @@ class MyInfoPage extends StatelessWidget {
|
||||
child: Column(children: children),
|
||||
);
|
||||
|
||||
String _genderText(int? gender) {
|
||||
if (gender == 1) return StrRes.man;
|
||||
if (gender == 2) return StrRes.woman;
|
||||
return '';
|
||||
}
|
||||
|
||||
Widget _buildItemView({
|
||||
required String label,
|
||||
String? value,
|
||||
|
||||
@@ -33,6 +33,7 @@ export 'src/utils/sp_util.dart';
|
||||
export 'src/utils/utils.dart';
|
||||
export 'src/utils/voice_record.dart';
|
||||
export 'src/utils/api_service.dart';
|
||||
export 'src/utils/user_full_info_mapper.dart';
|
||||
export 'src/widgets/avatar_view.dart';
|
||||
export 'src/widgets/azlist_view.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_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/water_mark_view.dart';
|
||||
export 'src/widgets/custom_pop_up_menu.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -11,14 +10,19 @@ import 'package:sprintf/sprintf.dart';
|
||||
import 'utils/api_service.dart';
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
@@ -197,43 +201,16 @@ class Apis {
|
||||
int? allowBeep,
|
||||
int? allowVibration,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> param = {'userID': userID};
|
||||
void put(String key, dynamic value) {
|
||||
if (null != value) {
|
||||
param[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
// 原 chat `/user/update` 已随 :10008 下线。OpenIM 只同步昵称/头像;
|
||||
// 邮箱、性别、生日等业务字段由调用方更新本地状态,不打账号服务。
|
||||
if (userID.isEmpty) return {};
|
||||
if (nickname != null || faceURL != null) {
|
||||
await OpenIM.iMManager.userManager.setSelfInfo(
|
||||
nickname: nickname,
|
||||
faceURL: faceURL,
|
||||
);
|
||||
} 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(
|
||||
@@ -251,7 +228,9 @@ class Apis {
|
||||
options: chatTokenOptions,
|
||||
);
|
||||
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 [];
|
||||
} catch (e, s) {
|
||||
@@ -269,26 +248,16 @@ class Apis {
|
||||
int showNumber = 10,
|
||||
required List<String> userIDList,
|
||||
}) async {
|
||||
if (userIDList.isEmpty) return [];
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.getUsersFullInfo,
|
||||
data: {
|
||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
||||
'userIDs': userIDList,
|
||||
'platform': IMUtils.getPlatform(),
|
||||
},
|
||||
options: chatTokenOptions,
|
||||
final users = await OpenIM.iMManager.userManager.getUsersInfo(
|
||||
userIDList: userIDList,
|
||||
);
|
||||
if (data['users'] is List) {
|
||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
||||
}
|
||||
return null;
|
||||
return users
|
||||
.map((e) => UserFullInfoMapper.fromImJson(e.toJson()))
|
||||
.toList();
|
||||
} 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');
|
||||
Logger.print('getUserFullInfo e:$e s:$s');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -298,34 +267,20 @@ class Apis {
|
||||
int pageNumber = 1,
|
||||
int showNumber = 10,
|
||||
}) async {
|
||||
try {
|
||||
final data = await HttpUtil.post(
|
||||
Urls.searchUserFullInfo,
|
||||
data: {
|
||||
'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 [];
|
||||
}
|
||||
final keyword = content.trim();
|
||||
if (keyword.isEmpty) return [];
|
||||
if (pageNumber > 1) return [];
|
||||
return getUserFullInfo(userIDList: [keyword], showNumber: showNumber);
|
||||
}
|
||||
|
||||
static Future<UserFullInfo?> queryMyFullInfo() async {
|
||||
final list = await Apis.getUserFullInfo(
|
||||
userIDList: [OpenIM.iMManager.userID],
|
||||
);
|
||||
return list?.firstOrNull;
|
||||
try {
|
||||
final user = await OpenIM.iMManager.userManager.getSelfUserInfo();
|
||||
return UserFullInfoMapper.fromImJson(user.toJson());
|
||||
} catch (e, s) {
|
||||
Logger.print('queryMyFullInfo e:$e s:$s');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
Urls.getTokenForRTC,
|
||||
data: {
|
||||
@@ -410,7 +366,10 @@ class Apis {
|
||||
}
|
||||
|
||||
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}) {
|
||||
|
||||
@@ -71,6 +71,7 @@ const Map<String, String> en_US = {
|
||||
"video": "Video",
|
||||
"voice": "Voice",
|
||||
"location": "Location",
|
||||
"locationMessage": "Location",
|
||||
"file": "File",
|
||||
"carte": "Card",
|
||||
"emoji": "Custom Emoji",
|
||||
@@ -120,7 +121,7 @@ const Map<String, String> en_US = {
|
||||
"cancel": "Cancel",
|
||||
"determine": "OK",
|
||||
"toolboxAlbum": "Album",
|
||||
"toolboxCall": "Video Call",
|
||||
"toolboxCall": "Voice Call",
|
||||
"toolboxCamera": "Camera",
|
||||
"toolboxCard": "Card",
|
||||
"toolboxFile": "File",
|
||||
@@ -224,7 +225,7 @@ const Map<String, String> en_US = {
|
||||
'position': 'Position',
|
||||
'personalInfo': 'Personal Info',
|
||||
'viewDynamics': 'View Dynamics',
|
||||
'audioAndVideoCall': 'Call',
|
||||
'audioAndVideoCall': 'Voice Call',
|
||||
'sendMessage': 'Message',
|
||||
'avatar': 'Avatar',
|
||||
'name': 'Name',
|
||||
|
||||
@@ -71,6 +71,7 @@ const Map<String, String> zh_CN = {
|
||||
"video": "视频",
|
||||
"voice": "语音",
|
||||
"location": "位置",
|
||||
"locationMessage": "位置消息",
|
||||
"file": "文件",
|
||||
"carte": "名片",
|
||||
"emoji": "自定义表情",
|
||||
@@ -120,7 +121,7 @@ const Map<String, String> zh_CN = {
|
||||
"cancel": "取消",
|
||||
"determine": "确定",
|
||||
"toolboxAlbum": "相册",
|
||||
"toolboxCall": "视频通话",
|
||||
"toolboxCall": "语音通话",
|
||||
"toolboxCamera": "拍摄",
|
||||
"toolboxCard": "名片",
|
||||
"toolboxFile": "文件",
|
||||
@@ -224,7 +225,7 @@ const Map<String, String> zh_CN = {
|
||||
'position': '职位',
|
||||
'personalInfo': '个人资料',
|
||||
'viewDynamics': '查看动态',
|
||||
'audioAndVideoCall': '音视频通话',
|
||||
'audioAndVideoCall': '语音通话',
|
||||
'sendMessage': '发消息',
|
||||
'avatar': '头像',
|
||||
'name': '姓名',
|
||||
|
||||
@@ -58,7 +58,8 @@ class StrRes {
|
||||
|
||||
static String get resendVerificationCode => 'resendVerificationCode'.tr;
|
||||
|
||||
static String get verificationCodeTimingReminder => 'verificationCodeTimingReminder'.tr;
|
||||
static String get verificationCodeTimingReminder =>
|
||||
'verificationCodeTimingReminder'.tr;
|
||||
|
||||
static String get defaultVerificationCode => 'defaultVerificationCode'.tr;
|
||||
|
||||
@@ -160,6 +161,8 @@ class StrRes {
|
||||
|
||||
static String get location => 'location'.tr;
|
||||
|
||||
static String get locationMessage => 'locationMessage'.tr;
|
||||
|
||||
static String get file => 'file'.tr;
|
||||
|
||||
static String get carte => 'carte'.tr;
|
||||
@@ -276,7 +279,8 @@ class StrRes {
|
||||
|
||||
static String get releaseToSend => 'releaseToSend'.tr;
|
||||
|
||||
static String get releaseToSendSwipeUpToCancel => 'releaseToSendSwipeUpToCancel'.tr;
|
||||
static String get releaseToSendSwipeUpToCancel =>
|
||||
'releaseToSendSwipeUpToCancel'.tr;
|
||||
|
||||
static String get liftFingerToCancelSend => 'liftFingerToCancelSend'.tr;
|
||||
|
||||
@@ -704,7 +708,8 @@ class StrRes {
|
||||
|
||||
static String get confirm => 'confirm'.tr;
|
||||
|
||||
static String get confirmTransferGroupToUser => 'confirmTransferGroupToUser'.tr;
|
||||
static String get confirmTransferGroupToUser =>
|
||||
'confirmTransferGroupToUser'.tr;
|
||||
|
||||
static String get removeGroupMember => 'removeGroupMember'.tr;
|
||||
|
||||
@@ -970,7 +975,8 @@ class StrRes {
|
||||
|
||||
static String get confirmTheChanges => 'confirmTheChanges'.tr;
|
||||
|
||||
static String get invitesYouToVideoConference => 'invitesYouToVideoConference'.tr;
|
||||
static String get invitesYouToVideoConference =>
|
||||
'invitesYouToVideoConference'.tr;
|
||||
|
||||
static String get over => 'over'.tr;
|
||||
|
||||
@@ -1074,7 +1080,8 @@ class StrRes {
|
||||
|
||||
static String get sendAnother => 'sendAnother'.tr;
|
||||
|
||||
static String get confirmDelTagNotificationHint => 'confirmDelTagNotificationHint'.tr;
|
||||
static String get confirmDelTagNotificationHint =>
|
||||
'confirmDelTagNotificationHint'.tr;
|
||||
|
||||
static String get contentNotBlank => 'contentNotBlank'.tr;
|
||||
|
||||
@@ -1086,11 +1093,13 @@ class StrRes {
|
||||
|
||||
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 periodicallyDeleteMessageDescription => 'periodicallyDeleteMessageDescription'.tr;
|
||||
static String get periodicallyDeleteMessageDescription =>
|
||||
'periodicallyDeleteMessageDescription'.tr;
|
||||
|
||||
static String get nDay => 'nDay'.tr;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ class Urls {
|
||||
"${Config.imApiUrl}/manager/get_users_online_status";
|
||||
static String get queryAllUsers =>
|
||||
"${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 searchFriendInfo => "${Config.appAuthUrl}/friend/search";
|
||||
static String get getUsersFullInfo => "${Config.appAuthUrl}/user/find/full";
|
||||
|
||||
@@ -10,10 +10,6 @@ import 'package:sprintf/sprintf.dart';
|
||||
class Permissions {
|
||||
Permissions._();
|
||||
|
||||
static Future<bool> checkSystemAlertWindow() async {
|
||||
return Permission.systemAlertWindow.isGranted;
|
||||
}
|
||||
|
||||
static Future<bool> checkStorage() async {
|
||||
return await Permission.storage.isGranted;
|
||||
}
|
||||
@@ -22,7 +18,8 @@ class Permissions {
|
||||
if (await Permission.camera.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.camera.isPermanentlyDenied || await Permission.camera.isDenied) {
|
||||
if (await Permission.camera.isPermanentlyDenied ||
|
||||
await Permission.camera.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.camera.title);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +49,8 @@ class Permissions {
|
||||
if (await Permission.manageExternalStorage.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.storage.isPermanentlyDenied || await Permission.storage.isDenied) {
|
||||
if (await Permission.storage.isPermanentlyDenied ||
|
||||
await Permission.storage.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.storage.title);
|
||||
}
|
||||
}
|
||||
@@ -61,25 +59,18 @@ class Permissions {
|
||||
if (await Permission.microphone.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.microphone.isPermanentlyDenied || await Permission.microphone.isDenied) {
|
||||
if (await Permission.microphone.isPermanentlyDenied ||
|
||||
await Permission.microphone.isDenied) {
|
||||
_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 {
|
||||
if (await Permission.speech.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.speech.isPermanentlyDenied || await Permission.speech.isDenied) {
|
||||
if (await Permission.speech.isPermanentlyDenied ||
|
||||
await Permission.speech.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.speech.title);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +84,8 @@ class Permissions {
|
||||
if (await Permission.photos.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) {
|
||||
if (await Permission.photos.isPermanentlyDenied ||
|
||||
await Permission.photos.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.photos.title);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +93,8 @@ class Permissions {
|
||||
if (await Permission.photos.request().isGranted) {
|
||||
onGranted?.call();
|
||||
}
|
||||
if (await Permission.photos.isPermanentlyDenied || await Permission.photos.isDenied) {
|
||||
if (await Permission.photos.isPermanentlyDenied ||
|
||||
await Permission.photos.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.photos.title);
|
||||
}
|
||||
}
|
||||
@@ -111,20 +104,14 @@ class Permissions {
|
||||
if (await Permission.notification.request().isGranted) {
|
||||
return true;
|
||||
}
|
||||
if (await Permission.notification.isPermanentlyDenied || await Permission.notification.isDenied) {
|
||||
if (await Permission.notification.isPermanentlyDenied ||
|
||||
await Permission.notification.isDenied) {
|
||||
_showPermissionDeniedDialog(Permission.notification.title);
|
||||
}
|
||||
|
||||
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 {
|
||||
final permissions = [
|
||||
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();
|
||||
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}) {
|
||||
DateTime now = DateTime.now();
|
||||
if (null == last || now.difference(last ?? now).inMilliseconds > milliseconds) {
|
||||
if (null == last ||
|
||||
now.difference(last ?? now).inMilliseconds > milliseconds) {
|
||||
last = now;
|
||||
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 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);
|
||||
}
|
||||
|
||||
@@ -172,14 +175,17 @@ class IMUtils {
|
||||
final directory = await createTempDir(dir: 'video');
|
||||
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 state = FFmpegKitConfig.sessionStateToString(await session.getState());
|
||||
final state =
|
||||
FFmpegKitConfig.sessionStateToString(await session.getState());
|
||||
final returnCode = await session.getReturnCode();
|
||||
|
||||
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();
|
||||
@@ -195,11 +201,14 @@ class IMUtils {
|
||||
|
||||
final output = await FFprobeKit.getMediaInformation(path);
|
||||
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';
|
||||
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';
|
||||
|
||||
String ffmpegCommand =
|
||||
@@ -210,7 +219,8 @@ class IMUtils {
|
||||
if (isAAC) {
|
||||
return File(targetPath);
|
||||
} 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);
|
||||
} 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();
|
||||
|
||||
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);
|
||||
|
||||
return File(targetPath);
|
||||
@@ -241,7 +253,8 @@ class IMUtils {
|
||||
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 name = path.substring(path.lastIndexOf("/") + 1).toLowerCase();
|
||||
|
||||
@@ -360,14 +373,16 @@ class IMUtils {
|
||||
String? externalStorageDirPath;
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
externalStorageDirPath = await PathProviderPlatform.instance.getDownloadsPath();
|
||||
externalStorageDirPath =
|
||||
await PathProviderPlatform.instance.getDownloadsPath();
|
||||
} catch (err, st) {
|
||||
Logger.print('failed to get downloads path: $err, $st');
|
||||
final directory = await getExternalStorageDirectory();
|
||||
externalStorageDirPath = directory?.path;
|
||||
}
|
||||
} else if (Platform.isIOS) {
|
||||
externalStorageDirPath = (await getApplicationDocumentsDirectory()).absolute.path;
|
||||
externalStorageDirPath =
|
||||
(await getApplicationDocumentsDirectory()).absolute.path;
|
||||
}
|
||||
return externalStorageDirPath!;
|
||||
}
|
||||
@@ -384,7 +399,8 @@ class IMUtils {
|
||||
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;
|
||||
var milliseconds = list.firstOrNull?.sendTime;
|
||||
if (null == milliseconds) return list;
|
||||
@@ -421,7 +437,9 @@ class IMUtils {
|
||||
final yesterday = now.subtract(Duration(days: 1));
|
||||
|
||||
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)) {
|
||||
@@ -448,13 +466,16 @@ class IMUtils {
|
||||
}
|
||||
|
||||
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) {
|
||||
final weekStart = date2.subtract(Duration(days: date2.weekday - 1));
|
||||
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) {
|
||||
@@ -530,7 +551,8 @@ class IMUtils {
|
||||
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({
|
||||
required String content,
|
||||
@@ -566,9 +588,11 @@ class IMUtils {
|
||||
int maxLines = 1,
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
final TextPainter textPainter =
|
||||
TextPainter(text: TextSpan(text: text, style: style), maxLines: maxLines, textDirection: TextDirection.ltr)
|
||||
..layout(minWidth: 0, maxWidth: maxWidth);
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
maxLines: maxLines,
|
||||
textDirection: TextDirection.ltr)
|
||||
..layout(minWidth: 0, maxWidth: maxWidth);
|
||||
return textPainter.size;
|
||||
}
|
||||
|
||||
@@ -578,7 +602,10 @@ class IMUtils {
|
||||
int maxLines = 1,
|
||||
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);
|
||||
|
||||
static bool isUrlValid(String? url) {
|
||||
@@ -600,11 +627,16 @@ class IMUtils {
|
||||
}
|
||||
|
||||
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) {
|
||||
return (userID == OpenIM.iMManager.userID ? OpenIM.iMManager.userInfo.nickname : nickname) ?? '';
|
||||
return (userID == OpenIM.iMManager.userID
|
||||
? OpenIM.iMManager.userInfo.nickname
|
||||
: nickname) ??
|
||||
'';
|
||||
}
|
||||
|
||||
static String? parseNtf(
|
||||
@@ -628,7 +660,8 @@ class IMUtils {
|
||||
case MessageType.groupInfoSetNotification:
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -649,8 +682,12 @@ class IMUtils {
|
||||
final ntf = InvitedJoinGroupNotification.fromJson(map);
|
||||
|
||||
final label = StrRes.invitedJoinGroupNtf;
|
||||
final b = ntf.invitedUserList?.map((e) => getGroupMemberShowName(e)).toList().join('、');
|
||||
text = sprintf(label, [getGroupMemberShowName(ntf.opUser!), b ?? '']);
|
||||
final b = ntf.invitedUserList
|
||||
?.map((e) => getGroupMemberShowName(e))
|
||||
.toList()
|
||||
.join('、');
|
||||
text = sprintf(
|
||||
label, [getGroupMemberShowName(ntf.opUser!), b ?? '']);
|
||||
}
|
||||
break;
|
||||
case MessageType.memberKickedNotification:
|
||||
@@ -658,7 +695,10 @@ class IMUtils {
|
||||
final ntf = KickedGroupMemeberNotification.fromJson(map);
|
||||
|
||||
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!)]);
|
||||
}
|
||||
break;
|
||||
@@ -683,7 +723,10 @@ class IMUtils {
|
||||
final ntf = GroupRightsTransferNoticication.fromJson(map);
|
||||
|
||||
final label = StrRes.transferredGroupNtf;
|
||||
text = sprintf(label, [getGroupMemberShowName(ntf.opUser!), getGroupMemberShowName(ntf.newGroupOwner!)]);
|
||||
text = sprintf(label, [
|
||||
getGroupMemberShowName(ntf.opUser!),
|
||||
getGroupMemberShowName(ntf.newGroupOwner!)
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case MessageType.groupMemberMutedNotification:
|
||||
@@ -692,8 +735,11 @@ class IMUtils {
|
||||
|
||||
final label = StrRes.muteMemberNtf;
|
||||
final c = ntf.mutedSeconds;
|
||||
text = sprintf(
|
||||
label, [getGroupMemberShowName(ntf.mutedUser!), getGroupMemberShowName(ntf.opUser!), mutedTime(c!)]);
|
||||
text = sprintf(label, [
|
||||
getGroupMemberShowName(ntf.mutedUser!),
|
||||
getGroupMemberShowName(ntf.opUser!),
|
||||
mutedTime(c!)
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case MessageType.groupMemberCancelMutedNotification:
|
||||
@@ -701,7 +747,10 @@ class IMUtils {
|
||||
final ntf = MuteMemberNotification.fromJson(map);
|
||||
|
||||
final label = StrRes.muteCancelMemberNtf;
|
||||
text = sprintf(label, [getGroupMemberShowName(ntf.mutedUser!), getGroupMemberShowName(ntf.opUser!)]);
|
||||
text = sprintf(label, [
|
||||
getGroupMemberShowName(ntf.mutedUser!),
|
||||
getGroupMemberShowName(ntf.opUser!)
|
||||
]);
|
||||
}
|
||||
break;
|
||||
case MessageType.groupMutedNotification:
|
||||
@@ -737,7 +786,8 @@ class IMUtils {
|
||||
break;
|
||||
case MessageType.groupMemberInfoChangedNotification:
|
||||
final ntf = GroupMemberInfoChangedNotification.fromJson(map);
|
||||
text = sprintf(StrRes.memberInfoChangedNtf, [getGroupMemberShowName(ntf.opUser!)]);
|
||||
text = sprintf(StrRes.memberInfoChangedNtf,
|
||||
[getGroupMemberShowName(ntf.opUser!)]);
|
||||
break;
|
||||
case MessageType.groupInfoSetAnnouncementNotification:
|
||||
if (isConversation) {
|
||||
@@ -747,7 +797,8 @@ class IMUtils {
|
||||
break;
|
||||
case MessageType.groupInfoSetNameNotification:
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -851,7 +902,8 @@ class IMUtils {
|
||||
switch (customType) {
|
||||
case CustomMessageType.call:
|
||||
var type = map['data']['type'];
|
||||
content = '[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]';
|
||||
content =
|
||||
'[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]';
|
||||
break;
|
||||
case CustomMessageType.emoji:
|
||||
content = '[${StrRes.emoji}]';
|
||||
@@ -924,7 +976,8 @@ class IMUtils {
|
||||
switch (state) {
|
||||
case 'beHangup':
|
||||
case 'hangup':
|
||||
content = sprintf(StrRes.callDuration, [seconds2HMS(duration)]);
|
||||
content =
|
||||
sprintf(StrRes.callDuration, [seconds2HMS(duration)]);
|
||||
break;
|
||||
case 'cancel':
|
||||
content = StrRes.cancelled;
|
||||
@@ -991,8 +1044,11 @@ class IMUtils {
|
||||
final atUserInfos = message.atTextElem!.atUsersInfo!;
|
||||
|
||||
for (final userID in atUserIDs) {
|
||||
final groupNickname =
|
||||
(newMapping[userID] ?? atUserInfos.firstWhere((e) => e.atUserID == userID).groupNickname) ?? userID;
|
||||
final groupNickname = (newMapping[userID] ??
|
||||
atUserInfos
|
||||
.firstWhere((e) => e.atUserID == userID)
|
||||
.groupNickname) ??
|
||||
userID;
|
||||
mapping[userID] = getAtNickname(userID, groupNickname);
|
||||
}
|
||||
}
|
||||
@@ -1065,20 +1121,27 @@ class IMUtils {
|
||||
previewUrlPicture(
|
||||
[
|
||||
MediaSource(
|
||||
url: message.pictureElem!.sourcePicture!.url!, thumbnail: message.pictureElem!.snapshotPicture!.url!)
|
||||
url: message.pictureElem!.sourcePicture!.url!,
|
||||
thumbnail: message.pictureElem!.snapshotPicture!.url!)
|
||||
],
|
||||
currentIndex: 0,
|
||||
);
|
||||
} else {
|
||||
final picList = allList
|
||||
.where((element) => element.contentType == MessageType.picture || element.contentType == MessageType.video)
|
||||
.where((element) =>
|
||||
element.contentType == MessageType.picture ||
|
||||
element.contentType == MessageType.video)
|
||||
.toList();
|
||||
final index = picList.indexOf(message);
|
||||
final urls = picList.map((e) {
|
||||
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 {
|
||||
return MediaSource(url: e.videoElem!.videoUrl!, thumbnail: e.videoElem!.snapshotUrl!);
|
||||
return MediaSource(
|
||||
url: e.videoElem!.videoUrl!,
|
||||
thumbnail: e.videoElem!.snapshotUrl!);
|
||||
}
|
||||
}).toList();
|
||||
previewUrlPicture(urls, currentIndex: index == -1 ? 0 : index);
|
||||
@@ -1106,7 +1169,8 @@ class IMUtils {
|
||||
const begin = Offset(0.0, 1.0);
|
||||
const end = Offset.zero;
|
||||
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);
|
||||
|
||||
return SlideTransition(
|
||||
@@ -1136,7 +1200,8 @@ class IMUtils {
|
||||
|
||||
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);
|
||||
String? availablePath;
|
||||
@@ -1145,9 +1210,11 @@ class IMUtils {
|
||||
} else if (isExitCachePath) {
|
||||
availablePath = cachePath;
|
||||
}
|
||||
final isAvailableFileSize =
|
||||
isExitSourcePath || isExitCachePath ? (await File(availablePath!).length() == fileSize) : false;
|
||||
Logger.print('previewFile isAvailableFileSize: $isAvailableFileSize isExitNetwork: $isExitNetwork');
|
||||
final isAvailableFileSize = isExitSourcePath || isExitCachePath
|
||||
? (await File(availablePath!).length() == fileSize)
|
||||
: false;
|
||||
Logger.print(
|
||||
'previewFile isAvailableFileSize: $isAvailableFileSize isExitNetwork: $isExitNetwork');
|
||||
if (isAvailableFileSize) {
|
||||
String? mimeType = lookupMimeType(fileName ?? '');
|
||||
if (null != mimeType && allowVideoType(mimeType)) {
|
||||
@@ -1159,7 +1226,9 @@ class IMUtils {
|
||||
previewPicture(Message()
|
||||
..clientMsgID = message.clientMsgID
|
||||
..contentType = MessageType.picture
|
||||
..pictureElem = PictureElem(sourcePath: availablePath, sourcePicture: PictureInfo(url: url)));
|
||||
..pictureElem = PictureElem(
|
||||
sourcePath: availablePath,
|
||||
sourcePicture: PictureInfo(url: url)));
|
||||
} else {
|
||||
openFileByOtherApp(availablePath);
|
||||
}
|
||||
@@ -1183,7 +1252,8 @@ class IMUtils {
|
||||
bool onlySave = false,
|
||||
ValueChanged<OperateType>? onOperate}) {
|
||||
void saveVideo(BuildContext ctx, String url, {int? length}) async {
|
||||
final cachedVideoControllerService = CachedVideoControllerService(DefaultCacheManager());
|
||||
final cachedVideoControllerService =
|
||||
CachedVideoControllerService(DefaultCacheManager());
|
||||
final cached = await cachedVideoControllerService.getCacheFile(url);
|
||||
|
||||
if (cached != null) {
|
||||
@@ -1196,7 +1266,8 @@ class IMUtils {
|
||||
} else {
|
||||
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) {
|
||||
if (status == EasyLoadingStatus.dismiss) {
|
||||
@@ -1235,7 +1306,8 @@ class IMUtils {
|
||||
switch (type) {
|
||||
case OperateType.save:
|
||||
if (msg.videoElem != null) {
|
||||
saveVideo(context, msg.videoElem!.videoUrl!, length: msg.videoElem!.videoSize);
|
||||
saveVideo(context, msg.videoElem!.videoUrl!,
|
||||
length: msg.videoElem!.videoSize);
|
||||
} else {
|
||||
final url = msg.pictureElem?.sourcePicture?.url;
|
||||
if (url?.isNotEmpty == true) {
|
||||
@@ -1254,14 +1326,18 @@ class IMUtils {
|
||||
final sources = message.isVideoType
|
||||
? MediaSource(
|
||||
url: message.videoElem?.videoUrl,
|
||||
thumbnail: message.videoElem!.snapshotUrl?.adjustThumbnailAbsoluteString(960) ?? '',
|
||||
thumbnail: message.videoElem!.snapshotUrl
|
||||
?.adjustThumbnailAbsoluteString(960) ??
|
||||
'',
|
||||
file: File(message.videoElem!.videoPath!),
|
||||
tag: message.clientMsgID,
|
||||
isVideo: true,
|
||||
)
|
||||
: MediaSource(
|
||||
url: message.pictureElem?.sourcePicture?.url,
|
||||
thumbnail: message.pictureElem!.snapshotPicture?.url?.adjustThumbnailAbsoluteString(960) ?? '',
|
||||
thumbnail: message.pictureElem!.snapshotPicture?.url
|
||||
?.adjustThumbnailAbsoluteString(960) ??
|
||||
'',
|
||||
file: File(message.pictureElem!.sourcePath!),
|
||||
tag: message.clientMsgID,
|
||||
);
|
||||
@@ -1326,19 +1402,36 @@ class IMUtils {
|
||||
}
|
||||
|
||||
static void previewLocation(Message message) {
|
||||
var location = message.locationElem;
|
||||
Map detail = json.decode(location!.description!);
|
||||
Logger.print('previewLocation ${location.latitude} ${location.longitude}');
|
||||
Get.to(
|
||||
() => MapView(
|
||||
try {
|
||||
final location = message.locationElem;
|
||||
if (location == null ||
|
||||
location.latitude == null ||
|
||||
location.longitude == null) {
|
||||
IMViews.showToast(StrRes.locationMessage);
|
||||
return;
|
||||
}
|
||||
final data = LocationBubbleData.parse(
|
||||
description: location.description ?? '',
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
address1: detail['name'],
|
||||
address2: detail['addr'],
|
||||
),
|
||||
transition: Transition.cupertino,
|
||||
popGesture: true,
|
||||
);
|
||||
fallbackTitle: StrRes.locationMessage,
|
||||
);
|
||||
Logger.print(
|
||||
'previewLocation ${location.latitude} ${location.longitude}');
|
||||
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(
|
||||
@@ -1353,7 +1446,8 @@ class IMUtils {
|
||||
Function(Message msg)? meetingItemClick,
|
||||
VoidCallback? onForward,
|
||||
}) async {
|
||||
if (message.contentType == MessageType.picture || message.contentType == MessageType.video) {
|
||||
if (message.contentType == MessageType.picture ||
|
||||
message.contentType == MessageType.video) {
|
||||
previewMediaFile(
|
||||
context: Get.context!,
|
||||
message: message,
|
||||
@@ -1464,15 +1558,18 @@ class IMUtils {
|
||||
if (mimeType == 'application/pdf') {
|
||||
return ImageRes.filePdf;
|
||||
} else if (mimeType == 'application/msword' ||
|
||||
mimeType == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||
mimeType ==
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||
return ImageRes.fileWord;
|
||||
} else if (mimeType == 'application/vnd.ms-excel' ||
|
||||
mimeType == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
||||
mimeType ==
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
||||
return ImageRes.fileExcel;
|
||||
} else if (mimeType == 'application/vnd.ms-powerpoint') {
|
||||
return ImageRes.filePpt;
|
||||
} 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;
|
||||
}
|
||||
/*else if (mimeType.startsWith('audio/')) {
|
||||
@@ -1516,7 +1613,10 @@ class IMUtils {
|
||||
final checkedList = <String>[];
|
||||
final values = result.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!);
|
||||
}
|
||||
}
|
||||
@@ -1531,7 +1631,10 @@ class IMUtils {
|
||||
for (var item in checkedList) {
|
||||
if (item is ConversationInfo) {
|
||||
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;
|
||||
} else if (item is GroupInfo) {
|
||||
checkedMap[item.groupID] = item;
|
||||
@@ -1540,10 +1643,14 @@ class IMUtils {
|
||||
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?>>[];
|
||||
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});
|
||||
} else if (item is GroupInfo) {
|
||||
map.add({'nickname': item.groupName, 'faceURL': item.faceURL});
|
||||
@@ -1555,7 +1662,10 @@ class IMUtils {
|
||||
}
|
||||
|
||||
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;
|
||||
} else if (info is ConversationInfo) {
|
||||
return info.userID;
|
||||
@@ -1570,14 +1680,18 @@ class IMUtils {
|
||||
} else if (info is ConversationInfo) {
|
||||
return info.groupID;
|
||||
}
|
||||
|
||||
|
||||
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?>>[];
|
||||
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});
|
||||
} else if (item is GroupInfo) {
|
||||
map.add({'userID': null, 'groupID': item.groupID});
|
||||
@@ -1612,8 +1726,10 @@ class IMUtils {
|
||||
return formatDateMs(ms, format: isZH ? 'yyyy年MM月dd' : 'yyyy/MM/dd');
|
||||
}
|
||||
|
||||
static Future<bool> checkingBiometric(LocalAuthentication auth) => auth.authenticate(
|
||||
localizedReason: 'Scan your fingerprint (or face or other) to authenticate.',
|
||||
static Future<bool> checkingBiometric(LocalAuthentication auth) =>
|
||||
auth.authenticate(
|
||||
localizedReason:
|
||||
'Scan your fingerprint (or face or other) to authenticate.',
|
||||
options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
),
|
||||
@@ -1636,7 +1752,8 @@ class IMUtils {
|
||||
goToSettingsButton: 'Go to settings',
|
||||
goToSettingsDescription:
|
||||
'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}$',
|
||||
).hasMatch(password);
|
||||
|
||||
static TextInputFormatter getPasswordFormatter() => FilteringTextInputFormatter.allow(
|
||||
static TextInputFormatter getPasswordFormatter() =>
|
||||
FilteringTextInputFormatter.allow(
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@@ -1680,7 +1801,8 @@ class IMUtils {
|
||||
notificationTitle: title,
|
||||
notificationText: text,
|
||||
notificationImportance: AndroidNotificationImportance.normal,
|
||||
notificationIcon: const AndroidResource(name: 'ic_launcher', defType: 'mipmap'),
|
||||
notificationIcon: const AndroidResource(
|
||||
name: 'ic_launcher', defType: 'mipmap'),
|
||||
shouldRequestBatteryOptimizationsOff: false));
|
||||
}
|
||||
if (hasPermissions && !FlutterBackground.isBackgroundExecutionEnabled) {
|
||||
@@ -1689,7 +1811,9 @@ class IMUtils {
|
||||
} catch (e) {
|
||||
if (!isRetry) {
|
||||
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 {
|
||||
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(
|
||||
height: height ?? 44.h,
|
||||
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),
|
||||
),
|
||||
child: InkWell(
|
||||
@@ -78,7 +80,7 @@ class ImageTextButton extends StatelessWidget {
|
||||
final Function()? onTap;
|
||||
|
||||
ImageTextButton.call({super.key, this.onTap})
|
||||
: icon = ImageRes.audioAndVideoCall,
|
||||
: icon = ImageRes.callVoice,
|
||||
text = StrRes.audioAndVideoCall,
|
||||
color = Styles.c_FFFFFF,
|
||||
textStyle = null,
|
||||
|
||||
@@ -4,6 +4,48 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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 {
|
||||
const ChatLocationView({
|
||||
Key? key,
|
||||
@@ -14,53 +56,62 @@ class ChatLocationView extends StatelessWidget {
|
||||
final String description;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final _decoder = const JsonDecoder();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
final map = _decoder.convert(description);
|
||||
String url = map['url'] ?? '';
|
||||
String name = map['name'] ?? '';
|
||||
String addr = map['addr'] ?? '';
|
||||
return Container(
|
||||
width: locationWidth,
|
||||
height: 130.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
border: Border.all(color: Styles.c_E8EAEF, width: 1),
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
4.verticalSpace,
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: name.toText
|
||||
..style = Styles.ts_0C1C33_14sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: addr.toText
|
||||
..style = Styles.ts_8E9AB0_12sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
2.verticalSpace,
|
||||
Expanded(
|
||||
child: ImageUtil.networkImage(
|
||||
url: url,
|
||||
width: locationWidth,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {}
|
||||
return Container();
|
||||
final data = LocationBubbleData.parse(
|
||||
description: description,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
fallbackTitle: StrRes.locationMessage,
|
||||
);
|
||||
return Container(
|
||||
width: locationWidth,
|
||||
height: 130.h,
|
||||
decoration: BoxDecoration(
|
||||
color: Styles.c_FFFFFF,
|
||||
border: Border.all(color: Styles.c_E8EAEF, width: 1),
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
4.verticalSpace,
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: data.title.toText
|
||||
..style = Styles.ts_0C1C33_14sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||
child: data.address.toText
|
||||
..style = Styles.ts_8E9AB0_12sp
|
||||
..maxLines = 1
|
||||
..overflow = TextOverflow.ellipsis,
|
||||
),
|
||||
2.verticalSpace,
|
||||
Expanded(
|
||||
child: data.thumbnailUrl == null
|
||||
? ColoredBox(
|
||||
color: Styles.c_F0F2F6,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.location_on_outlined,
|
||||
color: Styles.c_8E9AB0,
|
||||
size: 28.w,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ImageUtil.networkImage(
|
||||
url: data.thumbnailUrl!,
|
||||
width: locationWidth,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ class ChatToolBox extends StatelessWidget {
|
||||
this.onTapCamera,
|
||||
this.onTapCard,
|
||||
this.onTapFile,
|
||||
this.onTapLocation,
|
||||
this.onTapDirectionalMessage,
|
||||
});
|
||||
final Function()? onTapAlbum;
|
||||
@@ -18,7 +17,6 @@ class ChatToolBox extends StatelessWidget {
|
||||
final Function()? onTapCall;
|
||||
final Function()? onTapFile;
|
||||
final Function()? onTapCard;
|
||||
final Function()? onTapLocation;
|
||||
final VoidCallback? onTapDirectionalMessage;
|
||||
|
||||
@override
|
||||
@@ -50,11 +48,6 @@ class ChatToolBox extends StatelessWidget {
|
||||
icon: ImageRes.toolboxCard,
|
||||
onTap: onTapCard,
|
||||
),
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxLocation,
|
||||
icon: ImageRes.toolboxLocation,
|
||||
onTap: () => Permissions.location(onTapLocation),
|
||||
),
|
||||
if (onTapDirectionalMessage != null)
|
||||
ToolboxItemInfo(
|
||||
text: StrRes.toolboxDirectionalMessage,
|
||||
@@ -63,9 +56,11 @@ class ChatToolBox extends StatelessWidget {
|
||||
),
|
||||
];
|
||||
|
||||
final rowCount = (items.length / 4).ceil().clamp(1, 2);
|
||||
|
||||
return Container(
|
||||
color: Styles.c_F0F2F6,
|
||||
height: 224.h,
|
||||
height: rowCount <= 1 ? 118.h : 224.h,
|
||||
child: GridView.builder(
|
||||
itemCount: items.length,
|
||||
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: [
|
||||
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: '',
|
||||
),
|
||||
MarkerLayer(
|
||||
@@ -88,19 +89,27 @@ class MapView extends StatelessWidget {
|
||||
}
|
||||
|
||||
_openMapSheet() async {
|
||||
final availableMaps = await ml.MapLauncher.installedMaps;
|
||||
Get.bottomSheet(
|
||||
BottomSheetView(
|
||||
items: availableMaps
|
||||
.map((e) => SheetItem(
|
||||
label: _mapLabel(e),
|
||||
onTap: () async {
|
||||
_launcherMap(e);
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
try {
|
||||
final availableMaps = await ml.MapLauncher.installedMaps;
|
||||
if (availableMaps.isEmpty) {
|
||||
IMViews.showToast(StrRes.locationMessage);
|
||||
return;
|
||||
}
|
||||
Get.bottomSheet(
|
||||
BottomSheetView(
|
||||
items: availableMaps
|
||||
.map((e) => SheetItem(
|
||||
label: _mapLabel(e),
|
||||
onTap: () async {
|
||||
_launcherMap(e);
|
||||
},
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
IMViews.showToast(StrRes.locationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
String _mapLabel(ml.AvailableMap map) {
|
||||
@@ -119,11 +128,15 @@ class MapView extends StatelessWidget {
|
||||
}
|
||||
|
||||
_launcherMap(ml.AvailableMap map) async {
|
||||
await ml.MapLauncher.showMarker(
|
||||
mapType: map.mapType,
|
||||
coords: ml.Coords(latitude, longitude),
|
||||
title: address1,
|
||||
description: address2,
|
||||
);
|
||||
try {
|
||||
await ml.MapLauncher.showMarker(
|
||||
mapType: map.mapType,
|
||||
coords: ml.Coords(latitude, longitude),
|
||||
title: address1,
|
||||
description: address2,
|
||||
);
|
||||
} catch (_) {
|
||||
IMViews.showToast(StrRes.locationMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,9 @@ class RichTextInputBox extends StatefulWidget {
|
||||
this.showCameraIcon = true,
|
||||
this.showCardIcon = true,
|
||||
this.showFileIcon = true,
|
||||
this.showLocationIcon = true,
|
||||
this.onTapAlbum,
|
||||
this.onTapCard,
|
||||
this.onTapFile,
|
||||
this.onTapLocation,
|
||||
this.onSend,
|
||||
}) : super(key: key);
|
||||
final TextEditingController? controller;
|
||||
@@ -29,12 +27,10 @@ class RichTextInputBox extends StatefulWidget {
|
||||
final bool showCameraIcon;
|
||||
final bool showFileIcon;
|
||||
final bool showCardIcon;
|
||||
final bool showLocationIcon;
|
||||
final Function()? onTapAlbum;
|
||||
final Function()? onTapCamera;
|
||||
final Function()? onTapFile;
|
||||
final Function()? onTapCard;
|
||||
final Function()? onTapLocation;
|
||||
final Function()? onSend;
|
||||
|
||||
@override
|
||||
@@ -113,19 +109,12 @@ class _RichTextInputBoxState extends State<RichTextInputBox> {
|
||||
..height = 22.h
|
||||
..opacity = _opacity
|
||||
..onTap = widget.onTapCard,
|
||||
if (widget.showLocationIcon)
|
||||
ImageRes.toolboxLocation1.toImage
|
||||
..width = 16.w
|
||||
..height = 22.h
|
||||
..opacity = _opacity
|
||||
..onTap = widget.onTapLocation,
|
||||
],
|
||||
),
|
||||
if (widget.showAlbumIcon ||
|
||||
widget.showCameraIcon ||
|
||||
widget.showCardIcon ||
|
||||
widget.showFileIcon ||
|
||||
widget.showLocationIcon)
|
||||
widget.showFileIcon)
|
||||
15.verticalSpace,
|
||||
Row(
|
||||
children: [
|
||||
|
||||
@@ -72,12 +72,6 @@ class IMViews {
|
||||
alignment: MainAxisAlignment.start,
|
||||
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,
|
||||
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 [],
|
||||
int quality = 80}) {
|
||||
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;
|
||||
}
|
||||
@@ -134,23 +124,25 @@ class IMViews {
|
||||
SheetItem(
|
||||
label: StrRes.toolboxAlbum,
|
||||
onTap: () async {
|
||||
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
requestType: RequestType.image,
|
||||
maxAssets: 1,
|
||||
selectPredicate: (_, entity, isSelected) async {
|
||||
if (await allowSendImageType(entity)) {
|
||||
return true;
|
||||
}
|
||||
final List<AssetEntity>? assets =
|
||||
await AssetPicker.pickAssets(Get.context!,
|
||||
pickerConfig: AssetPickerConfig(
|
||||
requestType: RequestType.image,
|
||||
maxAssets: 1,
|
||||
selectPredicate: (_, entity, isSelected) async {
|
||||
if (await allowSendImageType(entity)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
IMViews.showToast(StrRes.supportsTypeHint);
|
||||
IMViews.showToast(StrRes.supportsTypeHint);
|
||||
|
||||
return false;
|
||||
}));
|
||||
return false;
|
||||
}));
|
||||
final file = await assets?.firstOrNull?.file;
|
||||
|
||||
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']);
|
||||
}
|
||||
},
|
||||
@@ -176,7 +168,8 @@ class IMViews {
|
||||
final file = await entity?.file;
|
||||
|
||||
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']);
|
||||
}
|
||||
},
|
||||
@@ -206,7 +199,9 @@ class IMViews {
|
||||
if (null != cropFile) {
|
||||
Logger.print('-----------crop path: ${cropFile.path}');
|
||||
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(
|
||||
id: putID,
|
||||
@@ -217,7 +212,8 @@ class IMViews {
|
||||
} else {
|
||||
Logger.print('-----------source path: $path');
|
||||
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(
|
||||
id: putID,
|
||||
|
||||
@@ -79,7 +79,6 @@ dependencies:
|
||||
flutter_map: ^6.0.1
|
||||
ffmpeg_kit_flutter_full_gpl: 6.0.3
|
||||
pull_to_refresh_new: ^2.0.5
|
||||
geolocator: ^12.0.0
|
||||
fixnum: ^1.1.0
|
||||
protobuf: ^3.0.0
|
||||
intl: ^0.19.0
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.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(
|
||||
(event) async {
|
||||
@@ -97,15 +97,13 @@ mixin OpenIMLive {
|
||||
_playSound(vibrate: true);
|
||||
final mediaType = event.data.invitation!.mediaType;
|
||||
final sessionType = event.data.invitation!.sessionType;
|
||||
final callType = mediaType == 'audio' ? CallType.audio : CallType.video;
|
||||
final callObj = sessionType == ConversationType.single ? CallObj.single : CallObj.group;
|
||||
final callType =
|
||||
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;
|
||||
OpenIMLiveClient().start(
|
||||
Get.overlayContext!,
|
||||
@@ -151,7 +149,8 @@ mixin OpenIMLive {
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.beAccepted) {
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.otherReject || event.state == CallState.otherAccepted) {
|
||||
} else if (event.state == CallState.otherReject ||
|
||||
event.state == CallState.otherAccepted) {
|
||||
_stopSound();
|
||||
} else if (event.state == CallState.timeout) {
|
||||
insertSignalingMessageSubject.add(event);
|
||||
@@ -257,15 +256,19 @@ mixin OpenIMLive {
|
||||
onRoomDisconnected(SignalingInfo signalingInfo) {}
|
||||
|
||||
Future<SignalingCertificate> onDialSingle(SignalingInfo signaling) async {
|
||||
final data = {'customType': CustomMessageType.callingInvite, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final data = {
|
||||
'customType': CustomMessageType.callingInvite,
|
||||
'data': signaling.invitation!.toJson()
|
||||
};
|
||||
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
|
||||
data: jsonEncode(data), extension: '', description: '');
|
||||
OpenIM.iMManager.messageManager.sendMessage(
|
||||
message: message,
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviteeUserIDList!.first,
|
||||
isOnlineOnly: true);
|
||||
final certificate = await getRtcCertificate(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
final certificate = await getRtcCertificate(
|
||||
signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
@@ -283,15 +286,19 @@ mixin OpenIMLive {
|
||||
_beCalledEvent = null; // ios bug
|
||||
_autoPickup = false;
|
||||
_stopSound();
|
||||
final data = {'customType': CustomMessageType.callingAccept, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final data = {
|
||||
'customType': CustomMessageType.callingAccept,
|
||||
'data': signaling.invitation!.toJson()
|
||||
};
|
||||
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
|
||||
data: jsonEncode(data), extension: '', description: '');
|
||||
OpenIM.iMManager.messageManager.sendMessage(
|
||||
message: message,
|
||||
offlinePushInfo: OfflinePushInfo(),
|
||||
userID: signaling.invitation!.inviterUserID,
|
||||
isOnlineOnly: true);
|
||||
final certificate = await getRtcCertificate(signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
final certificate = await getRtcCertificate(
|
||||
signaling.invitation!.roomID!, OpenIM.iMManager.userID);
|
||||
|
||||
return certificate;
|
||||
}
|
||||
@@ -300,35 +307,52 @@ mixin OpenIMLive {
|
||||
_stopSound();
|
||||
insertSignalingMessageSubject.add(CallEvent(CallState.reject, signaling));
|
||||
|
||||
final data = {'customType': CustomMessageType.callingReject, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID = 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);
|
||||
final data = {
|
||||
'customType': CustomMessageType.callingReject,
|
||||
'data': signaling.invitation!.toJson()
|
||||
};
|
||||
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
|
||||
data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID =
|
||||
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 {
|
||||
_stopSound();
|
||||
insertSignalingMessageSubject.add(CallEvent(CallState.cancel, signaling));
|
||||
|
||||
final data = {'customType': CustomMessageType.callingCancel, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID = 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);
|
||||
final data = {
|
||||
'customType': CustomMessageType.callingCancel,
|
||||
'data': signaling.invitation!.toJson()
|
||||
};
|
||||
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
|
||||
data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID =
|
||||
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;
|
||||
}
|
||||
|
||||
onTimeoutCancelled(SignalingInfo signaling) async {
|
||||
final data = {'customType': CustomMessageType.callingCancel, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final data = {
|
||||
'customType': CustomMessageType.callingCancel,
|
||||
'data': signaling.invitation!.toJson()
|
||||
};
|
||||
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
|
||||
data: jsonEncode(data), extension: '', description: '');
|
||||
|
||||
OpenIM.iMManager.messageManager.sendMessage(
|
||||
message: message,
|
||||
@@ -349,14 +373,21 @@ mixin OpenIMLive {
|
||||
|
||||
onTapHangup(SignalingInfo signaling, int duration, bool isPositive) async {
|
||||
if (isPositive) {
|
||||
final data = {'customType': CustomMessageType.callingHungup, 'data': signaling.invitation!.toJson()};
|
||||
final message = await OpenIM.iMManager.messageManager
|
||||
.createCustomMessage(data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID = 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);
|
||||
final data = {
|
||||
'customType': CustomMessageType.callingHungup,
|
||||
'data': signaling.invitation!.toJson()
|
||||
};
|
||||
final message = await OpenIM.iMManager.messageManager.createCustomMessage(
|
||||
data: jsonEncode(data), extension: '', description: '');
|
||||
final recvUserID =
|
||||
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();
|
||||
|
||||
@@ -389,7 +420,8 @@ mixin OpenIMLive {
|
||||
return list.firstOrNull;
|
||||
}
|
||||
|
||||
Future<List<GroupMembersInfo>> onSyncGroupMemberInfo(groupID, userIDList) async {
|
||||
Future<List<GroupMembersInfo>> onSyncGroupMemberInfo(
|
||||
groupID, userIDList) async {
|
||||
var list = await OpenIM.iMManager.groupManager.getGroupMembersInfo(
|
||||
groupID: groupID,
|
||||
userIDList: userIDList,
|
||||
@@ -412,7 +444,8 @@ mixin OpenIMLive {
|
||||
void _startIncomingVibrate() {
|
||||
_stopIncomingVibrate();
|
||||
_pulseVibrate();
|
||||
_incomingVibrateTimer = Timer.periodic(Styles.callVibrate, (_) => _pulseVibrate());
|
||||
_incomingVibrateTimer =
|
||||
Timer.periodic(Styles.callVibrate, (_) => _pulseVibrate());
|
||||
}
|
||||
|
||||
void _stopIncomingVibrate() {
|
||||
@@ -467,7 +500,8 @@ mixin OpenIMLive {
|
||||
receiverID = inviteeUserID;
|
||||
}
|
||||
|
||||
var msg = await OpenIM.iMManager.messageManager.insertSingleMessageToLocalStorage(
|
||||
var msg = await OpenIM.iMManager.messageManager
|
||||
.insertSingleMessageToLocalStorage(
|
||||
receiverID: inviteeUserID,
|
||||
senderID: inviterUserID,
|
||||
message: message
|
||||
@@ -495,7 +529,9 @@ class SignalingMessageEvent {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -930,54 +930,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user