Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
716e43cea3 | ||
|
|
d6fafe7f95 | ||
|
|
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,104 @@
|
|||||||
|
# B-272 Android 权限、通话文案与位置功能清理
|
||||||
|
|
||||||
|
分支:`agent/agent/d488336b`
|
||||||
|
施工基线:B-269 建议的最新已交付 Android HEAD `092039bc1a55c3ddf853ac39d04f6171e7cb5a4c`(`review-latest` / `origin/agent/agent/2b5c0459`)
|
||||||
|
方案依据:B-269 附件 `b269-architecture-plan.md` 的 P1-1 / P1-2
|
||||||
|
本卡只改 `mobile-next/` 与本验收记录,不改旧 `mobile/`、PC、现网服务,不发布 APK,不覆盖 Gitea Release。
|
||||||
|
|
||||||
|
**结论:Android release 构建通过。合并清单已去掉悬浮窗与定位权限;用户可见通话文案统一为「语音通话」;位置发送入口与定位依赖已移除,历史位置消息仍只读解析。真机登录、来电、历史位置点开未测,不得写成运行验收通过。**
|
||||||
|
|
||||||
|
## 权限前后对比(release APK 合并清单)
|
||||||
|
|
||||||
|
| 权限 / 组件 | 改前 | 改后 |
|
||||||
|
|---|---|---|
|
||||||
|
| `SYSTEM_ALERT_WINDOW` | 声明,同步完成后无条件申请 | `tools:node="remove"`,无申请调用 |
|
||||||
|
| 旧 `CallService`(`flutter_openim_live_alert`) | 在 app Manifest 声明 | 已删除声明;本地插件目录保留,未物理删除 |
|
||||||
|
| `ACCESS_COARSE/FINE/BACKGROUND_LOCATION`、`ACCESS_LOCATION_EXTRA_COMMANDS` | 声明,工具箱发位置会请求 | `tools:node="remove"`;去掉 `geolocator` |
|
||||||
|
| `FOREGROUND_SERVICE` / `ForegroundService` / `IsolateHolderService` | 保留 | 保留,未改通话中前台通知链路 |
|
||||||
|
| 定位发送页 `ChatWebViewMap` | 工具箱可进选点并 `createLocationMessage` | 已删除发送页与发送方法 |
|
||||||
|
|
||||||
|
合并 APK `aapt dump permissions` 已确认不含 `SYSTEM_ALERT_WINDOW`、`ACCESS_*LOCATION`。
|
||||||
|
|
||||||
|
## 文案
|
||||||
|
|
||||||
|
只改通话可见文本,未改协议字段、`CallType`、消息类型或 SDK 枚举。相册「视频」、视频消息、视频会议字样保留。
|
||||||
|
|
||||||
|
| 入口 | 改前 | 改后 |
|
||||||
|
|---|---|---|
|
||||||
|
| 工具箱 `toolboxCall` | 视频通话 | 语音通话 |
|
||||||
|
| 资料页 `audioAndVideoCall` | 音视频通话 | 语音通话,图标改用 `ImageRes.callVoice` |
|
||||||
|
| 来电/邀请/群通话提示中的「视频通话」 | 视频通话 | 语音通话 |
|
||||||
|
| 通话选择面板 | 语音 + 视频两项 | 仅语音一项 |
|
||||||
|
|
||||||
|
## 位置只读
|
||||||
|
|
||||||
|
- 聊天「更多」与备用输入条不再出现位置发送。
|
||||||
|
- 历史 `MessageType.location` 仍由 `ChatLocationView` 展示;点击走只读 `MapView`,不进入发送页。
|
||||||
|
- 描述 JSON 损坏或缺字段时显示「位置」占位,不抛异常、不渲染空白卡片。
|
||||||
|
|
||||||
|
## 提交主题(可逐项 `git revert`)
|
||||||
|
|
||||||
|
从新到旧回退:先本验收记录,再代码提交。
|
||||||
|
|
||||||
|
| 主题 | SHA | 文件 |
|
||||||
|
|---|---|---|
|
||||||
|
| 本验收记录 | 本文件当次提交 | `docs/acceptance/b272-android-permission-copy-location.md` |
|
||||||
|
| 权限/文案/位置清理 | 见代码提交 | `mobile-next/` 见 git log |
|
||||||
|
|
||||||
|
回退示例(新到旧):
|
||||||
|
|
||||||
|
```
|
||||||
|
git revert <本验收提交>
|
||||||
|
git revert <代码提交>
|
||||||
|
```
|
||||||
|
|
||||||
|
回退不影响 `mobile/`、`account-service/`、`config/`、`scripts/`、现网服务、PC、已发布安装包。旧悬浮窗插件目录 `mobile-next/local_plugin/flutter_openim_live_alert` 仍在仓库,仅不再接入。
|
||||||
|
|
||||||
|
## 环境
|
||||||
|
|
||||||
|
- Flutter 3.24.5 / Dart 3.5.4(`C:\flutter324`)
|
||||||
|
- Temurin JDK 17.0.20+8(`C:\devtools\jdk17`)
|
||||||
|
- Android SDK:`C:\devtools\android-sdk`(build-tools 34.0.0,compileSdk 34)
|
||||||
|
- `org.gradle.jvmargs=-Xmx4096M`
|
||||||
|
|
||||||
|
## 检查与构建
|
||||||
|
|
||||||
|
```
|
||||||
|
cd mobile-next
|
||||||
|
flutter test test/company/p1_cleanup_test.dart test/company/call_session_guard_test.dart test/company/rtc_token_mapper_test.dart test/company/call_visual_rework_test.dart test/company/user_full_info_mapper_test.dart
|
||||||
|
flutter analyze lib/core/controller/im_controller.dart lib/pages/chat/chat_logic.dart lib/pages/chat/chat_view.dart lib/company openim_common/lib/src/utils/permissions.dart openim_common/lib/src/widgets/chat/chat_toolbox.dart openim_common/lib/src/widgets/chat/chat_location_view.dart openim_common/lib/src/widgets/rich_text_input_box.dart openim_common/lib/src/widgets/button.dart openim_common/lib/src/widgets/views.dart openim_live/lib/src/live_controller.dart
|
||||||
|
flutter build apk --release
|
||||||
|
```
|
||||||
|
|
||||||
|
- 公司相关单测 **21 项通过**(含本卡 2 项:文案不含「视频通话/音视频通话」、位置描述损坏可解析)。
|
||||||
|
- 定点 `flutter analyze`:**无 error**。info/warning 为存量(Token const、废弃 API、Kotlin metadata 1.8 vs 1.6 等同 B-225/B-261)。
|
||||||
|
- Android release 构建成功。上游告警同前:Flutter Gradle apply-script 弃用;部分依赖 Kotlin metadata 1.8.0 vs 工程预期 1.6.0。
|
||||||
|
|
||||||
|
| 产物 | 版本 | 包名 | 大小 | SHA256 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Android release APK | 3.8.3+180 | `io.openim.flutter.demo` | 53,095,552 B | `A37BA10EB94D43F0B113130CE90EEE554FD9F3EC00C7C7A9D025CA1059C0E76C` |
|
||||||
|
|
||||||
|
路径:`mobile-next/build/app/outputs/flutter-apk/app-release.apk`(APK 内嵌时间戳,哈希不可位级复现)。label `F-DEMO`,仅 `arm64-v8a`,minSdk 24 / targetSdk 33 / compileSdk 34。未发布、未覆盖 Gitea 现有 Release。
|
||||||
|
|
||||||
|
## 已测 / 未测
|
||||||
|
|
||||||
|
已测:
|
||||||
|
|
||||||
|
- 施工基线为 `092039b`,不是 `origin/main` 的旧 `fa8584a`
|
||||||
|
- 中文通话文案静态扫描不再含「视频通话」「音视频通话」;「视频」消息字样仍在
|
||||||
|
- 位置描述合法 JSON / 损坏 JSON / 空值解析不抛异常
|
||||||
|
- 通话状态机、换票、视觉 Token、我的信息映射既有单测回归
|
||||||
|
- release APK 合并清单无悬浮窗、无定位权限;无旧 `CallService`;前台通知服务仍在
|
||||||
|
- Android release 构建通过
|
||||||
|
|
||||||
|
未测(不得写成已通过):
|
||||||
|
|
||||||
|
- 公司内网真机冷启动、登录、同步完成后是否还弹出悬浮窗设置
|
||||||
|
- 真机系统权限页是否仍列出「显示在其他应用上层」或定位
|
||||||
|
- 语音来电、接听、通话中最小化、后台返回
|
||||||
|
- 历史位置消息在真机会话中的展示与点击
|
||||||
|
- 文字/图片/文件/语音消息与相册视频发送的真机回归
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
未改:旧 `mobile/`、PC、`account-service/`、docker、端口、账号、数据库、LiveKit 配置、DNS/证书/推送、品牌换标、APK 发布。
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" tools:node="remove" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
@@ -26,14 +26,14 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" tools:node="remove" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" tools:node="remove" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
|
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" tools:node="remove" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" tools:node="remove" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||||
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
|
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
|
||||||
@@ -130,10 +130,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:stopWithTask="false" />
|
android:stopWithTask="false" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="io.openim.live.alert.flutter_openim_live_alert.services.CallService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileProvider"
|
android:authorities="${applicationId}.fileProvider"
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:openim_common/openim_common.dart';
|
import 'package:openim_common/openim_common.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:openim_live/openim_live.dart';
|
import 'package:openim_live/openim_live.dart';
|
||||||
|
|
||||||
import '../../company/feature_flags.dart';
|
import '../../company/feature_flags.dart';
|
||||||
@@ -236,9 +234,6 @@ class IMController extends GetxController with IMCallback, OpenIMLive {
|
|||||||
},
|
},
|
||||||
onSyncServerFinish: (reInstall) {
|
onSyncServerFinish: (reInstall) {
|
||||||
imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false);
|
imSdkStatus(IMSdkStatus.syncEnded, reInstall: reInstall ?? false);
|
||||||
if (Platform.isAndroid) {
|
|
||||||
Permissions.request([Permission.systemAlertWindow]);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSyncServerStart: (reInstall) {
|
onSyncServerStart: (reInstall) {
|
||||||
imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false);
|
imSdkStatus(IMSdkStatus.syncStart, reInstall: reInstall ?? false);
|
||||||
|
|||||||
@@ -446,17 +446,6 @@ class ChatLogic extends SuperController {
|
|||||||
_sendMessage(message);
|
_sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendLocation({
|
|
||||||
required dynamic location,
|
|
||||||
}) async {
|
|
||||||
var message = await OpenIM.iMManager.messageManager.createLocationMessage(
|
|
||||||
latitude: location['latitude'],
|
|
||||||
longitude: location['longitude'],
|
|
||||||
description: location['description'],
|
|
||||||
);
|
|
||||||
_sendMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendForwardRemarkMsg(
|
sendForwardRemarkMsg(
|
||||||
String content, {
|
String content, {
|
||||||
String? userId,
|
String? userId,
|
||||||
@@ -714,18 +703,6 @@ class ChatLogic extends SuperController {
|
|||||||
forceCloseToolbox.addSafely(true);
|
forceCloseToolbox.addSafely(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTapLocation() async {
|
|
||||||
var location = await Get.to(
|
|
||||||
const ChatWebViewMap(host: Config.locationHost, webKey: Config.webKey, webServerKey: Config.webServerKey),
|
|
||||||
transition: Transition.cupertino,
|
|
||||||
popGesture: true,
|
|
||||||
);
|
|
||||||
if (null != location) {
|
|
||||||
Logger.print(location);
|
|
||||||
sendLocation(location: location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTapAlbum() async {
|
void onTapAlbum() async {
|
||||||
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!,
|
final List<AssetEntity>? assets = await AssetPicker.pickAssets(Get.context!,
|
||||||
pickerConfig: AssetPickerConfig(
|
pickerConfig: AssetPickerConfig(
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ class ChatPage extends StatelessWidget {
|
|||||||
onTapCall: FeatureFlags.livekitCall && logic.isSingleChat ? logic.call : null,
|
onTapCall: FeatureFlags.livekitCall && logic.isSingleChat ? logic.call : null,
|
||||||
onTapCard: logic.onTapCarte,
|
onTapCard: logic.onTapCarte,
|
||||||
onTapFile: logic.onTapFile,
|
onTapFile: logic.onTapFile,
|
||||||
onTapLocation: logic.onTapLocation,
|
|
||||||
),
|
),
|
||||||
voiceRecordBar: bar,
|
voiceRecordBar: bar,
|
||||||
emojiView: ChatEmojiView(
|
emojiView: ChatEmojiView(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter_datetime_picker_plus/flutter_datetime_picker_plus.dart';
|
|
||||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:openim/pages/login/login_logic.dart';
|
import 'package:openim/pages/login/login_logic.dart';
|
||||||
@@ -26,8 +25,6 @@ class MyInfoLogic extends GetxController {
|
|||||||
attr: EditAttr.mobile,
|
attr: EditAttr.mobile,
|
||||||
);
|
);
|
||||||
|
|
||||||
void editEmail() => AppNavigator.startEditMyInfo(attr: EditAttr.email, maxLength: 30);
|
|
||||||
|
|
||||||
void openPhotoSheet() {
|
void openPhotoSheet() {
|
||||||
IMViews.openPhotoSheet(
|
IMViews.openPhotoSheet(
|
||||||
onData: (path, url) async {
|
onData: (path, url) async {
|
||||||
@@ -43,62 +40,6 @@ class MyInfoLogic extends GetxController {
|
|||||||
quality: 15);
|
quality: 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openDatePicker() {
|
|
||||||
var appLocale = Get.locale;
|
|
||||||
var isZh = appLocale!.languageCode.toLowerCase().contains("zh");
|
|
||||||
DatePicker.showDatePicker(
|
|
||||||
Get.context!,
|
|
||||||
locale: isZh ? LocaleType.zh : LocaleType.en,
|
|
||||||
maxTime: DateTime.now(),
|
|
||||||
currentTime: DateTime.fromMillisecondsSinceEpoch(imLogic.userInfo.value.birth ?? 0),
|
|
||||||
theme: DatePickerTheme(
|
|
||||||
cancelStyle: Styles.ts_0C1C33_17sp,
|
|
||||||
doneStyle: Styles.ts_0089FF_17sp,
|
|
||||||
itemStyle: Styles.ts_0C1C33_17sp,
|
|
||||||
),
|
|
||||||
onConfirm: (dateTime) {
|
|
||||||
_updateBirthday(dateTime.millisecondsSinceEpoch ~/ 1000);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectGender() {
|
|
||||||
Get.bottomSheet(
|
|
||||||
BottomSheetView(
|
|
||||||
items: [
|
|
||||||
SheetItem(
|
|
||||||
label: StrRes.man,
|
|
||||||
onTap: () => _updateGender(1),
|
|
||||||
),
|
|
||||||
SheetItem(
|
|
||||||
label: StrRes.woman,
|
|
||||||
onTap: () => _updateGender(2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateGender(int gender) {
|
|
||||||
LoadingView.singleton.wrap(
|
|
||||||
asyncFunction: () => Apis.updateUserInfo(userID: OpenIM.iMManager.userID, gender: gender)
|
|
||||||
.then((value) => imLogic.userInfo.update((val) {
|
|
||||||
val?.gender = gender;
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateBirthday(int birthday) {
|
|
||||||
LoadingView.singleton.wrap(
|
|
||||||
asyncFunction: () => Apis.updateUserInfo(
|
|
||||||
userID: OpenIM.iMManager.userID,
|
|
||||||
birth: birthday * 1000,
|
|
||||||
).then((value) => imLogic.userInfo.update((val) {
|
|
||||||
val?.birth = birthday * 1000;
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
_queryMyFullIno();
|
_queryMyFullIno();
|
||||||
|
|||||||
@@ -42,16 +42,18 @@ class MyInfoPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
_buildItemView(
|
_buildItemView(
|
||||||
label: StrRes.gender,
|
label: StrRes.gender,
|
||||||
value: imLogic.userInfo.value.isMale ? StrRes.man : StrRes.woman,
|
value: _genderText(imLogic.userInfo.value.gender),
|
||||||
onTap: logic.selectGender,
|
showRightArrow: false,
|
||||||
),
|
),
|
||||||
_buildItemView(
|
_buildItemView(
|
||||||
label: StrRes.birthDay,
|
label: StrRes.birthDay,
|
||||||
value: DateUtil.formatDateMs(
|
value: imLogic.userInfo.value.birth == null
|
||||||
imLogic.userInfo.value.birth ?? 0,
|
? ''
|
||||||
|
: DateUtil.formatDateMs(
|
||||||
|
imLogic.userInfo.value.birth!,
|
||||||
format: IMUtils.getTimeFormat1(),
|
format: IMUtils.getTimeFormat1(),
|
||||||
),
|
),
|
||||||
onTap: logic.openDatePicker,
|
showRightArrow: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -68,7 +70,7 @@ class MyInfoPage extends StatelessWidget {
|
|||||||
_buildItemView(
|
_buildItemView(
|
||||||
label: StrRes.email,
|
label: StrRes.email,
|
||||||
value: imLogic.userInfo.value.email,
|
value: imLogic.userInfo.value.email,
|
||||||
onTap: logic.editEmail,
|
showRightArrow: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -93,6 +95,12 @@ class MyInfoPage extends StatelessWidget {
|
|||||||
child: Column(children: children),
|
child: Column(children: children),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
String _genderText(int? gender) {
|
||||||
|
if (gender == 1) return StrRes.man;
|
||||||
|
if (gender == 2) return StrRes.woman;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildItemView({
|
Widget _buildItemView({
|
||||||
required String label,
|
required String label,
|
||||||
String? value,
|
String? value,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export 'src/utils/sp_util.dart';
|
|||||||
export 'src/utils/utils.dart';
|
export 'src/utils/utils.dart';
|
||||||
export 'src/utils/voice_record.dart';
|
export 'src/utils/voice_record.dart';
|
||||||
export 'src/utils/api_service.dart';
|
export 'src/utils/api_service.dart';
|
||||||
|
export 'src/utils/user_full_info_mapper.dart';
|
||||||
export 'src/widgets/avatar_view.dart';
|
export 'src/widgets/avatar_view.dart';
|
||||||
export 'src/widgets/azlist_view.dart';
|
export 'src/widgets/azlist_view.dart';
|
||||||
export 'src/widgets/bottom_bar.dart';
|
export 'src/widgets/bottom_bar.dart';
|
||||||
@@ -75,7 +76,6 @@ export 'src/widgets/chat/chat_voice_record_bar.dart';
|
|||||||
export 'src/widgets/chat/chat_voice_record_layout.dart';
|
export 'src/widgets/chat/chat_voice_record_layout.dart';
|
||||||
export 'src/widgets/chat/chat_voice_record_view.dart';
|
export 'src/widgets/chat/chat_voice_record_view.dart';
|
||||||
export 'src/widgets/chat/chat_voice_view.dart';
|
export 'src/widgets/chat/chat_voice_view.dart';
|
||||||
export 'src/widgets/chat/chat_webview_map.dart';
|
|
||||||
export 'src/widgets/chat/new_message_indicator.dart';
|
export 'src/widgets/chat/new_message_indicator.dart';
|
||||||
export 'src/widgets/chat/water_mark_view.dart';
|
export 'src/widgets/chat/water_mark_view.dart';
|
||||||
export 'src/widgets/custom_pop_up_menu.dart';
|
export 'src/widgets/custom_pop_up_menu.dart';
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
import 'package:flutter_openim_sdk/flutter_openim_sdk.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -11,14 +10,19 @@ import 'package:sprintf/sprintf.dart';
|
|||||||
import 'utils/api_service.dart';
|
import 'utils/api_service.dart';
|
||||||
|
|
||||||
class Apis {
|
class Apis {
|
||||||
static Options get imTokenOptions => Options(headers: {'token': DataSp.imToken});
|
static Options get imTokenOptions =>
|
||||||
|
Options(headers: {'token': DataSp.imToken});
|
||||||
|
|
||||||
static Options get chatTokenOptions => Options(headers: {'token': DataSp.chatToken});
|
static Options get chatTokenOptions =>
|
||||||
|
Options(headers: {'token': DataSp.chatToken});
|
||||||
|
|
||||||
static StreamController kickoffController = StreamController<int>.broadcast();
|
static StreamController kickoffController = StreamController<int>.broadcast();
|
||||||
|
|
||||||
static void _kickoff(int? errCode) {
|
static void _kickoff(int? errCode) {
|
||||||
if (errCode == 1501 || errCode == 1503 || errCode == 1504 || errCode == 1505) {
|
if (errCode == 1501 ||
|
||||||
|
errCode == 1503 ||
|
||||||
|
errCode == 1504 ||
|
||||||
|
errCode == 1505) {
|
||||||
kickoffController.sink.add(errCode);
|
kickoffController.sink.add(errCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,43 +201,16 @@ class Apis {
|
|||||||
int? allowBeep,
|
int? allowBeep,
|
||||||
int? allowVibration,
|
int? allowVibration,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
// 原 chat `/user/update` 已随 :10008 下线。OpenIM 只同步昵称/头像;
|
||||||
Map<String, dynamic> param = {'userID': userID};
|
// 邮箱、性别、生日等业务字段由调用方更新本地状态,不打账号服务。
|
||||||
void put(String key, dynamic value) {
|
if (userID.isEmpty) return {};
|
||||||
if (null != value) {
|
if (nickname != null || faceURL != null) {
|
||||||
param[key] = value;
|
await OpenIM.iMManager.userManager.setSelfInfo(
|
||||||
}
|
nickname: nickname,
|
||||||
}
|
faceURL: faceURL,
|
||||||
|
|
||||||
put('account', account);
|
|
||||||
put('phoneNumber', phoneNumber);
|
|
||||||
put('areaCode', areaCode);
|
|
||||||
put('email', email);
|
|
||||||
put('nickname', nickname);
|
|
||||||
put('faceURL', faceURL);
|
|
||||||
put('gender', gender);
|
|
||||||
put('gender', gender);
|
|
||||||
put('level', level);
|
|
||||||
put('birth', birth);
|
|
||||||
put('allowAddFriend', allowAddFriend);
|
|
||||||
put('allowBeep', allowBeep);
|
|
||||||
put('allowVibration', allowVibration);
|
|
||||||
|
|
||||||
return HttpUtil.post(
|
|
||||||
Urls.updateUserInfo,
|
|
||||||
data: {
|
|
||||||
...param,
|
|
||||||
'platform': IMUtils.getPlatform(),
|
|
||||||
},
|
|
||||||
options: chatTokenOptions,
|
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
|
||||||
final t = e as (int, String?);
|
|
||||||
final errCode = t.$1;
|
|
||||||
final errMsg = t.$2;
|
|
||||||
_kickoff(errCode);
|
|
||||||
Logger.print('e:$errCode s:$errMsg');
|
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<FriendInfo>> searchFriendInfo(
|
static Future<List<FriendInfo>> searchFriendInfo(
|
||||||
@@ -251,7 +228,9 @@ class Apis {
|
|||||||
options: chatTokenOptions,
|
options: chatTokenOptions,
|
||||||
);
|
);
|
||||||
if (data['users'] is List) {
|
if (data['users'] is List) {
|
||||||
return (data['users'] as List).map((e) => FriendInfo.fromJson(e)).toList();
|
return (data['users'] as List)
|
||||||
|
.map((e) => FriendInfo.fromJson(e))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -269,26 +248,16 @@ class Apis {
|
|||||||
int showNumber = 10,
|
int showNumber = 10,
|
||||||
required List<String> userIDList,
|
required List<String> userIDList,
|
||||||
}) async {
|
}) async {
|
||||||
|
if (userIDList.isEmpty) return [];
|
||||||
try {
|
try {
|
||||||
final data = await HttpUtil.post(
|
final users = await OpenIM.iMManager.userManager.getUsersInfo(
|
||||||
Urls.getUsersFullInfo,
|
userIDList: userIDList,
|
||||||
data: {
|
|
||||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
|
||||||
'userIDs': userIDList,
|
|
||||||
'platform': IMUtils.getPlatform(),
|
|
||||||
},
|
|
||||||
options: chatTokenOptions,
|
|
||||||
);
|
);
|
||||||
if (data['users'] is List) {
|
return users
|
||||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
.map((e) => UserFullInfoMapper.fromImJson(e.toJson()))
|
||||||
}
|
.toList();
|
||||||
return null;
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
final t = e as (int, String?);
|
Logger.print('getUserFullInfo e:$e s:$s');
|
||||||
final errCode = t.$1;
|
|
||||||
final errMsg = t.$2;
|
|
||||||
_kickoff(errCode);
|
|
||||||
Logger.print('e:$errCode s:$errMsg');
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,34 +267,20 @@ class Apis {
|
|||||||
int pageNumber = 1,
|
int pageNumber = 1,
|
||||||
int showNumber = 10,
|
int showNumber = 10,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
final keyword = content.trim();
|
||||||
final data = await HttpUtil.post(
|
if (keyword.isEmpty) return [];
|
||||||
Urls.searchUserFullInfo,
|
if (pageNumber > 1) return [];
|
||||||
data: {
|
return getUserFullInfo(userIDList: [keyword], showNumber: showNumber);
|
||||||
'pagination': {'pageNumber': pageNumber, 'showNumber': showNumber},
|
|
||||||
'keyword': content,
|
|
||||||
},
|
|
||||||
options: chatTokenOptions,
|
|
||||||
);
|
|
||||||
if (data['users'] is List) {
|
|
||||||
return (data['users'] as List).map((e) => UserFullInfo.fromJson(e)).toList();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e, s) {
|
|
||||||
final t = e as (int, String?);
|
|
||||||
final errCode = t.$1;
|
|
||||||
final errMsg = t.$2;
|
|
||||||
_kickoff(errCode);
|
|
||||||
Logger.print('e:$errCode s:$errMsg');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<UserFullInfo?> queryMyFullInfo() async {
|
static Future<UserFullInfo?> queryMyFullInfo() async {
|
||||||
final list = await Apis.getUserFullInfo(
|
try {
|
||||||
userIDList: [OpenIM.iMManager.userID],
|
final user = await OpenIM.iMManager.userManager.getSelfUserInfo();
|
||||||
);
|
return UserFullInfoMapper.fromImJson(user.toJson());
|
||||||
return list?.firstOrNull;
|
} catch (e, s) {
|
||||||
|
Logger.print('queryMyFullInfo e:$e s:$s');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> requestVerificationCode({
|
static Future<bool> requestVerificationCode({
|
||||||
@@ -353,7 +308,8 @@ class Apis {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<SignalingCertificate> getTokenForRTC(String roomID, String userID) async {
|
static Future<SignalingCertificate> getTokenForRTC(
|
||||||
|
String roomID, String userID) async {
|
||||||
return HttpUtil.post(
|
return HttpUtil.post(
|
||||||
Urls.getTokenForRTC,
|
Urls.getTokenForRTC,
|
||||||
data: {
|
data: {
|
||||||
@@ -410,7 +366,10 @@ class Apis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> getClientConfig() async {
|
static Future<Map<String, dynamic>> getClientConfig() async {
|
||||||
return {'discoverPageURL': Config.discoverPageURL, 'allowSendMsgNotFriend': Config.allowSendMsgNotFriend};
|
return {
|
||||||
|
'discoverPageURL': Config.discoverPageURL,
|
||||||
|
'allowSendMsgNotFriend': Config.allowSendMsgNotFriend
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _catchError(Object e, StackTrace s, {bool forceBack = true}) {
|
static void _catchError(Object e, StackTrace s, {bool forceBack = true}) {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const Map<String, String> en_US = {
|
|||||||
'rejectCall': 'Reject',
|
'rejectCall': 'Reject',
|
||||||
'acceptCall': 'Accept',
|
'acceptCall': 'Accept',
|
||||||
"callVoice": "Voice Call",
|
"callVoice": "Voice Call",
|
||||||
"callVideo": "Video Call",
|
"callVideo": "Voice Call",
|
||||||
"sentSuccessfully": "Sent Successfully",
|
"sentSuccessfully": "Sent Successfully",
|
||||||
"copySuccessfully": "Copied Successfully",
|
"copySuccessfully": "Copied Successfully",
|
||||||
"day": "day",
|
"day": "day",
|
||||||
@@ -120,7 +120,7 @@ const Map<String, String> en_US = {
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"determine": "OK",
|
"determine": "OK",
|
||||||
"toolboxAlbum": "Album",
|
"toolboxAlbum": "Album",
|
||||||
"toolboxCall": "Video Call",
|
"toolboxCall": "Voice Call",
|
||||||
"toolboxCamera": "Camera",
|
"toolboxCamera": "Camera",
|
||||||
"toolboxCard": "Card",
|
"toolboxCard": "Card",
|
||||||
"toolboxFile": "File",
|
"toolboxFile": "File",
|
||||||
@@ -156,7 +156,7 @@ const Map<String, String> en_US = {
|
|||||||
'nMessage': '%s New Messages',
|
'nMessage': '%s New Messages',
|
||||||
'plsSelectLocation': 'Please select a location',
|
'plsSelectLocation': 'Please select a location',
|
||||||
'groupAudioCallHint': '%s people in a voice call',
|
'groupAudioCallHint': '%s people in a voice call',
|
||||||
'groupVideoCallHint': '%s people in a video call',
|
'groupVideoCallHint': '%s people in a voice call',
|
||||||
'reEdit': 'Re-edit',
|
'reEdit': 'Re-edit',
|
||||||
'playSpeed': 'Playback Speed',
|
'playSpeed': 'Playback Speed',
|
||||||
'download': 'Download',
|
'download': 'Download',
|
||||||
@@ -224,7 +224,7 @@ const Map<String, String> en_US = {
|
|||||||
'position': 'Position',
|
'position': 'Position',
|
||||||
'personalInfo': 'Personal Info',
|
'personalInfo': 'Personal Info',
|
||||||
'viewDynamics': 'View Dynamics',
|
'viewDynamics': 'View Dynamics',
|
||||||
'audioAndVideoCall': 'Call',
|
'audioAndVideoCall': 'Voice Call',
|
||||||
'sendMessage': 'Message',
|
'sendMessage': 'Message',
|
||||||
'avatar': 'Avatar',
|
'avatar': 'Avatar',
|
||||||
'name': 'Name',
|
'name': 'Name',
|
||||||
@@ -436,7 +436,7 @@ const Map<String, String> en_US = {
|
|||||||
"waitingVoiceCallHint": "Waiting for the other party to accept the invitation...",
|
"waitingVoiceCallHint": "Waiting for the other party to accept the invitation...",
|
||||||
"invitedVoiceCallHint": "Inviting you to a voice call",
|
"invitedVoiceCallHint": "Inviting you to a voice call",
|
||||||
"waitingVideoCallHint": "Waiting for the other party to accept the invitation",
|
"waitingVideoCallHint": "Waiting for the other party to accept the invitation",
|
||||||
"invitedVideoCallHint": "Inviting you for a video call...",
|
"invitedVideoCallHint": "Inviting you to a voice call",
|
||||||
"waitingToAnswer": "Waiting to answer",
|
"waitingToAnswer": "Waiting to answer",
|
||||||
"invitedYouToCall": "Invited you to a call",
|
"invitedYouToCall": "Invited you to a call",
|
||||||
"calling": "Calling...",
|
"calling": "Calling...",
|
||||||
@@ -444,7 +444,7 @@ const Map<String, String> en_US = {
|
|||||||
'busyVideoCallHint': 'The other party is busy and cannot receive the invitation',
|
'busyVideoCallHint': 'The other party is busy and cannot receive the invitation',
|
||||||
'inviterBusyVideoCallHint': 'You are in another call and cannot send an invitation',
|
'inviterBusyVideoCallHint': 'You are in another call and cannot send an invitation',
|
||||||
"whoInvitedVoiceCallHint": "%s invited you for a voice call",
|
"whoInvitedVoiceCallHint": "%s invited you for a voice call",
|
||||||
"whoInvitedVideoCallHint": "%s invited you for a video call",
|
"whoInvitedVideoCallHint": "%s invited you for a voice call",
|
||||||
'plsInputMeetingSubject': 'Please enter meeting subject',
|
'plsInputMeetingSubject': 'Please enter meeting subject',
|
||||||
'meetingStartTime': 'Start Time',
|
'meetingStartTime': 'Start Time',
|
||||||
'meetingDuration': 'Meeting Duration',
|
'meetingDuration': 'Meeting Duration',
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const Map<String, String> zh_CN = {
|
|||||||
'rejectCall': '拒绝',
|
'rejectCall': '拒绝',
|
||||||
'acceptCall': '同意',
|
'acceptCall': '同意',
|
||||||
"callVoice": "语音通话",
|
"callVoice": "语音通话",
|
||||||
"callVideo": "视频通话",
|
"callVideo": "语音通话",
|
||||||
"sentSuccessfully": "发送成功",
|
"sentSuccessfully": "发送成功",
|
||||||
"copySuccessfully": "复制成功",
|
"copySuccessfully": "复制成功",
|
||||||
"day": "天",
|
"day": "天",
|
||||||
@@ -120,7 +120,7 @@ const Map<String, String> zh_CN = {
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"determine": "确定",
|
"determine": "确定",
|
||||||
"toolboxAlbum": "相册",
|
"toolboxAlbum": "相册",
|
||||||
"toolboxCall": "视频通话",
|
"toolboxCall": "语音通话",
|
||||||
"toolboxCamera": "拍摄",
|
"toolboxCamera": "拍摄",
|
||||||
"toolboxCard": "名片",
|
"toolboxCard": "名片",
|
||||||
"toolboxFile": "文件",
|
"toolboxFile": "文件",
|
||||||
@@ -156,7 +156,7 @@ const Map<String, String> zh_CN = {
|
|||||||
'nMessage': '%s条新消息',
|
'nMessage': '%s条新消息',
|
||||||
'plsSelectLocation': '请选择一个位置',
|
'plsSelectLocation': '请选择一个位置',
|
||||||
'groupAudioCallHint': '%s人正在语音通话',
|
'groupAudioCallHint': '%s人正在语音通话',
|
||||||
'groupVideoCallHint': '%s人正在视频通话',
|
'groupVideoCallHint': '%s人正在语音通话',
|
||||||
'reEdit': '重新编辑',
|
'reEdit': '重新编辑',
|
||||||
'playSpeed': '播放速度',
|
'playSpeed': '播放速度',
|
||||||
'download': '下载',
|
'download': '下载',
|
||||||
@@ -224,7 +224,7 @@ const Map<String, String> zh_CN = {
|
|||||||
'position': '职位',
|
'position': '职位',
|
||||||
'personalInfo': '个人资料',
|
'personalInfo': '个人资料',
|
||||||
'viewDynamics': '查看动态',
|
'viewDynamics': '查看动态',
|
||||||
'audioAndVideoCall': '音视频通话',
|
'audioAndVideoCall': '语音通话',
|
||||||
'sendMessage': '发消息',
|
'sendMessage': '发消息',
|
||||||
'avatar': '头像',
|
'avatar': '头像',
|
||||||
'name': '姓名',
|
'name': '姓名',
|
||||||
@@ -438,7 +438,7 @@ const Map<String, String> zh_CN = {
|
|||||||
"waitingVoiceCallHint": "正在等待对方接受邀请…",
|
"waitingVoiceCallHint": "正在等待对方接受邀请…",
|
||||||
"invitedVoiceCallHint": "邀请你语音通话",
|
"invitedVoiceCallHint": "邀请你语音通话",
|
||||||
"waitingVideoCallHint": "正在等待对方接受邀请",
|
"waitingVideoCallHint": "正在等待对方接受邀请",
|
||||||
"invitedVideoCallHint": "邀请你进行视频通话…",
|
"invitedVideoCallHint": "邀请你语音通话",
|
||||||
"waitingToAnswer": "等待接听",
|
"waitingToAnswer": "等待接听",
|
||||||
"invitedYouToCall": "邀请你通话",
|
"invitedYouToCall": "邀请你通话",
|
||||||
"calling": "通话中",
|
"calling": "通话中",
|
||||||
@@ -446,7 +446,7 @@ const Map<String, String> zh_CN = {
|
|||||||
'busyVideoCallHint': '对方正忙,无法接收邀请',
|
'busyVideoCallHint': '对方正忙,无法接收邀请',
|
||||||
'inviterBusyVideoCallHint': '你在其它通话中,无法发起邀请',
|
'inviterBusyVideoCallHint': '你在其它通话中,无法发起邀请',
|
||||||
"whoInvitedVoiceCallHint": "%s邀请你进行语音通话",
|
"whoInvitedVoiceCallHint": "%s邀请你进行语音通话",
|
||||||
"whoInvitedVideoCallHint": "%s邀请你进行视频通话",
|
"whoInvitedVideoCallHint": "%s邀请你进行语音通话",
|
||||||
'plsInputMeetingSubject': '请输入会议主题',
|
'plsInputMeetingSubject': '请输入会议主题',
|
||||||
'meetingStartTime': '开始时间',
|
'meetingStartTime': '开始时间',
|
||||||
'meetingDuration': '会议时长',
|
'meetingDuration': '会议时长',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Urls {
|
|||||||
"${Config.imApiUrl}/manager/get_users_online_status";
|
"${Config.imApiUrl}/manager/get_users_online_status";
|
||||||
static String get queryAllUsers =>
|
static String get queryAllUsers =>
|
||||||
"${Config.imApiUrl}/manager/get_all_users_uid";
|
"${Config.imApiUrl}/manager/get_all_users_uid";
|
||||||
|
// 以下 chat 路径仅作历史对照。账号服务 :10010 没有这些路由;
|
||||||
|
// 用户资料读写已改走 OpenIM SDK,见 Apis.getUserFullInfo / updateUserInfo。
|
||||||
static String get updateUserInfo => "${Config.appAuthUrl}/user/update";
|
static String get updateUserInfo => "${Config.appAuthUrl}/user/update";
|
||||||
static String get searchFriendInfo => "${Config.appAuthUrl}/friend/search";
|
static String get searchFriendInfo => "${Config.appAuthUrl}/friend/search";
|
||||||
static String get getUsersFullInfo => "${Config.appAuthUrl}/user/find/full";
|
static String get getUsersFullInfo => "${Config.appAuthUrl}/user/find/full";
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import 'package:sprintf/sprintf.dart';
|
|||||||
class Permissions {
|
class Permissions {
|
||||||
Permissions._();
|
Permissions._();
|
||||||
|
|
||||||
static Future<bool> checkSystemAlertWindow() async {
|
|
||||||
return Permission.systemAlertWindow.isGranted;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> checkStorage() async {
|
static Future<bool> checkStorage() async {
|
||||||
return await Permission.storage.isGranted;
|
return await Permission.storage.isGranted;
|
||||||
}
|
}
|
||||||
@@ -66,15 +62,6 @@ class Permissions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void location(Function()? onGranted) async {
|
|
||||||
if (await Permission.location.request().isGranted) {
|
|
||||||
onGranted?.call();
|
|
||||||
}
|
|
||||||
if (await Permission.location.isPermanentlyDenied || await Permission.location.isDenied) {
|
|
||||||
_showPermissionDeniedDialog(Permission.location.title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void speech(Function()? onGranted) async {
|
static void speech(Function()? onGranted) async {
|
||||||
if (await Permission.speech.request().isGranted) {
|
if (await Permission.speech.request().isGranted) {
|
||||||
onGranted?.call();
|
onGranted?.call();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -850,8 +850,7 @@ class IMUtils {
|
|||||||
var customData = map['data'];
|
var customData = map['data'];
|
||||||
switch (customType) {
|
switch (customType) {
|
||||||
case CustomMessageType.call:
|
case CustomMessageType.call:
|
||||||
var type = map['data']['type'];
|
content = '[${StrRes.callVoice}]';
|
||||||
content = '[${type == 'video' ? StrRes.callVideo : StrRes.callVoice}]';
|
|
||||||
break;
|
break;
|
||||||
case CustomMessageType.emoji:
|
case CustomMessageType.emoji:
|
||||||
content = '[${StrRes.emoji}]';
|
content = '[${StrRes.emoji}]';
|
||||||
@@ -1326,15 +1325,19 @@ class IMUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void previewLocation(Message message) {
|
static void previewLocation(Message message) {
|
||||||
var location = message.locationElem;
|
final location = message.locationElem;
|
||||||
Map detail = json.decode(location!.description!);
|
if (location == null) {
|
||||||
|
IMViews.showToast(StrRes.location);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final detail = LocationPreviewData.parse(location.description);
|
||||||
Logger.print('previewLocation ${location.latitude} ${location.longitude}');
|
Logger.print('previewLocation ${location.latitude} ${location.longitude}');
|
||||||
Get.to(
|
Get.to(
|
||||||
() => MapView(
|
() => MapView(
|
||||||
latitude: location.latitude!,
|
latitude: location.latitude ?? 0,
|
||||||
longitude: location.longitude!,
|
longitude: location.longitude ?? 0,
|
||||||
address1: detail['name'],
|
address1: detail.name,
|
||||||
address2: detail['addr'],
|
address2: detail.addr,
|
||||||
),
|
),
|
||||||
transition: Transition.cupertino,
|
transition: Transition.cupertino,
|
||||||
popGesture: true,
|
popGesture: true,
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class ImageTextButton extends StatelessWidget {
|
|||||||
final Function()? onTap;
|
final Function()? onTap;
|
||||||
|
|
||||||
ImageTextButton.call({super.key, this.onTap})
|
ImageTextButton.call({super.key, this.onTap})
|
||||||
: icon = ImageRes.audioAndVideoCall,
|
: icon = ImageRes.callVoice,
|
||||||
text = StrRes.audioAndVideoCall,
|
text = StrRes.audioAndVideoCall,
|
||||||
color = Styles.c_FFFFFF,
|
color = Styles.c_FFFFFF,
|
||||||
textStyle = null,
|
textStyle = null,
|
||||||
|
|||||||
@@ -276,9 +276,9 @@ class _ChatItemViewState extends State<ChatItemView> {
|
|||||||
} else if (_message.isLocationType) {
|
} else if (_message.isLocationType) {
|
||||||
final location = _message.locationElem;
|
final location = _message.locationElem;
|
||||||
child = ChatLocationView(
|
child = ChatLocationView(
|
||||||
description: location!.description!,
|
description: location?.description ?? '',
|
||||||
latitude: location.latitude!,
|
latitude: location?.latitude ?? 0,
|
||||||
longitude: location.longitude!,
|
longitude: location?.longitude ?? 0,
|
||||||
);
|
);
|
||||||
} else if (_message.isCardType) {
|
} else if (_message.isCardType) {
|
||||||
child = ChatCarteView(cardElem: _message.cardElem!);
|
child = ChatCarteView(cardElem: _message.cardElem!);
|
||||||
|
|||||||
@@ -4,25 +4,53 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:openim_common/openim_common.dart';
|
import 'package:openim_common/openim_common.dart';
|
||||||
|
|
||||||
|
class LocationPreviewData {
|
||||||
|
const LocationPreviewData({
|
||||||
|
required this.name,
|
||||||
|
required this.addr,
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final String addr;
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
static LocationPreviewData parse(String? description) {
|
||||||
|
var name = '';
|
||||||
|
var addr = '';
|
||||||
|
var url = '';
|
||||||
|
try {
|
||||||
|
final raw = description?.trim() ?? '';
|
||||||
|
if (raw.isNotEmpty) {
|
||||||
|
final decoded = json.decode(raw);
|
||||||
|
if (decoded is Map) {
|
||||||
|
url = '${decoded['url'] ?? ''}';
|
||||||
|
name = '${decoded['name'] ?? ''}';
|
||||||
|
addr = '${decoded['addr'] ?? ''}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
if (name.trim().isEmpty) {
|
||||||
|
name = StrRes.location;
|
||||||
|
}
|
||||||
|
return LocationPreviewData(name: name, addr: addr, url: url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChatLocationView extends StatelessWidget {
|
class ChatLocationView extends StatelessWidget {
|
||||||
const ChatLocationView({
|
const ChatLocationView({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.latitude,
|
required this.latitude,
|
||||||
required this.longitude,
|
required this.longitude,
|
||||||
}) : super(key: key);
|
});
|
||||||
final String description;
|
final String description;
|
||||||
final double latitude;
|
final double latitude;
|
||||||
final double longitude;
|
final double longitude;
|
||||||
final _decoder = const JsonDecoder();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
try {
|
final data = LocationPreviewData.parse(description);
|
||||||
final map = _decoder.convert(description);
|
|
||||||
String url = map['url'] ?? '';
|
|
||||||
String name = map['name'] ?? '';
|
|
||||||
String addr = map['addr'] ?? '';
|
|
||||||
return Container(
|
return Container(
|
||||||
width: locationWidth,
|
width: locationWidth,
|
||||||
height: 130.h,
|
height: 130.h,
|
||||||
@@ -37,22 +65,29 @@ class ChatLocationView extends StatelessWidget {
|
|||||||
4.verticalSpace,
|
4.verticalSpace,
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||||
child: name.toText
|
child: data.name.toText
|
||||||
..style = Styles.ts_0C1C33_14sp
|
..style = Styles.ts_0C1C33_14sp
|
||||||
..maxLines = 1
|
..maxLines = 1
|
||||||
..overflow = TextOverflow.ellipsis,
|
..overflow = TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
padding: EdgeInsets.symmetric(horizontal: 4.w),
|
||||||
child: addr.toText
|
child: (data.addr.isEmpty ? StrRes.location : data.addr).toText
|
||||||
..style = Styles.ts_8E9AB0_12sp
|
..style = Styles.ts_8E9AB0_12sp
|
||||||
..maxLines = 1
|
..maxLines = 1
|
||||||
..overflow = TextOverflow.ellipsis,
|
..overflow = TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
2.verticalSpace,
|
2.verticalSpace,
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ImageUtil.networkImage(
|
child: data.url.isEmpty
|
||||||
url: url,
|
? ColoredBox(
|
||||||
|
color: Styles.c_F0F2F6,
|
||||||
|
child: Center(
|
||||||
|
child: StrRes.location.toText..style = Styles.ts_8E9AB0_12sp,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ImageUtil.networkImage(
|
||||||
|
url: data.url,
|
||||||
width: locationWidth,
|
width: locationWidth,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
@@ -60,7 +95,5 @@ class ChatLocationView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {}
|
|
||||||
return Container();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ class ChatToolBox extends StatelessWidget {
|
|||||||
this.onTapCamera,
|
this.onTapCamera,
|
||||||
this.onTapCard,
|
this.onTapCard,
|
||||||
this.onTapFile,
|
this.onTapFile,
|
||||||
this.onTapLocation,
|
|
||||||
this.onTapDirectionalMessage,
|
this.onTapDirectionalMessage,
|
||||||
});
|
});
|
||||||
final Function()? onTapAlbum;
|
final Function()? onTapAlbum;
|
||||||
@@ -18,7 +17,6 @@ class ChatToolBox extends StatelessWidget {
|
|||||||
final Function()? onTapCall;
|
final Function()? onTapCall;
|
||||||
final Function()? onTapFile;
|
final Function()? onTapFile;
|
||||||
final Function()? onTapCard;
|
final Function()? onTapCard;
|
||||||
final Function()? onTapLocation;
|
|
||||||
final VoidCallback? onTapDirectionalMessage;
|
final VoidCallback? onTapDirectionalMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -50,11 +48,6 @@ class ChatToolBox extends StatelessWidget {
|
|||||||
icon: ImageRes.toolboxCard,
|
icon: ImageRes.toolboxCard,
|
||||||
onTap: onTapCard,
|
onTap: onTapCard,
|
||||||
),
|
),
|
||||||
ToolboxItemInfo(
|
|
||||||
text: StrRes.toolboxLocation,
|
|
||||||
icon: ImageRes.toolboxLocation,
|
|
||||||
onTap: () => Permissions.location(onTapLocation),
|
|
||||||
),
|
|
||||||
if (onTapDirectionalMessage != null)
|
if (onTapDirectionalMessage != null)
|
||||||
ToolboxItemInfo(
|
ToolboxItemInfo(
|
||||||
text: StrRes.toolboxDirectionalMessage,
|
text: StrRes.toolboxDirectionalMessage,
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,11 +14,9 @@ class RichTextInputBox extends StatefulWidget {
|
|||||||
this.showCameraIcon = true,
|
this.showCameraIcon = true,
|
||||||
this.showCardIcon = true,
|
this.showCardIcon = true,
|
||||||
this.showFileIcon = true,
|
this.showFileIcon = true,
|
||||||
this.showLocationIcon = true,
|
|
||||||
this.onTapAlbum,
|
this.onTapAlbum,
|
||||||
this.onTapCard,
|
this.onTapCard,
|
||||||
this.onTapFile,
|
this.onTapFile,
|
||||||
this.onTapLocation,
|
|
||||||
this.onSend,
|
this.onSend,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
@@ -29,12 +27,10 @@ class RichTextInputBox extends StatefulWidget {
|
|||||||
final bool showCameraIcon;
|
final bool showCameraIcon;
|
||||||
final bool showFileIcon;
|
final bool showFileIcon;
|
||||||
final bool showCardIcon;
|
final bool showCardIcon;
|
||||||
final bool showLocationIcon;
|
|
||||||
final Function()? onTapAlbum;
|
final Function()? onTapAlbum;
|
||||||
final Function()? onTapCamera;
|
final Function()? onTapCamera;
|
||||||
final Function()? onTapFile;
|
final Function()? onTapFile;
|
||||||
final Function()? onTapCard;
|
final Function()? onTapCard;
|
||||||
final Function()? onTapLocation;
|
|
||||||
final Function()? onSend;
|
final Function()? onSend;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -113,19 +109,12 @@ class _RichTextInputBoxState extends State<RichTextInputBox> {
|
|||||||
..height = 22.h
|
..height = 22.h
|
||||||
..opacity = _opacity
|
..opacity = _opacity
|
||||||
..onTap = widget.onTapCard,
|
..onTap = widget.onTapCard,
|
||||||
if (widget.showLocationIcon)
|
|
||||||
ImageRes.toolboxLocation1.toImage
|
|
||||||
..width = 16.w
|
|
||||||
..height = 22.h
|
|
||||||
..opacity = _opacity
|
|
||||||
..onTap = widget.onTapLocation,
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.showAlbumIcon ||
|
if (widget.showAlbumIcon ||
|
||||||
widget.showCameraIcon ||
|
widget.showCameraIcon ||
|
||||||
widget.showCardIcon ||
|
widget.showCardIcon ||
|
||||||
widget.showFileIcon ||
|
widget.showFileIcon)
|
||||||
widget.showLocationIcon)
|
|
||||||
15.verticalSpace,
|
15.verticalSpace,
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -72,12 +72,6 @@ class IMViews {
|
|||||||
alignment: MainAxisAlignment.start,
|
alignment: MainAxisAlignment.start,
|
||||||
onTap: () => onTapSheetItem.call(0),
|
onTap: () => onTapSheetItem.call(0),
|
||||||
),
|
),
|
||||||
SheetItem(
|
|
||||||
label: StrRes.callVideo,
|
|
||||||
icon: ImageRes.callVideo,
|
|
||||||
alignment: MainAxisAlignment.start,
|
|
||||||
onTap: () => onTapSheetItem.call(1),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -96,11 +90,6 @@ class IMViews {
|
|||||||
icon: ImageRes.callVoice,
|
icon: ImageRes.callVoice,
|
||||||
onTap: () => onTapSheetItem.call(0),
|
onTap: () => onTapSheetItem.call(0),
|
||||||
),
|
),
|
||||||
SheetItem(
|
|
||||||
label: StrRes.callVideo,
|
|
||||||
icon: ImageRes.callVideo,
|
|
||||||
onTap: () => onTapSheetItem.call(1),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ dependencies:
|
|||||||
flutter_map: ^6.0.1
|
flutter_map: ^6.0.1
|
||||||
ffmpeg_kit_flutter_full_gpl: 6.0.3
|
ffmpeg_kit_flutter_full_gpl: 6.0.3
|
||||||
pull_to_refresh_new: ^2.0.5
|
pull_to_refresh_new: ^2.0.5
|
||||||
geolocator: ^12.0.0
|
|
||||||
fixnum: ^1.1.0
|
fixnum: ^1.1.0
|
||||||
protobuf: ^3.0.0
|
protobuf: ^3.0.0
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -100,13 +99,6 @@ mixin OpenIMLive {
|
|||||||
final callType = mediaType == 'audio' ? CallType.audio : CallType.video;
|
final callType = mediaType == 'audio' ? CallType.audio : CallType.video;
|
||||||
final callObj = sessionType == ConversationType.single ? CallObj.single : CallObj.group;
|
final callObj = sessionType == ConversationType.single ? CallObj.single : CallObj.group;
|
||||||
|
|
||||||
if (Platform.isAndroid && _isRunningBackground) {
|
|
||||||
_beCalledEvent = event;
|
|
||||||
if (await Permissions.checkSystemAlertWindow()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_beCalledEvent = null;
|
|
||||||
OpenIMLiveClient().start(
|
OpenIMLiveClient().start(
|
||||||
Get.overlayContext!,
|
Get.overlayContext!,
|
||||||
callEventSubject: signalingSubject,
|
callEventSubject: signalingSubject,
|
||||||
|
|||||||
@@ -930,54 +930,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
geolocator:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: geolocator
|
|
||||||
sha256: "149876cc5207a0f5daf4fdd3bfcf0a0f27258b3fe95108fa084f527ad0568f1b"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "12.0.0"
|
|
||||||
geolocator_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: geolocator_android
|
|
||||||
sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.6.1"
|
|
||||||
geolocator_apple:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: geolocator_apple
|
|
||||||
sha256: c4ecead17985ede9634f21500072edfcb3dba0ef7b97f8d7bc556d2d722b3ba3
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.3.9"
|
|
||||||
geolocator_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: geolocator_platform_interface
|
|
||||||
sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.2.4"
|
|
||||||
geolocator_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: geolocator_web
|
|
||||||
sha256: "2ed69328e05cd94e7eb48bb0535f5fc0c0c44d1c4fa1e9737267484d05c29b5e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.1"
|
|
||||||
geolocator_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: geolocator_windows
|
|
||||||
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.3"
|
|
||||||
gesture_password_widget:
|
gesture_password_widget:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:openim_common/src/res/lang/zh_CN.dart';
|
||||||
|
import 'package:openim_common/src/widgets/chat/chat_location_view.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('用户可见通话文案不再出现视频通话或音视频通话', () {
|
||||||
|
for (final entry in zh_CN.entries) {
|
||||||
|
expect(entry.value.contains('视频通话'), isFalse, reason: '${entry.key}=${entry.value}');
|
||||||
|
expect(entry.value.contains('音视频通话'), isFalse, reason: '${entry.key}=${entry.value}');
|
||||||
|
}
|
||||||
|
expect(zh_CN['toolboxCall'], '语音通话');
|
||||||
|
expect(zh_CN['audioAndVideoCall'], '语音通话');
|
||||||
|
expect(zh_CN['video'], '视频');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('历史位置描述损坏时仍能解析成只读占位,不抛异常', () {
|
||||||
|
final ok = LocationPreviewData.parse('{"url":"http://x","name":"公司门口","addr":"一楼大厅"}');
|
||||||
|
expect(ok.name, '公司门口');
|
||||||
|
expect(ok.addr, '一楼大厅');
|
||||||
|
expect(ok.url, 'http://x');
|
||||||
|
|
||||||
|
expect(() => LocationPreviewData.parse('{not-json'), returnsNormally);
|
||||||
|
expect(() => LocationPreviewData.parse(null), returnsNormally);
|
||||||
|
expect(() => LocationPreviewData.parse(''), returnsNormally);
|
||||||
|
|
||||||
|
final broken = LocationPreviewData.parse('{not-json');
|
||||||
|
expect(broken.url, isEmpty);
|
||||||
|
expect(broken.name, isNotEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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