diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43186d53..5319c924 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,11 +134,7 @@ jobs: - name: Install pods working-directory: example - # --repo-update is required: the cached ~/.cocoapods may hold a spec repo that - # predates the version pinned in the podspec (the Pods cache is keyed by the - # Podfile.lock hash, so any dependency change restores an older cache). Without - # it the resolver fails with "none of your spec sources contain a spec". - run: bundle exec pod install --repo-update --project-directory=ios + run: bundle exec pod install --project-directory=ios - name: Build iOS example run: | diff --git a/.github/workflows/device-smoke.yml b/.github/workflows/device-smoke.yml index 0f631cad..5b2fa033 100644 --- a/.github/workflows/device-smoke.yml +++ b/.github/workflows/device-smoke.yml @@ -2,7 +2,7 @@ name: Device Smoke on: schedule: - - cron: '43 18 * * *' + - cron: '37 19 * * *' workflow_dispatch: permissions: @@ -153,11 +153,7 @@ jobs: - name: Install pods working-directory: example - # --repo-update is required: the cached ~/.cocoapods may hold a spec repo that - # predates the version pinned in the podspec (the Pods cache is keyed by the - # Podfile.lock hash, so any dependency change restores an older cache). Without - # it the resolver fails with "none of your spec sources contain a spec". - run: bundle exec pod install --repo-update --project-directory=ios + run: bundle exec pod install --project-directory=ios - name: Build iOS smoke app working-directory: example diff --git a/.github/workflows/single-account-nightly.yml b/.github/workflows/single-account-nightly.yml index b79254d9..9d96244a 100644 --- a/.github/workflows/single-account-nightly.yml +++ b/.github/workflows/single-account-nightly.yml @@ -76,11 +76,19 @@ jobs: path: artifacts - name: Prepare protected test configuration + # User tokens expire in ~24h and cannot be stored as secrets, so each + # test job exchanges the app credentials for a fresh token right here, + # before the emulator/simulator boots (fail fast on auth problems). env: E2E_APP_KEY: ${{ secrets.E2E_APP_KEY }} E2E_USER_ID: ${{ secrets.E2E_USER_ID }} - E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }} + E2E_REST_API: ${{ secrets.E2E_REST_API }} + E2E_CLIENT_ID: ${{ secrets.E2E_CLIENT_ID }} + E2E_CLIENT_SECRET: ${{ secrets.E2E_CLIENT_SECRET }} run: | + E2E_USER_TOKEN="$(node scripts/ci/fetch_e2e_user_token.js)" + echo "::add-mask::$E2E_USER_TOKEN" + export E2E_USER_TOKEN bash scripts/ci/write_single_account_config.sh "$RUNNER_TEMP/rn_single_account_config.json" echo "E2E_CONFIG_PATH=$RUNNER_TEMP/rn_single_account_config.json" >> "$GITHUB_ENV" @@ -176,11 +184,7 @@ jobs: - name: Install pods working-directory: example - # --repo-update is required: the cached ~/.cocoapods may hold a spec repo that - # predates the version pinned in the podspec (the Pods cache is keyed by the - # Podfile.lock hash, so any dependency change restores an older cache). Without - # it the resolver fails with "none of your spec sources contain a spec". - run: bundle exec pod install --repo-update --project-directory=ios + run: bundle exec pod install --project-directory=ios - name: Build iOS nightly app working-directory: example @@ -241,11 +245,19 @@ jobs: path: artifacts - name: Prepare protected test configuration + # User tokens expire in ~24h and cannot be stored as secrets, so each + # test job exchanges the app credentials for a fresh token right here, + # before the emulator/simulator boots (fail fast on auth problems). env: E2E_APP_KEY: ${{ secrets.E2E_APP_KEY }} E2E_USER_ID: ${{ secrets.E2E_USER_ID }} - E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }} + E2E_REST_API: ${{ secrets.E2E_REST_API }} + E2E_CLIENT_ID: ${{ secrets.E2E_CLIENT_ID }} + E2E_CLIENT_SECRET: ${{ secrets.E2E_CLIENT_SECRET }} run: | + E2E_USER_TOKEN="$(node scripts/ci/fetch_e2e_user_token.js)" + echo "::add-mask::$E2E_USER_TOKEN" + export E2E_USER_TOKEN bash scripts/ci/write_single_account_config.sh "$RUNNER_TEMP/rn_single_account_config.json" echo "E2E_CONFIG_PATH=$RUNNER_TEMP/rn_single_account_config.json" >> "$GITHUB_ENV" diff --git a/.gitignore b/.gitignore index 9b2f1ce8..717083ed 100644 --- a/.gitignore +++ b/.gitignore @@ -395,6 +395,8 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,reactnative example/src/env.ts +example/config.local.json +example/config.*.json src/version.ts *.tgz .workbuddy/ diff --git a/AGENTS.md b/AGENTS.md index 5e4475c3..eb33b7ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ - Java for the Android native bridge (`modules/java/`, `android/`) - Objective-C / Objective-C++ for the iOS native bridge (`modules/objc/`, `ios/`) - CMake for Android C++ compilation -- CocoaPods for iOS dependency management +- Swift Package Manager (default/recommended from 5.0.0) and CocoaPods for iOS dependency management (see `docs/spm.md`) - Yarn 4 workspaces (monorepo: root SDK + `example/`); Yarn 4.14.1 is pinned via `.yarnrc.yml` `yarnPath` - React Native Builder Bob for library builds (module, typescript) - TypeDoc for API documentation generation @@ -17,7 +17,7 @@ - Lefthook for git hooks (pre-commit, commit-msg, post-commit) - Commitlint enforcing conventional commits - Gitleaks for secret scanning (post-commit hook) -- HyphenateChat 4.25.0 (underlying native IM SDK for iOS and Android) +- HyphenateChat 5.0.0 (underlying native IM SDK for iOS and Android) ## Project Structure @@ -38,7 +38,7 @@ modules/ # Shared native code (RN and Flutter) android/ # Android native module ios/ # iOS native module (delegates to modules/objc) lib/ # Generated output (do not edit) -example/ # Example app (API tester; API registry in src/registry/) +example/ # Example app (API tester: Init/Login/Search/ApiCall pages + auto mode) ``` ## Commands @@ -51,7 +51,11 @@ yarn lint # ESLint (+ Prettier) over js/ts/tsx yarn test # run all Jest tests (unit + contract) yarn test:unit # unit tests only yarn test:contract # TS <-> native method-name contract tests +yarn test:ci-scripts # Node tests for auto-mode result parsing/reporting yarn check:circular:dpdm # circular dependency check +yarn report:api --platform android # run the 5.0.0 positive path and archive a local report +yarn report:api --platform ios # same for a booted iOS simulator +yarn report:api --platform android --script example/ci/port_5_0_0_negative.json # negative path yarn scan:deprecated # native deprecated API scan (needs gradle / xcodebuild): # clean build + parse; stdout is JSON, full build log and a # warnings-only copy (SDK code only) land in build/reports/. @@ -77,7 +81,8 @@ yarn example ios # run on iOS - Smoke cases live in `example/ci/no_login_smoke.json` (one no-login API call per manager, each with an `expect.errorCode`). The example app's auto mode (`example/src/auto/auto_mode.ts`) executes the script on launch; `scripts/ci/assert_script.js` compares the pulled device log against the expected error codes. Local runs match CI: `bash scripts/ci/smoke_local.sh ` builds the smoke package and runs the driver (`scripts/ci/run_device_android.sh` / `run_device_ios.sh`) in one step. - CI test jobs do not call the drivers directly: they go through `scripts/ci/run_ci_android.sh` / `run_ci_ios.sh` (thin wrappers around the drivers; the Android one exists because android-emulator-runner's `script:` input mangles multi-line scripts — only the first line reaches the shell — and it also handles on-device cleanup plus the logcat dump). The iOS build jobs zip the `.app` with `ditto` before upload-artifact: uploading a directory path strips the `ChatSdkExample.app` wrapper and download drops the executable bit. Android test jobs must keep the "Enable KVM group perms" udev step before android-emulator-runner: the ubuntu-24.04 image has /dev/kvm but the runner user is not in the kvm group (actions/runner-images#8542), and without it the emulator silently falls back to ~10x slower software emulation and the driver's `script.done` watchdog (default 180s) times out during app startup. - Smoke builds differ from dev builds because they must embed the JS bundle: Android `./gradlew app:assembleDebug -PbundleInDebug=true -PreactNativeArchitectures=x86_64` with `API_SCRIPT=/data/local/tmp/rn_smoke_no_login.json` in the environment; iOS `xcodebuild ... FORCE_BUNDLING=1 API_SCRIPT=/tmp/rn_smoke_no_login.json` (both are user-defined build settings so the RN bundle phase sees them). Debug builds normally skip bundling and load JS from Metro. -- Single-account nightly workflow: `.github/workflows/single-account-nightly.yml`, triggered daily (UTC 19:37) and by manual dispatch. It logs in with a real account and calls one read-only fetch API per manager, expecting success. Cases live in `example/ci/single_account.json` (a pure case file like `no_login_smoke.json`; the presence case references the account via `$config.accounts.0.id`). `scripts/ci/write_single_account_config.sh` generates the API_CONFIG (appKey + account, mirroring `example/src/env.ts`'s shape) from the `E2E_APP_KEY` / `E2E_USER_ID` / `E2E_USER_PASSWORD` secrets (stored in the GitHub environment `rn-single-account`) as a 0600 file that is deleted after the run; auto-mode derives init/login from it. The four jobs (build/test per platform) run in two parallel lanes; the console has multi-device login enabled for the account, so Android and iOS may log in at the same time. The driver scripts are the smoke ones with `SCRIPT_JSON` / `CONFIG_JSON` / `DEVICE_SCRIPT_PATH` / `DEVICE_CONFIG_PATH` / `HOST_SCRIPT_PATH` / `HOST_CONFIG_PATH` / `OUT_LOG` overrides; the nightly build inlines `API_SCRIPT` + `API_CONFIG` (`/data/local/tmp/rn_single_account{,_config}.json` on Android, `/tmp/rn_single_account{,_config}.json` on iOS). Local runs match CI: export the three `E2E_*` variables, then `bash scripts/ci/nightly_local.sh `. +- Traceable 5.0.0 API verification uses `example/ci/port_5_0_0_positive.json` and `example/ci/port_5_0_0_negative.json`. `yarn report:api --platform [--device ]` defaults to the positive path; pass `--script example/ci/port_5_0_0_negative.json` for the negative path. The runner reuses `smoke_local.sh` and the device drivers, then writes sanitized `run.json`, `events.jsonl`, `steps.json`, `summary.md`, `issues.md`, and `crash.log` under the Git-ignored `build/reports/5.0.0//`. The known Android crash for a missing message in `fetchGroupMessageReadReceipts` is intentionally excluded from execution and recorded as an issue candidate. Compare completed runs with `yarn report:api --android-report --ios-report `; only reviewed, sanitized conclusions belong in `docs/porting/`. +- Single-account nightly workflow: `.github/workflows/single-account-nightly.yml`, triggered daily (UTC 19:37) and by manual dispatch. It logs in with a real account and calls one read-only fetch API per manager, expecting success. Cases live in `example/ci/single_account.json` (a pure case file like `no_login_smoke.json`; the presence case references the account via `$config.accounts.0.id`). `scripts/ci/write_single_account_config.sh` generates the API_CONFIG (appKey + account + top-level `loginToken`, mirroring `example/src/env.ts`'s shape) as a 0600 file that is deleted after the run; auto-mode derives init/login from it. Password login is gone in 5.0 and user tokens expire in ~24h, so each test job first runs `scripts/ci/fetch_e2e_user_token.js`, which exchanges the `E2E_REST_API` / `E2E_APP_KEY` / `E2E_CLIENT_ID` / `E2E_CLIENT_SECRET` secrets (GitHub environment `rn-single-account`, along with `E2E_USER_ID`) for a fresh user token via the REST token API; the token never touches the build jobs. The four jobs (build/test per platform) run in two parallel lanes; the console has multi-device login enabled for the account, so Android and iOS may log in at the same time. The driver scripts are the smoke ones with `SCRIPT_JSON` / `CONFIG_JSON` / `DEVICE_SCRIPT_PATH` / `DEVICE_CONFIG_PATH` / `HOST_SCRIPT_PATH` / `HOST_CONFIG_PATH` / `OUT_LOG` overrides; the nightly build inlines `API_SCRIPT` + `API_CONFIG` (`/data/local/tmp/rn_single_account{,_config}.json` on Android, `/tmp/rn_single_account{,_config}.json` on iOS). Local runs match CI: export the five `E2E_*` variables (`E2E_APP_KEY`, `E2E_USER_ID`, `E2E_REST_API`, `E2E_CLIENT_ID`, `E2E_CLIENT_SECRET`), then `bash scripts/ci/nightly_local.sh `. ## Architecture @@ -122,6 +127,7 @@ TypeScript Manager → Native._callMethod(methodName, args) → NativeModule.cal ### Shared Native Code The `modules/` directory contains native code shared between React Native and Flutter SDKs: + - `modules/cpp/common/ExtSdkMethodType.*` defines all method name constants (must stay in sync with `src/__internal__/Consts.ts`) - `modules/java/com/chatsdk/dispatch/` and `modules/objc/dispatch/` contain the actual SDK wrapper implementations - `modules/*/rn/` contains React Native-specific adapters @@ -135,7 +141,7 @@ The `modules/` directory contains native code shared between React Native and Fl - Do not modify files under `lib/` directly — they are generated by `yarn prepare` (`bob build`). - Do not modify `src/version.ts` directly — it is generated by `scripts/generate-version.js`. - The `modules/*/flutter/` directories are for the Flutter SDK variant and should not be modified for React Native work. -- Keep the example app (`example/`) functional when making SDK changes. It is an API tester app (Init/Login/Search/ApiCall pages); API entries are registered in `example/src/registry/`. The example app uses bare React Native (no Expo). +- Keep the example app (`example/`) functional when making SDK changes. The example app is an API tester (Init/Login/Search/ApiCall pages plus an auto mode driven by JSON scripts; see `example/src/`). The example app uses bare React Native (no Expo). - Prefer running `yarn typecheck` and `yarn lint` to validate changes before committing. - This project uses Yarn 4 (Berry) with PnP disabled (node-modules linker). Do not use `npm`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f2039e..cc61c213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,74 @@ _English | [Chinese](./CHANGELOG.zh.md)_ # Update Log -## 1.21.0 +## 5.0.0 -- Dependent native SDKs are upgraded to versions (iOS 4.25.0 and Android 4.25.0). -- Add `ChatManager.fetchConversationsFromDB` method to get the list of conversations from the local database with pagination. Before calling this method, you need to set `ChatOptions.autoLoadConversations` to `false`. -- Add the `enableChatroomConversation` and `autoLoadConversations` properties in `ChatOptions`. -- Add the `apnsCertName` and `pushKitCertName` properties in `ChatOptions` to set the iOS APNs and PushKit certificate names when the SDK is initialized. -- Add `ChatClient.bindPushKitToken` and `ChatClient.unbindPushKitToken` methods to bind and unbind the Apple PushKit token for VoIP push notifications. Both methods are available on the iOS platform only. +This is a major release with breaking changes. Starting from this version, the React Native SDK version number is kept in sync with the underlying native SDK versions. + +- Dependent native SDKs are upgraded to versions (iOS 5.0.0 and Android 5.0.0). +- iOS integration: Swift Package Manager (SPM) is the recommended default integration from this version; CocoaPods remains supported, and `ChatSdk.podspec` picks one automatically based on the host app environment. See [SPM integration](./docs/spm.md). + +### Breaking Changes + +#### Login and Disconnection + +- Token login only: `ChatClient.loginWithToken` is the only remaining login method. Remove password login `ChatClient.login`, Agora token login `ChatClient.loginWithAgoraToken`, account registration `ChatClient.createAccount`, and `ChatClient.isLoginBefore`. +- Auto-login is removed: the `autoLogin` property of `ChatOptions` and the `onUserDidLoginFromOtherDevice` callback are removed; call `loginWithToken` proactively after app startup. +- Rename `ChatClient.renewAgoraToken` to `ChatClient.renewToken`. +- Device management APIs now authenticate with a token instead of a password: `ChatClient.getLoggedInDevicesFromServer(userId, token)`, `ChatClient.kickDevice(userId, token, resource)`, and `ChatClient.kickAllDevices(userId, token)`. +- Connection events: the server-side disconnection callbacks of `ChatConnectEventListener` are consolidated into a single `onDisconnected(errorCode?, info?)` callback that carries the reason as an error code (see the new `ChatDisconnectErrorCode` enum, whose values match the native `EMError` codes). Remove `onAppActiveNumberReachLimit`, `onUserDidLoginFromOtherDeviceWithInfo`, `onUserDidRemoveFromServer`, `onUserDidForbidByServer`, `onUserDidChangePassword`, `onUserDidLoginTooManyDevice`, `onUserKickedByOtherDevice`, and `onUserAuthenticationFailed`. For `USER_LOGIN_FROM_OTHER_DEVICE` (206), the device name and extension information are carried by the `info` parameter. The error code is passed through unchanged and codes not listed in the enum may also be received: the logout reasons (8, 104, 110, 202, 204, 206, 207, 213, 214, 216, 217, 220, 304, 305) invalidate the login state and require logging in again; the connection reasons (2, 4, 300, 303, 306) and any unlisted code keep the user logged in while the SDK reconnects automatically. A network disconnection carries no code on iOS and `NETWORK_ERROR` (2) on Android. A logout caused by token expiration still only fires `onTokenDidExpire`. + +#### Message Read Receipts + +- Message read receipts are reworked to batch sending: remove `ChatManager.sendMessageReadAck`, `sendGroupMessageReadAck`, `sendConversationReadAck`, `markAllConversationsAsRead`, `markMessageAsRead`, `markAllMessagesAsRead`, `fetchGroupAcks`, and `groupAckCount`; add `ChatManager.sendMessageReadReceipts` (send read receipts in batches), `clearConversationUnreadMessageCount`, `clearAllConversationUnreadMessageCount` (clearing the unread count no longer sends read receipts), `getGroupMessageReadReceipts`, and `fetchGroupMessageReadReceipts`. Remove the `onMessagesRead`, `onGroupMessageRead`, and `onConversationRead` callbacks and add the unified `onMessageReadReceipts` callback in `ChatMessageEventListener`; replace the `ChatGroupMessageAck` model with `ChatGroupReadReceipt` (the `from` field changes from a user ID string to a `ChatGroupMemberInfo` object, and the `content` field is removed), and add the `ChatMessageReadReceipt` model. +- Rename message read-state properties in `ChatMessage`: `hasReadAck` -> `isPeerRead`, `hasRead` -> `isRead`, `needGroupAck` -> `isNeedReadReceipt`, and `groupAckCount` -> `groupReadReceiptCount`. `isPeerRead`, `isRead`, and `groupReadReceiptCount` are read-only now; set `isNeedReadReceipt` before sending a message to request a read receipt for it. + +#### Group Configuration + +- Group configuration model refactor: remove the `ChatGroupStyle` enum and the `ChatGroupOptions` class; add the `ChatGroupConfigs` class (`isPublic`, `joinApprovalRequired`, `allowInvites`, `maxCount`, `inviteNeedConfirm`, `ext`) and the `ChatGroupConfigsType` enum. The `options` property of `ChatGroup` is replaced by `configs`, `createGroupEx` takes `configs` instead of `options`, the deprecated `createGroup` method is removed, and the `isDisabled` property moves to the top level of `ChatGroup`. Add `ChatGroupManager.updateGroupConfigs` to update group configs by type after group creation. Remove `ChatGroupManager.fetchPublicGroupsFromServer`. + +#### Removed Legacy APIs + +- Server-side pull APIs are removed in favor of the data automatically synced after login: `ChatManager.fetchAllConversations`, `ChatManager.fetchConversationsFromServerWithPage`, `ChatManager.fetchConversationsFromServerWithCursor`, `ChatManager.fetchPinnedConversationsFromServerWithCursor`, `ChatManager.fetchConversationsByOptions`, `ChatGroupManager.fetchJoinedGroupsFromServer`, `ChatContactManager.getAllContactsFromServer`, `ChatContactManager.fetchAllContacts`, and `ChatContactManager.fetchContacts`; read local data instead after `onDataSyncFinish`. The `onContactSyncStart` and `onContactSyncFinish` callbacks are removed. The `requireAck` and `enableAutoSyncContacts` properties of `ChatOptions` are removed, and the `ChatConversationFetchOptions` class is removed. +- Remove `ChatRoomManager.createChatRoom` and `ChatRoomManager.destroyChatRoom`; create and destroy chat rooms through the server-side REST API. The native SDKs on both platforms have also removed `getAllChatRooms`. +- Remove `ChatManager.reportMessage`; report messages to your app server instead. +- Remove long-deprecated APIs (use the replacements): `ChatRoomManager.joinChatRoom` (use `joinChatRoomEx`), `ChatGroupManager.fetchGroupInfoFromServer` (use `fetchGroupInfoWithoutMembersFromServer`), `ChatManager.searchMsgFromDB`/`getMessagesWithMsgType`/`getMessages`/`getMessagesWithKeyword`/`getMessageWithTimestamp` and the same four deprecated methods on `ChatConversation` (use `getMsgsWithMsgType`/`getMsgs`/`getMsgsWithKeyword`/`getMsgWithTimestamp`), `ChatManager.modifyMessageBody` (use `modifyMsgBody`), `ChatManager.fetchHistoryMessages` (use `fetchHistoryMessagesByOptions`), `ChatImageMessageBody.thumbnailSecret` (use `secret`), `ChatFetchMessageOptions.from` (use `senders`), the deprecated `sender` parameter of `ChatManager.getConvMsgsWithKeyword` and `ChatConversation.getMsgsWithKeyword` (use `senders`), `ChatRoom.muteList` (use `muteKVList`), the deprecated group callbacks `onMemberJoined`/`onMemberExited` (use `onMembersJoined`/`onMembersExited`), and the deprecated chat room callback `onMuteListAdded` (use `onMuteListAddedV2`). The public constructor of `ChatOptions` is removed; use `ChatOptions.withAppKey` or `ChatOptions.withAppId`. The deprecated event constants `onMessagesRecalled`, `onMessageReadAck`, `onMessageDeliveryAck`, and `onMessageStatusChanged` are removed. + +#### Other Behavior Changes + +- The statistical scope of `ChatManager.getUnreadMessageCount` changes: chat room conversations, thread messages, and conversations whose push remind type is `MentionOnly` or `None` are no longer counted. + +#### Push Credentials + +- Remove the `pushConfig` property of `ChatOptions` and the `ChatPushConfig` class: the configuration object that carried the vendor push appId / appKey / certificate name is no longer serialized by `ChatOptions`, and the native branches that read it are removed as well (the `EMOptions` mapping in the iOS `ExtSdkToJson`, and the `EMPushConfig` building in the Android `ExtSdkOptionsHelper`). **Initializing the Android vendor pushes (FCM, HUAWEI, Xiaomi, Meizu, OPPO, vivo) moves to the native project** (push dependencies, manifest, and `EMPushConfig`); the SDK only binds the device token. +- The two iOS certificate names became initialization options: add `ChatOptions.apnsCertName` (APNs) and `ChatOptions.pushKitCertName` (PushKit), which are passed to the native `EMOptions` by `ChatClient.init` and cannot change at runtime. The native SDK reads them when a token is bound instead of being written while binding. +- Remove `ChatClient.updatePushConfig(config)` in favor of `ChatClient.bindDeviceToken({ deviceToken, notifierName? })`: `notifierName` is the vendor push credential required on Android (FCM Sender ID, HUAWEI/Honor app ID, Xiaomi/Meizu app ID, OPPO app key, or the vivo `appId#appKey`) and is ignored on iOS, where the certificate name comes from `ChatOptions.apnsCertName`. + +### New Features + +- Data sync: add the `dataSyncType` property in `ChatOptions` and the `ChatDataSyncType` enum to configure automatic sync of conversations, contacts, and joined groups after login. Add the `onDataSyncStart`, `onDataSyncFinish`, and `onDatabaseOpened` callbacks in `ChatConnectEventListener`. +- Add the `displayName` and `displayAvatar` properties in `ChatConversation` (both may be empty). +- Add the `ConversationUnreadMessageCountCleared`(65) and `AllConversationUnreadMessageCountCleared`(66) multi-device events, which are fired when another device of the current account clears conversation unread counts. +- Add the `GROUP_UPDATE`(34) multi-device event, when another device of the current account updates the group information. +- Add `ChatManager.deleteConversations` to delete multiple local conversations at once, with an option to also delete the messages in them. IDs of conversations that do not exist are ignored. +- Add `ChatClient.bindDeviceToken({ deviceToken, notifierName? })` to bind the push token of the device, replacing the removed `ChatClient.updatePushConfig` (the method existed in 4.x; it is renamed here and its parameters are adjusted). +- Add the `apnsCertName` and `pushKitCertName` properties in `ChatOptions` to configure the iOS APNs and PushKit certificate names at initialization; both are available on iOS only and are ignored on other platforms. +- Add the iOS-only PushKit (VoIP push) APIs: `ChatClient.bindPushKitToken({ deviceToken })` binds a token and `ChatClient.unbindPushKitToken()` unbinds it, calling the native asynchronous `registerPushKitToken:completion:` and `unRegisterPushKitTokenWithCompletion:`; on other platforms both do nothing. Binding before login fails but caches the token, which the SDK binds automatically after the next successful login; `ChatClient.logout` with `unbindDeviceToken` set to `true` unbinds the PushKit token as well. + +### Bug Fixes + +- Fix `ChatManager.getConvMsgsWithKeyword` on Android: when the optional `senders` parameter was omitted, the native bridge passed a sender list containing an empty string, so the keyword search in the local database always returned an empty result. Omitting `senders` now disables sender filtering as documented. +- Fix `ChatManager.getUnreadCount` on iOS: it now uses the native unread-count statistic, so chat room conversations, chat threads, and conversations whose push remind type is `MentionOnly` or `None` are excluded, consistent with Android. +- Fix event payload key mismatches between the native bridge and the TypeScript layer: `onAdminAdded`/`onAdminRemoved` in `ChatGroupEventListener` now correctly carry `admin`, `onAllGroupMemberMuteStateChanged`/`onAllChatRoomMemberMuteStateChanged` now correctly carry `isAllMuted`, `onRequestToJoinDeclined` on iOS now carries `decliner` (previously always `undefined` on iOS), and `ChatRecalledMessageInfo.recalledConvId` is now populated from recall events. +- `ChatMessage.isPeerRead`, `ChatMessage.isRead`, and `ChatMessage.groupReadReceiptCount` are now typed as `readonly` in TypeScript, matching the native read-only semantics. +- Fixes included from the upgraded native SDKs: + - Fix the issue where the local login state might not be cleaned up in time after the account is forcibly disconnected by the server (including login on another device, account removal, or account disablement). + - (Android) Fix the issue where the database cache of the previous account might be reused when switching accounts. + - (Android) Fix the issue where reusing the request identifier when modifying a message could cause response matching errors. + - (Android) Fix the issue where the group namecard could not be set to an empty string in some scenarios. + - (Android) Fix the issue where MD5 pre-verification might still be performed when uploading attachments after the attachment MD5 verification was disabled. + - (Android) Fix several concurrency safety issues in network transmission, task queues, and database caching. + - (iOS) Fix the issue where uploading an attachment might hit the 2 MB size limit when `displayName` was not set; the SDK now falls back to the local file name as the display name. ## 1.20.0 diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 52423375..acd03845 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -2,13 +2,74 @@ _Chinese | [English](./CHANGELOG.md)_ # Update Log -## 1.21.0 +## 5.0.0 -- 依赖的原生 SDK 升级到版本(`iOS` 4.25.0 和`Android` 4.25.0)。 -- 新增 `ChatManager.fetchConversationsFromDB` 方法,用于分页从本地数据库获取会话列表。调用该方法前,需将 `ChatOptions.autoLoadConversations` 设置为 `false`。 -- 在 `ChatOptions` 中新增 `enableChatroomConversation` 和 `autoLoadConversations` 属性。 -- 在 `ChatOptions` 中新增 `apnsCertName` 和 `pushKitCertName` 属性,用于在 SDK 初始化时设置 iOS 的 APNs 和 PushKit 证书名称。 -- 新增 `ChatClient.bindPushKitToken` 和 `ChatClient.unbindPushKitToken` 方法,用于绑定和解绑苹果 PushKit token,以实现 VoIP 推送。两个方法仅在 iOS 平台有效。 +本次为大版本升级,包含不兼容变更。自本版本起,React Native SDK 版本号与依赖的原生 SDK 版本号保持同步。 + +- 依赖的原生 SDK 升级到版本(`iOS` 5.0.0 和`Android` 5.0.0)。 +- `iOS` 集成:自本版本起默认推荐 Swift Package Manager(SPM)方式集成,CocoaPods 继续支持,由 `ChatSdk.podspec` 按宿主 App 环境自动选择。详见 [SPM 集成说明](./docs/spm.md)。 + +### 不兼容变更(Breaking Changes) + +#### 登录与断开连接 + +- 登录仅保留 Token 方式(`ChatClient.loginWithToken`):移除密码登录 `ChatClient.login`、Agora Token 登录 `ChatClient.loginWithAgoraToken`、账号注册 `ChatClient.createAccount` 和 `ChatClient.isLoginBefore`。 +- 移除自动登录:删除 `ChatOptions` 的 `autoLogin` 属性与 `onUserDidLoginFromOtherDevice` 回调,应用启动后需主动调用 `loginWithToken`。 +- `ChatClient.renewAgoraToken` 更名为 `ChatClient.renewToken`。 +- 设备管理接口改用 Token 鉴权(原为密码):`ChatClient.getLoggedInDevicesFromServer(userId, token)`、`ChatClient.kickDevice(userId, token, resource)`、`ChatClient.kickAllDevices(userId, token)`。 +- 连接事件:`ChatConnectEventListener` 的服务器强制下线回调收敛为统一的 `onDisconnected(errorCode?, info?)` 回调,断开原因以错误码承载(见新增 `ChatDisconnectErrorCode` 枚举,枚举值与原生 `EMError` 错误码一致)。移除 `onAppActiveNumberReachLimit`、`onUserDidLoginFromOtherDeviceWithInfo`、`onUserDidRemoveFromServer`、`onUserDidForbidByServer`、`onUserDidChangePassword`、`onUserDidLoginTooManyDevice`、`onUserKickedByOtherDevice` 和 `onUserAuthenticationFailed` 回调。`USER_LOGIN_FROM_OTHER_DEVICE`(206) 场景的设备名与扩展信息由 `info` 参数携带。错误码原样透传,枚举未列出的码也可能收到:退出原因码(8、104、110、202、204、206、207、213、214、216、217、220、304、305)表示登录态已失效,需要重新登录;连接原因码(2、4、300、303、306)与未列出的码表示用户仍在登录态,SDK 会自动重连。网络断开时 iOS 无错误码、Android 为 `NETWORK_ERROR`(2)。Token 过期导致的登出仍只触发 `onTokenDidExpire`。 + +#### 消息已读回执 + +- 消息已读回执重构为批量发送:移除 `ChatManager.sendMessageReadAck`、`sendGroupMessageReadAck`、`sendConversationReadAck`、`markAllConversationsAsRead`、`markMessageAsRead`、`markAllMessagesAsRead`、`fetchGroupAcks`、`groupAckCount`;新增 `ChatManager.sendMessageReadReceipts`(批量发送已读回执)、`clearConversationUnreadMessageCount`、`clearAllConversationUnreadMessageCount`(清除未读数不再附带发送已读回执)、`getGroupMessageReadReceipts`、`fetchGroupMessageReadReceipts`。移除 `onMessagesRead`、`onGroupMessageRead`、`onConversationRead` 回调,`ChatMessageEventListener` 新增统一的 `onMessageReadReceipts` 回调;`ChatGroupMessageAck` 模型替换为 `ChatGroupReadReceipt`(`from` 字段由用户 ID 字符串改为 `ChatGroupMemberInfo` 对象,移除 `content` 字段),新增 `ChatMessageReadReceipt` 模型。 +- `ChatMessage` 已读相关属性改名:`hasReadAck` 改为 `isPeerRead`,`hasRead` 改为 `isRead`,`needGroupAck` 改为 `isNeedReadReceipt`,`groupAckCount` 改为 `groupReadReceiptCount`。其中 `isPeerRead`、`isRead`、`groupReadReceiptCount` 变为只读;如需消息已读回执,请在发送前设置 `isNeedReadReceipt`。 + +#### 群组配置 + +- 群组配置模型重构:移除 `ChatGroupStyle` 枚举与 `ChatGroupOptions` 类;新增 `ChatGroupConfigs` 类(`isPublic`、`joinApprovalRequired`、`allowInvites`、`maxCount`、`inviteNeedConfirm`、`ext`)与 `ChatGroupConfigsType` 枚举。`ChatGroup` 的 `options` 属性替换为 `configs`;`createGroupEx` 参数由 `options` 改为 `configs`;移除已废弃的 `createGroup` 方法;`isDisabled` 属性移至 `ChatGroup` 顶层。新增 `ChatGroupManager.updateGroupConfigs`,支持建群后按配置类型更新群组属性。移除 `ChatGroupManager.fetchPublicGroupsFromServer`。 + +#### 移除的旧 API + +- 数据同步取代服务端拉取接口:移除 `ChatManager.fetchAllConversations`、`fetchConversationsFromServerWithPage`、`fetchConversationsFromServerWithCursor`、`fetchPinnedConversationsFromServerWithCursor`、`fetchConversationsByOptions`,`ChatGroupManager.fetchJoinedGroupsFromServer`,`ChatContactManager.getAllContactsFromServer`、`fetchAllContacts`、`fetchContacts`,请在 `onDataSyncFinish` 后改为读取本地数据;同时移除 `onContactSyncStart`、`onContactSyncFinish` 回调、`ChatOptions` 的 `requireAck`、`enableAutoSyncContacts` 属性及 `ChatConversationFetchOptions` 类。 +- 移除 `ChatRoomManager.createChatRoom` 和 `ChatRoomManager.destroyChatRoom`,请改用服务端 REST API 创建和解散聊天室;双端原生 SDK 的 `getAllChatRooms` 也已移除。 +- 移除 `ChatManager.reportMessage`,请将消息举报提交至业务服务器。 +- 清理长期废弃的 API(请使用替代项):`ChatRoomManager.joinChatRoom`(改用 `joinChatRoomEx`)、`ChatGroupManager.fetchGroupInfoFromServer`(改用 `fetchGroupInfoWithoutMembersFromServer`)、`ChatManager.searchMsgFromDB`/`getMessagesWithMsgType`/`getMessages`/`getMessagesWithKeyword`/`getMessageWithTimestamp` 及 `ChatConversation` 同名四个废弃方法(分别改用 `getMsgsWithMsgType`/`getMsgs`/`getMsgsWithKeyword`/`getMsgWithTimestamp`)、`ChatManager.modifyMessageBody`(改用 `modifyMsgBody`)、`ChatManager.fetchHistoryMessages`(改用 `fetchHistoryMessagesByOptions`)、`ChatImageMessageBody.thumbnailSecret`(改用 `secret`)、`ChatFetchMessageOptions.from`(改用 `senders`)、`ChatManager.getConvMsgsWithKeyword` 和 `ChatConversation.getMsgsWithKeyword` 的废弃参数 `sender`(改用 `senders`)、`ChatRoom.muteList`(改用 `muteKVList`)、群组废弃回调 `onMemberJoined`/`onMemberExited`(改用 `onMembersJoined`/`onMembersExited`)、聊天室废弃回调 `onMuteListAdded`(改用 `onMuteListAddedV2`)。`ChatOptions` 公开构造函数移除,请使用 `ChatOptions.withAppKey` 或 `ChatOptions.withAppId`。废弃事件常量 `onMessagesRecalled`、`onMessageReadAck`、`onMessageDeliveryAck`、`onMessageStatusChanged` 一并移除。 + +#### 其他行为变化 + +- `ChatManager.getUnreadMessageCount` 统计范围调整:不再统计聊天室会话、Thread 消息以及推送提醒方式为 `MentionOnly` 或 `None` 的会话。 + +#### 推送配置 + +- 移除 `ChatOptions.pushConfig` 属性与 `ChatPushConfig` 类:承载厂商推送 appId / appKey / 证书名的配置对象不再由 `ChatOptions` 序列化,桥接层中读取该字段的原生分支(iOS `ExtSdkToJson` 的 `EMOptions` 映射、Android `ExtSdkOptionsHelper` 的 `EMPushConfig` 构建)一并删除。**Android 厂商推送(FCM、华为、小米、魅族、OPPO、vivo)的初始化需改在原生工程侧完成**(推送依赖、manifest 与 `EMPushConfig`),SDK 只负责上报设备 token。 +- iOS 的两个推送证书名改为初始化配置:新增 `ChatOptions.apnsCertName`(APNs)与 `ChatOptions.pushKitCertName`(PushKit),在 `ChatClient.init` 时下发给原生 `EMOptions`,运行时不可修改。原生在绑定 token 时读取该属性,绑定时不再写入证书名。 +- 移除 `ChatClient.updatePushConfig(config)`,改用 `ChatClient.bindDeviceToken({ deviceToken, notifierName? })`:`notifierName` 是 Android 必填的厂商推送凭据(FCM Sender ID、华为/荣耀 App ID、小米/魅族 App ID、OPPO App Key、vivo `appId#appKey`),iOS 忽略该参数,其证书名由 `ChatOptions.apnsCertName` 提供。 + +### 新增功能(New Features) + +- 数据同步:`ChatOptions` 新增 `dataSyncType` 属性与 `ChatDataSyncType` 枚举,用于配置登录后自动同步会话、好友和已加入群组;`ChatConnectEventListener` 新增 `onDataSyncStart`、`onDataSyncFinish` 和 `onDatabaseOpened` 回调。 +- `ChatConversation` 新增 `displayName` 和 `displayAvatar` 属性(均可为空)。 +- 新增多设备事件 `ConversationUnreadMessageCountCleared`(65) 和 `AllConversationUnreadMessageCountCleared`(66):当前账号的其他设备清除会话未读数时触发。 +- 新增多设备事件 `GROUP_UPDATE`(34):当前账号的其他设备修改群组信息。 +- 新增 `ChatManager.deleteConversations`,支持批量删除本地会话,并可选择是否同时删除会话内消息;不存在的会话 ID 会被忽略。 +- 新增 `ChatClient.bindDeviceToken({ deviceToken, notifierName? })`,用于绑定设备推送 token;取代已移除的 `ChatClient.updatePushConfig`(该方法名 4.x 即已存在,本次为改名并调整参数)。 +- `ChatOptions` 新增 `apnsCertName` 和 `pushKitCertName` 属性,用于在初始化时配置 iOS 的 APNs 与 PushKit 推送证书名;两个属性仅对 iOS 生效,其他平台忽略。 +- 新增 iOS 专用的 PushKit(VoIP 推送)接口:`ChatClient.bindPushKitToken({ deviceToken })` 绑定 token、`ChatClient.unbindPushKitToken()` 解绑 token,内部调用原生异步接口 `registerPushKitToken:completion:` 与 `unRegisterPushKitTokenWithCompletion:`;其他平台调用直接返回。未登录时绑定会失败但 token 已缓存,下次登录成功后 SDK 自动完成绑定;`ChatClient.logout` 且 `unbindDeviceToken` 为 `true` 时也会同时解绑 PushKit token。 + +### 问题修复(Bug Fixes) + +- 修复 Android 平台 `ChatManager.getConvMsgsWithKeyword` 的问题:省略可选参数 `senders` 时,原生桥接层会传入包含空字符串的发送者列表,导致本地关键字搜索始终返回空结果。现在省略 `senders` 即不按发送者过滤,与文档描述一致。 +- 修复 iOS 平台 `ChatManager.getUnreadCount` 的问题:改用原生未读数统计接口,统计范围排除聊天室会话、消息话题及推送提醒方式为 `MentionOnly` 或 `None` 的会话,与 Android 保持一致。 +- 修复原生桥接层与 TypeScript 层之间的事件载荷键名不一致问题:`ChatGroupEventListener` 的 `onAdminAdded`/`onAdminRemoved` 现在能正确携带 `admin`,`onAllGroupMemberMuteStateChanged`/`onAllChatRoomMemberMuteStateChanged` 现在能正确携带 `isAllMuted`,iOS 平台的 `onRequestToJoinDeclined` 回调现在能正确携带 `decliner`(此前 iOS 上始终为 `undefined`),`ChatRecalledMessageInfo.recalledConvId` 现在能从撤回事件中正确填充。 +- `ChatMessage.isPeerRead`、`ChatMessage.isRead` 和 `ChatMessage.groupReadReceiptCount` 在 TypeScript 类型层面标记为 `readonly`,与原生只读语义一致。 +- 本次升级同步包含以下原生 SDK 修复: + - 修复账号被服务端强制断开(包括在其他设备登录、被移除或被禁用)后,本地登录状态可能未及时清理的问题。 + - (Android)修复切换账号登录时可能复用上一账号数据库缓存的问题。 + - (Android)修复修改消息时复用请求标识可能导致响应匹配异常的问题。 + - (Android)修复部分场景下群名片无法设置为空字符串的问题。 + - (Android)修复关闭附件 MD5 校验后,上传附件仍可能执行 MD5 预校验的问题。 + - (Android)修复网络传输、任务队列和数据库缓存中的若干并发安全问题。 + - (iOS)修复发送附件消息未设置 `displayName` 时上传附件可能有 2 MB 大小限制的问题,SDK 现在自动使用本地文件名作为显示名。 ## 1.20.0 diff --git a/ChatSdk.podspec b/ChatSdk.podspec index 134a9a84..74c8b867 100644 --- a/ChatSdk.podspec +++ b/ChatSdk.podspec @@ -50,11 +50,11 @@ Pod::Spec.new do |s| spm_dependency( s, url: 'https://github.com/easemob/HyphenateChat_iOS.git', - requirement: { kind: 'upToNextMinorVersion', minimumVersion: '4.25.0' }, + requirement: { kind: 'upToNextMinorVersion', minimumVersion: '5.0.0' }, products: ['HyphenateChat'] ) else - s.dependency 'HyphenateChat', '~> 4.25.0' + s.dependency 'HyphenateChat', '~> 5.0.0' end install_modules_dependencies(s) diff --git a/README.md b/README.md index a9577762..598d53be 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,9 @@ See [iOS integration: CocoaPods and Swift Package Manager](docs/spm.md) for deta appKey !== undefined ? ChatOptions.withAppKey({ appKey: appKey, - autoLogin: false, }) : ChatOptions.withAppId({ appId: appId, - autoLogin: false, }) ) .then(() => { diff --git a/README.zh.md b/README.zh.md index fcdc9d9d..a729fa7d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -57,11 +57,9 @@ iOS 端的 `HyphenateChat` 原生依赖支持两种集成方式: appKey !== undefined ? ChatOptions.withAppKey({ appKey: appKey, - autoLogin: false, }) : ChatOptions.withAppId({ appId: appId, - autoLogin: false, }) ) .then(() => { diff --git a/android/build.gradle b/android/build.gradle index e1a8f239..3cb7d715 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -88,5 +88,5 @@ android { dependencies { implementation "com.facebook.react:react-android" - implementation 'io.hyphenate:hyphenate-chat:4.25.1' + implementation 'io.hyphenate:hyphenate-chat:5.0.0' } diff --git a/docs/5.0.0-migration-guide/2026-09-23-rn-4.x-to-5.0.0-migration-guide.md b/docs/5.0.0-migration-guide/2026-09-23-rn-4.x-to-5.0.0-migration-guide.md new file mode 100644 index 00000000..8a0c5175 --- /dev/null +++ b/docs/5.0.0-migration-guide/2026-09-23-rn-4.x-to-5.0.0-migration-guide.md @@ -0,0 +1,719 @@ +# 环信 React Native IM SDK 4.x 到 5.0.0 迁移指南 + +## 升级总览 + +React Native IM SDK 5.0.0 是一次源代码不兼容的大版本升级,主要涉及以下五个方面: + +1. **版本号与原生 SDK 对齐** + 自 5.0.0 起,React Native SDK 的版本号与底层原生 SDK 保持一致(iOS 5.0.0 和 Android 5.0.0),不再单独维护 `1.x` 版本序列。 +2. **iOS 集成方式变化** + iOS 侧推荐使用 Swift Package Manager(SPM)集成(4.x 已可选,5.0.0 起改为推荐/默认),CocoaPods 继续支持。`ChatSdk.podspec` 根据宿主应用环境自动选择集成方式。 +3. **数据同步机制调整** + 登录后,SDK 可自动同步会话、好友和已加入的群组数据,并将数据保存到本地数据库,替代原先由应用主动调用服务端拉取接口。 +4. **消息已读回执机制重构** + 已读回执由逐条发送调整为批量发送;清除本地未读数与向消息发送方发送已读回执相互独立;单聊和群聊使用统一的回执模型与回调。 +5. **群组配置模型与历史 API 精简** + `ChatGroupStyle` 枚举和 `ChatGroupOptions` 类拆分为 `ChatGroupConfigs` 与 `ChatGroupConfigsType`;移除一批服务端拉取接口、长期标记为废弃的接口及部分边缘能力。注册、举报、聊天室创建与解散、公开群列表等能力需由业务服务或服务端 REST API 实现;密码登录接口下线,仅保留 Token 登录。 + +:::tip +**升级要求:** React Native IM SDK 5.0.0 包含不兼容的 TypeScript API 变更,且依赖的原生 SDK 也包含不兼容的原生 API 变更。更新 SDK 后必须重新编译 iOS 和 Android 工程,并重点验证 Token 登录、数据同步、会话未读数、单聊和群聊已读回执、群组创建和配置更新、好友、附件消息及推送设置。 +::: + +## SDK 集成与版本要求 + +### 版本号与原生依赖对齐 + +本指南以 4.x 的最后一个版本 `1.20.0`(原生 SDK 4.24.1)为基线。更早的 4.x 版本(例如 npm 上的 `1.18.0`,对应原生 SDK 4.22.1)同样适用,只需注意其原生依赖版本不同。 + +| 项目 | 4.x(以 `1.20.0` 为例) | 5.0.0 | +| :--- | :--- | :--- | +| React Native SDK 版本号 | `1.x`(如 `1.20.0`) | `5.0.0`,与原生 SDK 同步 | +| iOS 原生 SDK(`HyphenateChat`) | `~> 4.24.1` | `~> 5.0.0` | +| Android 原生 SDK(`io.hyphenate:hyphenate-chat`) | `4.24.1` | `5.0.0` | +| iOS 集成方式 | CocoaPods 为默认,SPM 已可选(`USE_FRAMEWORKS=dynamic`) | SPM 推荐/默认,CocoaPods 继续支持 | + +### iOS 集成方式选择 + +`ChatSdk.podspec` 会根据宿主应用环境自动选择 `HyphenateChat` 的集成方式: + +| 条件 | 集成方式 | +| :--- | :--- | +| React Native >= 0.75 且使用 `USE_FRAMEWORKS=dynamic pod install` | Swift Package Manager(**推荐/默认**) | +| 其他情况(RN < 0.75、静态链接、默认 `pod install`) | CocoaPods(继续支持) | + +使用 SPM 集成时执行: + +```sh +cd ios && USE_FRAMEWORKS=dynamic pod install +``` + +- `pod install` 后请重新打开 `.xcworkspace`,让 Xcode 刷新 SPM 包状态。 +- 首次安装需要联网下载 SPM 包及其二进制 xcframework。 +- 静态链接(默认或 `USE_FRAMEWORKS=static`)下请勿使用 SPM 路径,否则可能遇到 duplicate symbols 或 undefined symbols 链接错误;此时会自动回退到 CocoaPods。 +- 源码中的 `#import ` 等导入方式在两种集成方式下均无需修改。 + +:::tip +CocoaPods trunk 将于 2026-12-02 进入只读状态,已发布版本仍可 `pod install`,但无法再从 trunk 获取新版本。这也是 5.0.0 起推荐 SPM 的原因之一。 +::: + +### 其他环境要求 + +- 插件自身的 Android 编译配置与 4.x 一致(`minSdkVersion 24`、`compileSdkVersion 36`);`android/build.gradle` 相对 4.x 只改了原生 SDK 依赖版本。 +- 但原生 SDK 5.0.0 的传递依赖有增加:Android 侧新增 `androidx.core:core:1.12.0`、`androidx.annotation:annotation:1.6.0` 和 `androidx.lifecycle:lifecycle-process:2.6.2`(4.24.1 只有 `cn.shengwang.infra:aosl`)。宿主工程如果 `compileSdk` 偏低,或锁定、排除了 androidx 版本,可能出现依赖解析或编译失败,需要相应放开。 +- 本项目在 React Native 0.83(仅新架构,`newArchEnabled=true`)上完成验证。新架构要求在 4.x 已经存在(Android 侧固定按 TurboModule 注册,`IS_NEW_ARCHITECTURE_ENABLED = true`),5.0.0 延续该要求:宿主工程需要已经启用新架构。跨 RN 大版本升级(如 0.72 到 0.83)时,JSON 数据传输契约与桥接方式不受影响,但需注意 RN 自身的类型重命名等变化。 + +## 初始化与登录 + +### 自动登录移除 + +React Native SDK 5.0.0 不再提供自动登录配置和自动登录状态查询。应用冷启动后,需要自行管理用户 ID 与 Token,并在适当时机主动调用 `ChatClient.loginWithToken`。 + +| 删除的 API | 替代方式 | 接口说明 | +| :--- | :--- | :--- | +| `ChatOptions#autoLogin` | 无直接替代。应用启动后主动调用 `ChatClient.loginWithToken`。 | 配置 SDK 初始化后是否自动登录。 | +| `ChatClient#isLoginBefore` | 根据业务需要调用 `ChatClient.isConnected()` 或缓存登录状态。 | 查询 SDK 是否处于自动登录流程或自动登录状态。 | + +`isConnected()` 表示是否连接至聊天服务器,不能替代应用对用户凭证和 Token 生命周期的管理。 + +### 密码登录下线 + +React Native SDK 5.0.0 仅保留 Token 登录方式。用户注册、密码校验和 Token 获取等账号管理操作需要 REST API 或由业务服务器完成。 + +| 删除的 API | 替代方式 | 接口说明 | +| :--- | :--- | :--- | +| `ChatClient#createAccount(userId, password)` | 无客户端替代;通过服务端 REST API 注册 IM 账号。 | 使用用户 ID 和密码注册 IM 账号。 | +| `ChatClient#login(userId, pwdOrToken, isPassword)` | `ChatClient#loginWithToken(userId, token)` | 使用用户 ID 和密码登录;4.x 中已废弃。 | +| `ChatClient#loginWithAgoraToken(userId, agoraToken)` | `ChatClient#loginWithToken(userId, token)` | 使用 Agora Token 登录;4.x 中已废弃。 | +| `ChatClient#renewAgoraToken(agoraToken)` | `ChatClient#renewToken(token)` | 更新当前登录会话的 Token,方法名和参数名同步调整。 | + +`renewToken(token)` 仍用于异步更新当前登录会话的 Token,业务代码应优先使用该方法处理 Token 更新与错误。 + +自动登录下线后,Token 生命周期需要应用自己闭环管理: + +1. 由业务服务器或服务端 REST API 为用户换取 IM Token,并安全下发到客户端; +2. 监听 `ChatConnectEventListener#onTokenWillExpire`(Token 有效期剩余约 20% 时触发),向业务服务器申请新 Token 后调用 `renewToken(token)` 更新当前会话; +3. 监听 `onTokenDidExpire`,此时登录态已失效,需要重新获取 Token 并调用 `loginWithToken`; +4. 冷启动时读取本地保存的用户 ID 与 Token,主动调用 `loginWithToken`。 + +Token 登录示例如下: + +```typescript +import { + ChatClient, + ChatOptions, + ChatDataSyncType, +} from 'react-native-chat-sdk'; + +// 初始化:必须在任何其他接口之前调用 +const options = ChatOptions.withAppKey({ + appKey: '', + // 登录后自动同步会话、好友和已加入的群组 + dataSyncType: + ChatDataSyncType.Conversations | + ChatDataSyncType.Contacts | + ChatDataSyncType.JoinedGroups, +}); +await ChatClient.getInstance().init(options); + +// 登录:应用自行管理 userId 与 token 的生命周期 +await ChatClient.getInstance().loginWithToken(userId, token); +``` + +### 登录与数据库打开解耦 + +React Native SDK 5.0.0 新增本地数据库打开回调。数据库打开后即可读取当前账号的本地数据,不必等待会话、好友或已加入群组的服务端同步完成,有助于加快冷启动时的首屏展示。 + +- `ChatConnectEventListener#onDatabaseOpened(username, errorCode)`:当前账号本地数据库打开完成时触发;`errorCode` 为 `0` 表示打开成功。 + +### 服务器强制下线回调收敛 + +4.x 中 8 类服务器强制下线场景(对应 9 个回调名,其中 `onUserDidLoginFromOtherDevice` 是已废弃的旧版)全部删除,统一为 `ChatConnectEventListener#onDisconnected(errorCode?, info?)`。 + +| 删除的回调 | 替代方式 | 回调说明 | +| :--- | :--- | :--- | +| `onUserDidLoginFromOtherDevice(deviceName?)`、`onUserDidLoginFromOtherDeviceWithInfo(params)` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.USER_LOGIN_FROM_OTHER_DEVICE`(206) | 当前账号在其他设备登录;设备名和扩展信息从 `info` 读取。 | +| `onUserDidRemoveFromServer()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.USER_REMOVED_FROM_SERVER`(207) | 当前账号被服务端删除。 | +| `onUserDidForbidByServer()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.SERVER_SERVICE_RESTRICTED`(305) | 当前账号被服务端禁用。 | +| `onUserDidChangePassword()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.USER_KICKED_BY_CHANGE_PASSWORD`(216) | 当前账号密码被修改。 | +| `onUserDidLoginTooManyDevice()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.USER_LOGIN_TOO_MANY_DEVICES`(214) | 当前账号登录设备数超限。 | +| `onUserKickedByOtherDevice()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.USER_KICKED_BY_OTHER_DEVICE`(217) | 当前账号被其他设备踢下线。 | +| `onAppActiveNumberReachLimit()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.APP_ACTIVE_NUMBER_REACH_LIMIT`(8) | 应用日活或月活数量达到上限。 | +| `onUserAuthenticationFailed()` | `onDisconnected(errorCode, info)`,判断 `errorCode === ChatDisconnectErrorCode.USER_AUTHENTICATION_FAILED`(202) | 用户认证失败。 | + +新增的 `ChatDisconnectErrorCode` 枚举取值与原生 SDK 的错误码一致,分为两组,**处理方式不同**: + +- **退出原因**:本地登录态已失效,需要回到登录页重新登录。 + `APP_ACTIVE_NUMBER_REACH_LIMIT = 8`、`INVALID_TOKEN = 104`、`INVALID_PARAM = 110`、`USER_AUTHENTICATION_FAILED = 202`、`USER_NOT_FOUND = 204`、`USER_LOGIN_FROM_OTHER_DEVICE = 206`、`USER_REMOVED_FROM_SERVER = 207`、`USER_BIND_ANOTHER_DEVICE = 213`、`USER_LOGIN_TOO_MANY_DEVICES = 214`、`USER_KICKED_BY_CHANGE_PASSWORD = 216`、`USER_KICKED_BY_OTHER_DEVICE = 217`、`USER_DEVICE_CHANGED = 220`、`SERVER_GET_DNSLIST_FAILED = 304`、`SERVER_SERVICE_RESTRICTED = 305`。 +- **连接原因**:用户仍处于登录态,SDK 会自动重连,**不要跳转登录页**。 + `NETWORK_ERROR = 2`、`EXCEED_SERVICE_LIMIT = 4`、`SERVER_NOT_REACHABLE = 300`、`SERVER_UNKNOWN_ERROR = 303`、`SERVER_DECRYPTION_FAILED = 306`。 + +处理该回调时需要注意: + +- `errorCode` 由 SDK 原样透传并不过滤,枚举未列出的码(包括原生 SDK 后续新增的码)也会送达,默认按连接原因处理。 +- 无 `errorCode` 表示平台未提供具体原因(例如 iOS 网络断开),同样按连接事件处理。 +- `info.deviceName` 和 `info.ext` 仅在 `errorCode` 为 206 时携带。 +- Token 过期导致的登出**不会**走 `onDisconnected`,仍然只触发 `onTokenDidExpire`。接入方需要同时处理这两个回调才能覆盖所有登出场景。 + +```typescript +import { + ChatClient, + ChatDisconnectErrorCode, + type ChatConnectEventListener, +} from 'react-native-chat-sdk'; + +const listener: ChatConnectEventListener = { + onDisconnected: ( + errorCode?: number, + info?: { deviceName?: string; ext?: string } + ) => { + switch (errorCode) { + case ChatDisconnectErrorCode.USER_LOGIN_FROM_OTHER_DEVICE: + // 206:其他设备登录,info.deviceName / info.ext 携带登录设备信息 + break; + case ChatDisconnectErrorCode.NETWORK_ERROR: + case ChatDisconnectErrorCode.SERVER_NOT_REACHABLE: + // 连接原因:保持登录态,等待 SDK 自动重连 + break; + default: + // 枚举未列出的码默认按连接原因处理 + break; + } + }, +}; +ChatClient.getInstance().addConnectionListener(listener); +``` + +## 数据同步与服务端拉取 API 迁移 + +### 数据同步 API + +React Native SDK 5.0.0 新增登录后自动数据同步机制。应用应在初始化 SDK 前通过 `ChatOptions#dataSyncType` 指定同步的数据类型,并通过 `ChatConnectEventListener` 监听同步进度。同步完成后,从各模块的本地接口读取数据。 + +| 所属类 | API 或配置 | 接口说明 | +| :--- | :--- | :--- | +| `ChatOptions` | `ChatDataSyncType` | 数据同步位掩码:`None = 0`、`Conversations = 1`、`Contacts = 2`、`JoinedGroups = 4`;多个类型可使用按位或组合。 | +| `ChatOptions` | `dataSyncType` | 设置登录后自动同步的数据类型;必须在 `ChatClient.init` 前配置。**未设置时沿用原生 SDK 默认值,而 iOS 与 Android 的默认值不一致**,建议显式配置。 | +| `ChatConnectEventListener` | `onDataSyncStart(type)` | 通知指定类型的数据开始同步。 | +| `ChatConnectEventListener` | `onDataSyncFinish(type, errorCode)` | 通知指定类型的数据同步结束;`errorCode` 为 `0` 表示成功。 | +| `ChatConnectEventListener` | `onDatabaseOpened(username, errorCode)` | 通知指定账号的本地数据库已经打开;该事件不表示任何服务端数据已经同步完成。`errorCode` 为 `0` 表示成功;**Android 原生回调不带错误码,桥接层固定回填 `0`**,只有 iOS 会返回真实的打开失败错误码。 | + +:::tip +`ChatOptions#dataSyncType` 不设置时,React Native SDK 不会下发该字段,由原生 SDK 使用自身默认值决定同步范围:iOS 默认同步会话(`Conversations`),Android 默认不自动同步(`None`)。为保证双端行为一致,建议显式设置 `dataSyncType`。 +::: + +典型配置如下: + +```typescript +import { + ChatClient, + ChatDataSyncType, + ChatOptions, + type ChatConnectEventListener, +} from 'react-native-chat-sdk'; + +const options = ChatOptions.withAppKey({ + appKey: '', + dataSyncType: + ChatDataSyncType.Conversations | + ChatDataSyncType.Contacts | + ChatDataSyncType.JoinedGroups, +}); +await ChatClient.getInstance().init(options); + +const listener: ChatConnectEventListener = { + onDatabaseOpened: (username: string, errorCode: number) => { + // 本地数据库已可读,可先渲染上次登录的本地数据 + }, + onDataSyncStart: (type: ChatDataSyncType) => { + // 指定类型的数据开始同步 + }, + onDataSyncFinish: (type: ChatDataSyncType, errorCode: number) => { + if (errorCode === 0) { + // 该类型数据同步完成,刷新对应列表 + } + }, +}; +ChatClient.getInstance().addConnectionListener(listener); +``` + +### 服务端拉取 API 迁移 + +原先主动拉取会话、好友和已加入群组,再在回调中刷新数据的方式,统一调整为 **配置数据同步范围、登录后自动同步、读取本地数据,并在 `onDataSyncFinish` 成功后刷新 UI**。 + +| 类 | 删除的 API | 5.0.0 推荐方式 | +| :--- | :--- | :--- | +| `ChatManager` | `fetchAllConversations`、`fetchConversationsFromServerWithPage`、`fetchConversationsFromServerWithCursor` | 这些接口用于从服务器拉取会话列表。改用本地 `getAllConversations`,并在 `onDataSyncFinish` 收到 `ChatDataSyncType.Conversations` 成功后刷新。 | +| `ChatManager` | `fetchPinnedConversationsFromServerWithCursor` | 该接口用于拉取服务端置顶会话。置顶状态随会话数据同步落地,改为读取本地 `ChatConversation.isPinned` 和 `pinnedTime`。 | +| `ChatManager` | `fetchConversationsByOptions`、`ChatConversationFetchOptions` | 该接口按条件拉取服务端会话,底层能力已整体移除且无客户端替代。改用本地 `getAllConversations` 后自行过滤。 | +| `ChatGroupManager` | `fetchJoinedGroupsFromServer` | 该接口分页拉取已加入群组。改用本地 `getJoinedGroups`,并在 `onDataSyncFinish` 收到 `ChatDataSyncType.JoinedGroups` 成功后刷新。 | +| `ChatContactManager` | `getAllContactsFromServer`、`fetchAllContacts`、`fetchContacts` | 这些接口从服务器拉取好友列表。5.0.0 已无好友列表服务端拉取入口;改用本地 `getAllContacts`(返回 `ChatContact` 列表)、`getAllContactsFromDB`(返回用户 ID 列表)或 `getContact`,并在 `onDataSyncFinish` 收到 `ChatDataSyncType.Contacts` 成功后刷新。 | +| `ChatOptions` | `enableAutoSyncContacts` | 该配置控制好友自动同步。改为在 `dataSyncType` 中包含 `ChatDataSyncType.Contacts`。 | + +相应地,`ChatContactEventListener#onContactSyncStart` 和 `onContactSyncFinish` 已删除。请改用 `ChatConnectEventListener#onDataSyncStart` 和 `onDataSyncFinish`,并判断 `type` 是否包含 `ChatDataSyncType.Contacts`。详见 [监听器回调变化汇总](#监听器回调变化汇总)。 + +## 已读回执体系重构 + +消息已读回执由逐条发送调整为批量发送;是否需要回执通过 `ChatMessage.isNeedReadReceipt` 按消息设置;发送消息已读回执与清理会话未读数相互独立。旧 API 不提供兼容别名,属于不兼容变更。 + +### 发送消息已读回执与清除未读数 + +| 删除的 API | 5.0.0 替代 | 说明 | +| :--- | :--- | :--- | +| `ChatManager#sendMessageReadAck(message)` | `ChatManager#sendMessageReadReceipts(msgIds)` | 批量发送消息已读回执,单聊和群聊统一使用。 | +| `ChatManager#sendGroupMessageReadAck(...)` | `ChatManager#sendMessageReadReceipts(msgIds)` | 不再为群聊提供单独的逐条已读回执接口,也不再支持在群消息已读回执中携带自定义内容。 | +| `ChatManager#sendConversationReadAck(convId)` | `ChatManager#clearConversationUnreadMessageCount(convId)`,并按需调用 `sendMessageReadReceipts` | 旧接口发送会话级已读回执。5.0.0 将清除当前用户未读数与通知消息发送方拆为两个操作。 | +| `ChatManager#markAllConversationsAsRead()` | `ChatManager#clearAllConversationUnreadMessageCount()` | 清除所有本地会话的未读数,并同步至当前用户的其他设备。 | +| `ChatManager#markMessageAsRead(convId, convType, msgId, isChatThread)`、`markAllMessagesAsRead(convId, convType, isChatThread)` | `ChatManager#clearConversationUnreadMessageCount(convId)` | 通过会话级接口清除本地未读数;不再由消息维度修改已读态。 | +| `ChatConversation#markMessageAsRead(msgId)`、`ChatConversation#markAllMessagesAsRead()` | `ChatManager#clearConversationUnreadMessageCount(convId)` | 通过会话级接口清除本地未读数;不再由 `ChatConversation` 修改消息已读态。 | +| `ChatOptions#requireAck` | 发送前设置 `ChatMessage.isNeedReadReceipt = true` | 删除全局已读回执开关,改为按消息指定是否需要已读回执。**默认值发生反转**:4.x 的 `requireAck` 默认为 `true`(所有消息都请求回执),5.0.0 的 `isNeedReadReceipt` 默认为 `false`,不显式设置将收不到对方已读。 | + +`sendMessageReadReceipts` 每次最多接收 50 条属于同一会话的消息。消息的 `isNeedReadReceipt` 必须为 `true`;该接口不会清除或修改会话的本地未读数。 + +两个批量回执接口(`sendMessageReadReceipts`、`getGroupMessageReadReceipts`)都按消息 ID 从**本地数据库**取消息:本地不存在的 ID 会被静默忽略,若整批都取不到消息,原生返回 `INVALID_PARAM`(110)。调用前请确认这些消息已存在于本地。 + +```typescript +// 批量发送已读回执(消息须属于同一会话,且 isNeedReadReceipt 为 true) +await ChatClient.getInstance().chatManager.sendMessageReadReceipts([ + 'msgId1', + 'msgId2', +]); + +// 清除会话未读数(不会向消息发送方发送已读回执) +await ChatClient.getInstance().chatManager.clearConversationUnreadMessageCount( + 'convId' +); +``` + +### 接收消息已读回执 + +React Native SDK 5.0.0 将单聊和群聊的实时已读回执统一通过 `ChatMessageEventListener#onMessageReadReceipts` 回调,不再分别使用单聊和群聊回调。 + +| 4.x 回调 | 5.0.0 回调 | 说明 | +| :--- | :--- | :--- | +| `ChatMessageEventListener#onMessagesRead(messages)` | `ChatMessageEventListener#onMessageReadReceipts(receipts)` | 接收单聊消息的已读回执。 | +| `ChatMessageEventListener#onGroupMessageRead(groupMessageAcks)` | `ChatMessageEventListener#onMessageReadReceipts(receipts)` | 接收群聊消息的已读回执。 | +| `ChatMessageEventListener#onConversationRead(from, to?)` | 无直接替代 | 会话级已读回执不再单独回调;消息级已读结果由 `onMessageReadReceipts` 通知。 | + +5.0.0 新增 `ChatMessageReadReceipt`,用于统一描述消息已读回执: + +- `msgId`:消息 ID。 +- `convId`:会话 ID。 +- `isPeerReceipt`:`true` 表示回执来自对端用户(单聊对端已读,或群成员已读);`false` 表示回执来自当前用户在其他设备上的操作。 +- `readCount`:群聊中该消息的已读人数;单聊恒为 `0`。 + +### 回执详情查询 + +| 4.x API | 5.0.0 API | 说明 | +| :--- | :--- | :--- | +| `ChatManager#fetchGroupAcks(msgId, groupId, startAckId, pageSize)` | `ChatManager#fetchGroupMessageReadReceipts(msgId, groupId, cursor?, pageSize?)` | 分页获取指定群消息的已读成员详情;返回 `ChatCursorResult`。4.x 的第 3 个参数 `startAckId` 改名为 `cursor`,与 `pageSize` 一起变为可选(默认 `''` 和 `20`);下一页将上一页返回的游标传入 `cursor`。 | +| `ChatManager#groupAckCount(msgId)` | `ChatMessage#groupReadReceiptCount` | 群消息已读人数改为从消息对象读取,且为只读属性。 | +| `ChatMessage#groupReadCount`(返回 Promise 的只读 getter,内部调用 `groupAckCount`) | `ChatMessage#groupReadReceiptCount`,或 `ChatManager#getGroupMessageReadReceipts(msgIds)` | 该 getter 已删除。单条消息改用同步只读属性 `groupReadReceiptCount`;需要批量或最新数据时用 `getGroupMessageReadReceipts`。 | +| 无 | `ChatManager#getGroupMessageReadReceipts(msgIds)` | 从本地数据库批量获取群消息的已读回执汇总;每次最多传入 20 条消息,且所有消息必须属于同一会话,返回 `ChatMessageReadReceipt` 列表。 | + +回执详情模型由 `ChatGroupMessageAck` 替换为 `ChatGroupReadReceipt`,**字段名同时由下划线命名改为驼峰命名**: + +- `msg_id` → `msgId`:群消息 ID。 +- `ack_id` → `ackId`:已读回执 ID,也用于分页游标。 +- `from`:发送已读回执的群成员,类型由字符串(用户 ID)改为 `ChatGroupMemberInfo` 对象;原先直接使用 `from` 的位置改为 `from.memberId`。 +- `count`:已读回执数量。 +- `timestamp`:发送已读回执的时间戳。 +- 原 `content` 属性已移除,服务端不再下发 ACK 扩展内容。 + +`ChatGroupMemberInfo` 是 5.0.0 新增的模型,字段如下: + +| 字段 | 类型 | 说明 | +| :--- | :--- | :--- | +| `memberId` | `string` | 群成员的用户 ID。 | +| `joinedTimestamp` | `number` | 入群时间,Unix 时间戳(毫秒)。 | +| `role` | `ChatGroupPermissionType` | 成员角色:`Member`、`Admin`、`Owner`;未知为 `None`(`-1`)。 | +| `namecard?` | `string` | 群名片。 | +| `nickname?` | `string` | 昵称。 | +| `avatarUrl?` | `string` | 头像 URL。 | + +:::tip +`fetchGroupMessageReadReceipts` 存在平台差异:`groupId` 参数在 Android 侧被忽略;返回结果的 `totalCount` 字段(`ChatCursorResult` 在 5.0.0 新增)**仅 iOS 提供**,Android 为 `undefined`,业务代码需按可选字段处理。 +::: + +:::warning +已知问题:Android 侧给 `fetchGroupMessageReadReceipts` 传入本地不存在的 `msgId` 会导致原生崩溃(`NullPointerException`),iOS 侧则返回空结果(`{ cursor: '', totalCount: 0, list: [] }`)。调用前请确认 `msgId` 对应的消息存在于本地数据库。 +::: + +### ChatMessage 已读相关属性重命名 + +| 4.x 属性 | 5.0.0 属性 | 说明 | +| :--- | :--- | :--- | +| `ChatMessage.hasReadAck` | `ChatMessage.isPeerRead` | 判断消息对端是否已读;5.0.0 为只读属性。 | +| 可读写的 `ChatMessage.hasRead` | 只读的 `ChatMessage.isRead` | 属性名变更且 setter 被删除,消息的本地已读状态由 SDK 内部维护。 | +| `ChatMessage.needGroupAck` | `ChatMessage.isNeedReadReceipt` | 单聊和群聊均适用;发送消息前设置是否需要已读回执,是唯一保留的可写已读相关属性。 | +| `ChatMessage.groupAckCount` | `ChatMessage.groupReadReceiptCount` | 获取群聊消息的已读人数;5.0.0 为只读属性。 | +| `ChatMessage.groupReadCount`(返回 Promise 的 getter) | 已删除 | 该 getter 内部调用 `groupAckCount`;改用同步只读属性 `groupReadReceiptCount`。 | + +:::tip +以上改名后的四个属性同时参与消息的序列化与反序列化,升级后从本地数据库或服务端读取的消息对象使用新属性名,旧属性名将读取到 `undefined`。请全局搜索并替换业务代码中的旧属性引用。 +::: + +:::warning +默认集成方式(npm 依赖)下无需关心桥接层,本节仅适用于基于本仓库自行维护原生 wrapper 的场景:桥接层使用的 JSON 字段名同步改名(`hasReadAck` → `isPeerRead`、`hasRead` → `isRead`、`needGroupAck` → `isNeedReadReceipt`、`groupAckCount` → `groupReadReceiptCount`),其中只有 `isNeedReadReceipt` 保留写入语义,其余三个字段的 setter 已移除。 +::: + +### 多设备事件 + +`ChatMultiDeviceEvent` 新增以下枚举值。当前账号在其他设备清除未读数后,当前设备通过 `ChatMultiDeviceEventListener#onConversationEvent(event, convId, convType)` 收到通知: + +- `CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED = 65`:其他设备清除了指定会话的未读数。 +- `ALL_CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED = 66`:其他设备清除了所有会话的未读数。 + +同时新增 `GROUP_UPDATE`(原生值 `34`):其他设备更新了群组信息时触发,通过 `ChatMultiDeviceEventListener#onGroupEvent(event, target, usernames)` 收到。该事件**仅 iOS 上报**,Android 侧群元数据变更使用原生值 `52`(`GROUP_METADATA_CHANGED`,4.x 已有)上报。 + +## 群组配置模型重构 + +React Native SDK 5.0.0 将群组可见性、入群审批和成员邀请权限从 `ChatGroupStyle` 单一枚举改为 `ChatGroupConfigs` 中的独立属性。**该调整不提供兼容层,升级时需要修改建群和群组配置代码。** + +### ChatGroupStyle 与 ChatGroupConfigs 对照 + +| 4.x `ChatGroupStyle`(已删除) | 5.0.0 `ChatGroupConfigs` 配置 | +| :--- | :--- | +| `PrivateOnlyOwnerInvite` | `isPublic = false`,`allowInvites = false` | +| `PrivateMemberCanInvite` | `isPublic = false`,`allowInvites = true` | +| `PublicJoinNeedApproval` | `isPublic = true`,`joinApprovalRequired = true` | +| `PublicOpenJoin` | `isPublic = true`,`joinApprovalRequired = false` | + +### ChatGroupOptions 与 ChatGroupConfigs 对照 + +| 4.x `ChatGroupOptions`(已删除) | 5.0.0 `ChatGroupConfigs` | +| :--- | :--- | +| `style: ChatGroupStyle` | `isPublic`、`joinApprovalRequired`、`allowInvites` 三个布尔属性;默认值均为 `false` | +| `maxCount: number = 200` | `maxCount: number = 200`,保持不变;iOS 侧头文件标注取值范围 `[3, 2000]` | +| `inviteNeedConfirm: boolean` | 属性和默认值保持不变(默认 `false`) | +| `ext?: string` | 属性保持不变 | + +:::warning +**默认群类型发生变化。** 4.x 中 `new ChatGroupOptions({})` 未指定 `style` 时默认为 `PublicJoinNeedApproval`(公开群 + 入群需审批);5.0.0 中 `new ChatGroupConfigs({})` 的三个布尔属性默认全为 `false`,等价于「私有群 + 仅群主可邀请」。原先依赖默认值建群的代码升级后会静默创建出另一种群,请显式设置 `isPublic`、`joinApprovalRequired`、`allowInvites`。 +::: + +其中 `allowInvites` 仅对私有群生效,`joinApprovalRequired` 仅对公开群生效。 + +### 相关 API 变化 + +| 4.x API | 5.0.0 API 或适配方式 | +| :--- | :--- | +| `ChatGroupManager#createGroup(options, groupName, desc?, inviteMembers?, inviteReason?)` | 已删除。使用 `ChatGroupManager#createGroupEx(params)`;位置参数改为单个 `params` 对象,`params.configs` 类型为 `ChatGroupConfigs`,并支持设置群头像(`groupAvatar`)。 | +| `createGroupEx` 的 `options?: ChatGroupOptions` | 方法名不变,参数改为 `configs: ChatGroupConfigs`,用于提交拆分后的群组配置。 | +| `ChatGroup.options` 的 `ChatGroupOptions` 类型 | `ChatGroup.configs` 的 `ChatGroupConfigs` 类型,用于读取群组配置;原 `options` 属性已删除。 | +| `ChatGroupOptions.isDisabled` | 提升为顶层的 `ChatGroup.isDisabled`,读取路径由 `group.options.isDisabled` 改为 `group.isDisabled`;该字段只读,`ChatGroupConfigs` 和 `ChatGroupConfigsType` 中都没有对应项。 | +| 无 | `ChatGroupManager#updateGroupConfigs(groupId, types, configs)`:创建群组后,按 `types` 指定的字段更新群组配置;**仅群主和管理员可调用**。 | +| 无 | `ChatGroupConfigsType`:包含 `AllowInvites = 1`、`MaxUsers = 2`、`InviteNeedConfirm = 4`、`JoinApprovalRequired = 8`、`IsPublic = 16`、`Ext = 32`,可按位或组合。 | + +群名称、描述和头像不属于 `ChatGroupConfigsType`,仍分别使用 `changeGroupName`、`changeGroupDescription` 和 `updateGroupAvatar` 更新。 + +建群与更新群配置示例如下: + +```typescript +import { ChatClient, ChatGroupConfigs, ChatGroupConfigsType } from 'react-native-chat-sdk'; + +// 创建群组:对应 4.x 的 ChatGroupStyle.PublicJoinNeedApproval +const configs = new ChatGroupConfigs({ + isPublic: true, + joinApprovalRequired: true, + allowInvites: false, + maxCount: 200, +}); +const group = await ChatClient.getInstance().groupManager.createGroupEx({ + groupName: 'groupName', + configs, + inviteMembers: ['userId1'], +}); + +// 创建后按类型更新群配置:只更新最大成员数和群扩展 +await ChatClient.getInstance().groupManager.updateGroupConfigs( + group.groupId, + ChatGroupConfigsType.MaxUsers | ChatGroupConfigsType.Ext, + new ChatGroupConfigs({ maxCount: 500, ext: '{"key":"value"}' }) +); +``` + +## 设备管理与鉴权 + +4.x 的三个设备管理接口用最后一个参数 `isPassword?: boolean` 区分密码/Token 鉴权;5.0.0 删除了 `isPassword` 参数,第二个参数固定为 Token: + +| 4.x API | 5.0.0 API | 接口说明 | +| :--- | :--- | :--- | +| `getLoggedInDevicesFromServer(userId, pwdOrToken, isPassword?)` | `getLoggedInDevicesFromServer(userId, token)` | 查询指定账号当前已登录的设备列表。 | +| `kickDevice(userId, pwdOrToken, resource, isPassword?)` | `kickDevice(userId, token, resource)` | 踢出指定账号的某一登录设备;通过设备列表获取 `resource`。 | +| `kickAllDevices(userId, pwdOrToken, isPassword?)` | `kickAllDevices(userId, token)` | 踢出指定账号的全部登录设备。 | + +即使 4.x 已经用 Token 调用(`isPassword` 传 `false` 或省略),也需要删除多余的实参,否则 TypeScript 会因参数个数不匹配报错。 + +调用这些接口所需的目标用户 Token 应由可信的业务服务器提供,不应在客户端保存其他用户的密码。 + +## 推送与设备 Token + +4.x 通过 `ChatOptions.pushConfig`(`ChatPushConfig` 对象)在初始化时下发厂商推送参数与证书名,运行时再用 `ChatClient.updatePushConfig(config)` 上报设备 Token。5.0.0 把推送配置拆成两部分:**初始化时配置 iOS 证书名**,**运行时绑定设备 Token**: + +| 4.x API | 5.0.0 API | 接口说明 | +| :--- | :--- | :--- | +| `ChatOptions.pushConfig`(`ChatPushConfig`) | 已删除。iOS 证书名改用 `ChatOptions.apnsCertName` / `ChatOptions.pushKitCertName`;Android 厂商凭据改用 `ChatClient.bindDeviceToken` 的 `notifierName` 参数 | 初始化配置。 | +| `ChatClient.updatePushConfig(config)` | `ChatClient.bindDeviceToken({ deviceToken, notifierName? })` | 绑定设备推送 Token;`notifierName` 仅 Android 使用。 | +| `ChatPushConfig.deviceToken` | `ChatClient.bindDeviceToken` 的 `deviceToken` | 设备 Token,取值来源不变(厂商推送 SDK 或 APNs 回调)。 | +| `ChatPushConfig.deviceId`(iOS 为 APNs 证书名,Android 为厂商推送凭据) | iOS:`ChatOptions.apnsCertName`;Android:`ChatClient.bindDeviceToken` 的 `notifierName` | 该字段两端语义不同,5.0.0 按平台拆分到不同入口。 | + +```typescript +// 初始化:iOS 证书名只能在 init 时下发 +await ChatClient.getInstance().init( + ChatOptions.withAppKey({ + appKey: appKey, + // 仅 iOS 生效,其他平台忽略 + apnsCertName: 'your_apns_cert_name', + }) +); + +// 运行时:绑定设备 Token +await ChatClient.getInstance().bindDeviceToken({ + deviceToken: deviceToken, // APNs 或厂商推送 SDK 返回的 token + // Android 必填的厂商推送凭据(下例为 FCM Sender ID),iOS 忽略 + notifierName: 'your_fcm_sender_id', +}); +``` + +:::warning +**Android 厂商推送的初始化责任转移到业务侧**:4.x 由 `ChatOptions.pushConfig` 的 `manufacturer` / `deviceId` 自动构建原生 `EMPushConfig`(`enableFCM` / `enableHWPush` / `enableOppoPush` 等);5.0.0 删除该入口后,厂商推送的依赖、manifest 配置与 `EMPushConfig` 初始化都需要在原生 Android 工程侧完成(可借助 `react-native-push-collection` 等推送插件),SDK 侧只负责用 `bindDeviceToken` 上报 Token。 +::: + +:::warning +**iOS 的证书名只能在初始化时设置,运行时不可修改**:`ChatOptions.apnsCertName` 与 `ChatOptions.pushKitCertName` 在 `ChatClient.init` 时下发给原生 `EMOptions`,原生在绑定 Token 时读取该属性。因此: + +- `bindDeviceToken` 的 `notifierName` 在 **iOS 上被忽略**(不再写入证书名)。仅依赖 4.x 写法(在 `pushConfig.deviceId` 或 `updatePushConfig` 里传证书名)的应用,升级后必须在 `ChatOptions.apnsCertName` 中配置,否则绑定失败(原生返回 `EMErrorUserIllegalArgument`,提示证书名为空); +- 证书名无法在运行中修改,需要变更只能重新初始化 SDK。 +::: + +`notifierName` 在 Android 上是**厂商推送凭据**,不是厂商标识串,传空或传错会直接导致推送不可用(原生返回参数非法错误): + +| 平台 / 厂商 | 取值 | 4.x 对应来源 | +| :--- | :--- | :--- | +| iOS | 忽略该参数,证书名见 `ChatOptions.apnsCertName` | `ChatPushConfig.deviceId` 或 `updatePushConfig` 的 `deviceId` | +| Android FCM | Sender ID | `ChatPushConfig.deviceId` | +| Android 小米 / 魅族 | App ID | `ChatPushConfig.deviceId` | +| Android OPPO | App Key | `ChatPushConfig.deviceId` | +| Android 华为 HMS / 荣耀 | App ID | 原生工程配置 | +| Android VIVO | `App ID` + `#` + `App Key` | 原生工程配置 | + +### PushKit(VoIP 推送) + +VoIP 推送使用独立的 PushKit 证书与 PushKit token,与上面的普通推送(APNs / 厂商推送)不是同一套配置:`bindDeviceToken` 只处理普通推送 token,VoIP 推送需要下面这几个 **iOS 专用**入口。 + +4.x 的 React Native SDK 未暴露 PushKit 能力(`ChatOptions` 与 `ChatClient` 都没有对应入口),5.0.0 新增: + +| 新增 API | 接口说明 | +| :--- | :--- | +| `ChatOptions.pushKitCertName` | 控制台配置的 **PushKit 证书名**(不是 APNs 证书名)。与 `apnsCertName` 一样,仅初始化时生效、运行时不可修改。 | +| `ChatClient.bindPushKitToken({ deviceToken })` | 绑定 PushKit token;`deviceToken` 传 `PKPushRegistry` 回调返回的十六进制 token。内部调用原生异步接口 `registerPushKitToken:completion:`。 | +| `ChatClient.unbindPushKitToken()` | 解绑 PushKit token;内部调用原生异步接口 `unRegisterPushKitTokenWithCompletion:`。`ChatClient.logout(unbindDeviceToken: true)` 已经会同时解绑,仅在用户保持登录状态、需要单独解绑时才调用。 | + +使用要点: + +- **仅 iOS 有效**:TypeScript 层在其他平台直接返回、不做任何处理,跨平台代码无需自行判断平台;Android 侧的原生 SDK 没有 PushKit API,对应原生路由只会返回「不支持」错误(正常调用路径不可达)。 +- PushKit token 需要业务在 iOS 工程侧自行实现 `PKPushRegistry`(`pushRegistry(_:didUpdate:for:)` 回调)拿到后传入;SDK 不负责申请 VoIP 推送权限,证书也需在控制台与原生工程侧配置完成。 +- 原生 SDK 会先缓存 token 再执行绑定:未登录时调用会失败(用户未登录),但 token 已缓存,登录成功后 SDK 会自动完成绑定并带退避重试。 +- 绑定失败最常见的原因是 `ChatOptions.pushKitCertName` 未配置或为空(原生返回 `EMErrorUserIllegalArgument`),其次是未登录(`EMErrorUserNotLogin`)。 + +```typescript +import { ChatClient, ChatOptions } from 'react-native-chat-sdk'; + +// iOS:初始化时配置 PushKit 证书名 +await ChatClient.getInstance().init( + ChatOptions.withAppKey({ + appKey: appKey, + apnsCertName: 'your_apns_cert_name', + pushKitCertName: 'your_pushkit_cert_name', + }) +); + +// 拿到 PKPushRegistry 回调的 token 后绑定;保持登录状态下可单独解绑 +await ChatClient.getInstance().bindPushKitToken({ + deviceToken: voipPushToken, +}); +await ChatClient.getInstance().unbindPushKitToken(); +``` + +## 其他删除的 API + +### 无客户端替代 + +| 所属类 | 删除的 API | 接口说明 | 迁移建议 | +| :--- | :--- | :--- | :--- | +| `ChatClient` | `createAccount(userId, password)` | 注册 IM 账号。 | 通过服务端 REST API 完成注册。 | +| `ChatManager` | `reportMessage(msgId, tag, reason)` | 举报消息。 | 将消息 ID、举报类型和原因提交至 App Server。 | +| `ChatGroupManager` | `fetchPublicGroupsFromServer(pageSize, cursor?)` | 分页获取服务端公开群组列表。 | 由业务服务维护可发现的群组目录。 | +| `ChatRoomManager` | `createChatRoom(subject, description?, welcome?, members?, maxCount)` | 创建聊天室。 | 通过服务端 REST API 创建聊天室。 | +| `ChatRoomManager` | `destroyChatRoom(roomId)` | 解散聊天室。 | 通过服务端 REST API 解散聊天室。 | +| `ChatPushConfig` | `ChatOptions.pushConfig` 不再序列化,承载厂商推送参数与证书名的 `ChatPushConfig` 类已删除。 | 初始化时配置厂商推送与证书名。 | iOS 证书名改由 `ChatOptions.apnsCertName` / `pushKitCertName` 承担,Android 厂商凭据改由 `ChatClient.bindDeviceToken` 的 `notifierName` 承担;Android 厂商推送初始化需在原生工程侧完成,详见[推送与设备 Token](#推送与设备-token)。 | + +### 有替代方式的 API + +| 4.x API | 5.0.0 API | 接口说明 | 迁移说明 | +| :--- | :--- | :--- | :--- | +| `ChatManager#modifyMessageBody(msgId, body)` | `ChatManager#modifyMsgBody({ msgId, body?, ext? })` | 修改本地和服务端消息。 | 位置参数改为单个 `params` 对象;`modifyMsgBody` 支持同时修改消息体与扩展信息,`modifyMessageBody` 已删除。 | +| `ChatManager#fetchHistoryMessages(convId, convType, { pageSize?, startMsgId?, direction? })` | `ChatManager#fetchHistoryMessagesByOptions(convId, convType, { options?, cursor?, pageSize? })` | 分页获取服务端历史消息。 | **分页锚点由 `startMsgId` 改为 `cursor`**:首次调用传空或省略,后续把上一页返回的 `ChatCursorResult.cursor` 传入。需要按发送方、消息类型或时间范围过滤时构造 `ChatFetchMessageOptions`(其 `startTs`、`endTs`、`direction`、`needSave` 为必填字段);`options` 整体可省略,`pageSize` 默认 20。 | +| `ChatManager#getMessagesWithMsgType(...)` | `ChatManager#getMsgsWithMsgType({ convId, convType, ... })` | 按消息类型从指定会话的本地数据库加载消息。 | 方法名由 `getMessages*` 调整为 `getMsgs*`。 | +| `ChatManager#getMessages(...)` | `ChatManager#getMsgs({ convId, convType, ... })` | 从指定会话的本地数据库加载消息。 | 同上。 | +| `ChatManager#getMessagesWithKeyword(convId, convType, keywords, ...)` | `ChatManager#getConvMsgsWithKeyword({ convId, convType, keywords, ... })` | 按关键字搜索指定会话的本地消息。 | 方法名由 `getMessagesWithKeyword` 调整为 `getConvMsgsWithKeyword`,参数改为单个 `params` 对象。 | +| `ChatManager#getMessageWithTimestamp(...)` | `ChatManager#getMsgWithTimestamp({ convId, convType, ... })` | 按时间戳从本地数据库获取消息。 | 同上。 | +| `ChatConversation#getMessagesWithMsgType(...)`、`getMessages(...)`、`getMessagesWithKeyword(...)`、`getMessageWithTimestamp(...)` | `ChatConversation#getMsgsWithMsgType(params)`、`getMsgs(params)`、`getMsgsWithKeyword(params)`、`getMsgWithTimestamp(params)` | `ChatConversation` 上的本地消息加载方法。 | 方法名同步由 `getMessages*` 调整为 `getMsgs*`,参数由位置参数改为单个 `params` 对象。 | +| `ChatManager#searchMsgFromDB(...)` | `ChatManager#getMsgsWithKeyword(params)` | 按关键字搜索本地消息。 | 使用保留的本地搜索接口;该方法的 `from?` 参数表示发送方,仍然可用。 | +| `ChatManager#getConvMsgsWithKeyword`、`ChatConversation#getMsgsWithKeyword` 的 `sender` 参数 | `senders: Array` 参数 | 按关键字搜索指定会话的本地消息,可按发送方过滤。 | 长期废弃的单个 `sender` 参数已删除,统一使用 `senders` 数组。 | +| `ChatRoomManager#joinChatRoom(roomId)` | `ChatRoomManager#joinChatRoomEx(params)` | 加入聊天室。 | 使用支持扩展信息的新接口。 | +| `ChatGroupManager#fetchGroupInfoFromServer(groupId, isFetchMembers?)` | `ChatGroupManager#fetchGroupInfoWithoutMembersFromServer(groupId)` | 获取群组详情。 | 新接口不返回成员列表。原先传 `isFetchMembers = true` 的场景,需要另外调用 `fetchMemberListFromServer(groupId, pageSize?, cursor?)`(成员 ID)或 `fetchMemberInfoListFromServer(groupId, cursor, limit?)`(含角色和入群时间)。 | +| `ChatImageMessageBody.thumbnailSecret` | `ChatImageMessageBody.secret` | 图片缩略图鉴权密钥。 | 属性改名。 | +| `ChatFetchMessageOptions.from` | `ChatFetchMessageOptions.senders` | 拉取历史消息时按发送方过滤。 | 属性改名并支持多个发送方。 | +| `ChatRoom.muteList` | `ChatRoom.muteKVList` | 聊天室禁言列表。 | 属性改名。 | +| `ChatGroupEventListener#onMemberJoined`、`onMemberExited` | `ChatGroupEventListener#onMembersJoined`、`onMembersExited` | 群成员加入或离开通知。 | 由单成员回调改为成员数组回调;单成员版本已删除。 | +| `ChatRoomEventListener#onMuteListAdded` | `ChatRoomEventListener#onMuteListAddedV2` | 聊天室禁言名单变化通知。 | 使用新回调;负载中的 `expireTime` 不再下发,`mutes` 由 `Array` 改为 `Record`(成员 ID → 禁言到期时间)。 | +| 公开的 `new ChatOptions(params)` 构造函数 | `ChatOptions.withAppKey(params)` 或 `ChatOptions.withAppId(params)` | 构造 SDK 初始化配置。 | 构造函数已改为私有,使用静态工厂方法。 | +| 事件常量 `onMessagesRecalled`、`onMessageReadAck`、`onMessageDeliveryAck`、`onMessageStatusChanged` | `onMessagesRecalledInfo` 等现有回调 | 消息撤回、已读回执、送达回执和消息状态变化事件。 | 这些常量属于桥接层内部实现(不对外导出),仅在使用本仓库源码编译时相关;已废弃的事件常量删除后,对应事件由 `onMessagesRecalledInfo` 等回调承载。 | +| `ChatClient#updatePushConfig(config)` | `ChatClient#bindDeviceToken({ deviceToken, notifierName? })` | 上报设备推送 Token。 | 方法改名并调整参数:原 `deviceId` 在 iOS 上是 APNs 证书名(改用初始化配置 `ChatOptions.apnsCertName`)、在 Android 上是厂商推送凭据(改用 `notifierName`);原 `deviceToken` 名称不变。详见[推送与设备 Token](#推送与设备-token)。 | + +## 主要新增 API + +| 所属类 | 新增 API | 接口说明 | +| :--- | :--- | :--- | +| `ChatOptions` | `dataSyncType`、`ChatDataSyncType` | 配置登录后自动同步会话、好友和已加入群组,可按位组合。 | +| `ChatConnectEventListener` | `onDatabaseOpened(username, errorCode)`、`onDataSyncStart(type)`、`onDataSyncFinish(type, errorCode)` | 监听本地数据库打开及自动数据同步的开始和结束。 | +| `ChatConnectEventListener` | `onDisconnected(errorCode?, info?)`、`ChatDisconnectErrorCode` | 统一的服务器断开回调,`errorCode` 表示断开原因。 | +| `ChatManager` | `sendMessageReadReceipts(msgIds)` | 批量发送单聊或群聊消息已读回执;最多 50 条且必须属于同一会话。 | +| `ChatManager` | `clearConversationUnreadMessageCount(convId)`、`clearAllConversationUnreadMessageCount()` | 清除指定会话或全部会话的本地未读数,并同步至当前账号其他设备,不向发送方发送消息已读回执。 | +| `ChatManager` | `getGroupMessageReadReceipts(msgIds)` | 批量查询群消息已读回执汇总;最多 20 条且必须属于同一会话。 | +| `ChatManager` | `fetchGroupMessageReadReceipts(msgId, groupId, cursor?, pageSize?)` | 分页查询指定群消息的已读成员详情。 | +| `ChatManager` | `deleteConversations(convIds, deleteMessages?)` | 批量删除本地会话,`deleteMessages` 默认为 `true`(同时删除会话中的本地历史消息);不存在的会话 ID 会被忽略。 | +| `ChatConversation` | `displayName?`、`displayAvatar?` | 获取会话展示名称和头像(只读);单聊返回对端用户昵称/头像,群聊返回群名称/群头像。相关数据未同步时可能为空字符串,此时可回退到既有的异步方法 `ChatConversation#name()`。 | +| `ChatCursorResult` | `totalCount?` | 分页结果的总条数;目前仅 `fetchGroupMessageReadReceipts` 在 iOS 侧返回,其余场景与 Android 为 `undefined`。 | +| `ChatGroupManager` | `updateGroupConfigs(groupId, types, configs)` | 按 `ChatGroupConfigsType` 指定的字段更新群组配置。 | +| `ChatMultiDeviceEvent` | `CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED`、`ALL_CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED`、`GROUP_UPDATE` | 通知其他设备清除了单个会话或全部会话的未读数,以及其他设备更新了群组信息。 | +| 模型 | `ChatMessageReadReceipt`、`ChatGroupReadReceipt`、`ChatGroupConfigs`、`ChatGroupConfigsType`、`ChatGroupMemberInfo`、`ChatDataSyncType`、`ChatDisconnectErrorCode` | 5.0.0 新增或替换的数据模型与枚举。 | +| `ChatOptions` | `apnsCertName`、`pushKitCertName` | iOS 专用:APNs 与 PushKit 推送证书名,仅初始化时生效、运行时不可修改;承接 4.x `ChatPushConfig.deviceId` 与推送证书配置。 | +| `ChatClient` | `bindDeviceToken({ deviceToken, notifierName? })` | 绑定设备推送 token(双端公用);取代已移除的 `ChatClient.updatePushConfig`,`notifierName` 仅在 Android 使用。 | +| `ChatClient` | `bindPushKitToken({ deviceToken })`、`unbindPushKitToken()` | 仅 iOS 有效;绑定和解绑 Apple PushKit(VoIP 推送)token,证书名由 `ChatOptions.pushKitCertName` 提供,详见[推送与设备 Token](#推送与设备-token)一节。 | + +## 监听器回调变化汇总 + +React Native SDK 的监听器回调都是可选方法(`?`)。删除的回调是否产生编译错误取决于实现方式:**类实现**(`class X implements ChatConnectEventListener`)保留旧回调不会报错,只会在运行时收不到事件;**对象字面量实现**会报 TS2353(对象字面量只能指定已知属性)。升级时应逐项检查监听器实现和监听器注册位置。 + +| 监听器 | 4.x 回调 | 5.0.0 回调 | 回调说明 | +| :--- | :--- | :--- | :--- | +| `ChatConnectEventListener` | 无 | `onDatabaseOpened(username, errorCode)` | 当前账号本地数据库打开完成;不代表业务数据同步完成。 | +| `ChatConnectEventListener` | 无 | `onDataSyncStart(type)`、`onDataSyncFinish(type, errorCode)` | 指定类型的数据同步开始和结束通知。 | +| `ChatConnectEventListener` | `onUserDidLoginFromOtherDevice(deviceName?)`、`onUserDidLoginFromOtherDeviceWithInfo(params)` | `onDisconnected(errorCode, info)` | 当前账号在其他设备登录的通知;`errorCode` 为 206,设备信息从 `info` 读取。 | +| `ChatConnectEventListener` | `onUserDidRemoveFromServer()`、`onUserDidForbidByServer()`、`onUserDidChangePassword()`、`onUserDidLoginTooManyDevice()`、`onUserKickedByOtherDevice()`、`onAppActiveNumberReachLimit()`、`onUserAuthenticationFailed()` | `onDisconnected(errorCode, info)` | 服务器强制下线回调统一收敛;按 `errorCode` 区分具体原因,详见[服务器强制下线回调收敛](#服务器强制下线回调收敛)。 | +| `ChatConnectEventListener` | 无参数的 `onDisconnected()` | `onDisconnected(errorCode?, info?)` | 回调签名扩展,携带断开原因和设备信息。原无参数实现不会编译报错(参数更少的实现兼容新签名),但拿不到原因,需要按需扩展签名。 | +| `ChatContactEventListener` | `onContactSyncStart()`、`onContactSyncFinish(error?)` | `ChatConnectEventListener#onDataSyncStart`、`onDataSyncFinish` | 好友同步回调统一迁移至连接监听器,并通过 `ChatDataSyncType.Contacts` 识别类型。 | +| `ChatMessageEventListener` | `onMessagesRead(messages)`、`onGroupMessageRead(groupMessageAcks)` | `onMessageReadReceipts(receipts)` | 统一接收单聊和群聊的消息已读回执。 | +| `ChatMessageEventListener` | `onConversationRead(from, to?)` | 无直接替代 | 会话级已读回执回调已删除;按消息处理 `onMessageReadReceipts`。 | +| `ChatGroupEventListener` | `onMemberJoined({ groupId, member })` | `onMembersJoined({ groupId, members })` | 由单个成员参数改为成员数组,一次可通知多名成员加入群组。 | +| `ChatGroupEventListener` | `onMemberExited({ groupId, member })` | `onMembersExited({ groupId, members })` | 由单个成员参数改为成员数组,一次可通知多名成员离开群组。 | +| `ChatRoomEventListener` | `onMuteListAdded({ roomId, mutes: Array, expireTime? })` | `onMuteListAddedV2({ roomId, mutes: Record })` | 聊天室禁言名单变化通知;`expireTime` 不再下发,`mutes` 改为成员 ID 到禁言到期时间的映射。 | + +`ChatGroupEventListener#onRequestToJoinDeclined` 的回调负载包含 `applicant`(入群申请被拒者的用户 ID)和 `decliner`(拒绝者的用户 ID)。其中 `decliner` 在 4.x 的接口中已声明,但 iOS 侧不下发(恒为 `undefined`),5.0.0 起双端都会填充;`applicant` 双端一直正常下发,语义不变。 + +### 回调负载修复 + +以下字段在 4.x 的 TypeScript 接口中已声明,但因为桥接层读取的键名与原生下发的键名不一致,运行时恒为 `undefined`;5.0.0 已修复,双端都会正常填充。业务代码如果为此写过兜底逻辑(例如另行拉取管理员信息),升级后可以移除: + +| 回调或模型 | 字段 | 说明 | +| :--- | :--- | :--- | +| `ChatGroupEventListener#onAdminAdded`、`onAdminRemoved` | `admin` | 被添加或移除的群管理员用户 ID。 | +| `ChatGroupEventListener#onAllGroupMemberMuteStateChanged`、`ChatRoomEventListener#onAllChatRoomMemberMuteStateChanged` | `isAllMuted` | 是否全员禁言。 | +| `ChatGroupEventListener#onRequestToJoinDeclined` | `decliner` | 拒绝入群申请的用户 ID;4.x 中 iOS 侧不下发该字段。 | +| `ChatRecalledMessageInfo` | `recalledConvId` | 被撤回消息所属的会话 ID。 | + +另外,Android 侧 `ChatManager#getConvMsgsWithKeyword` 省略 `senders` 参数时,4.x 会向原生传入包含空字符串的发送方列表,导致本地关键字搜索恒返回空结果;5.0.0 修复为「省略即不按发送方过滤」,搜索结果会从空变为正常命中。 + +## 行为变化 + +以下变化可能不会触发编译错误,但会影响业务逻辑: + +1. **未读消息总数的统计范围发生变化** + + `ChatManager#getUnreadCount` 获取本地单聊和群聊会话的未读消息总数。该方法在 4.x 中已存在且签名不变,本次**仅统计范围收窄**: + + - 不统计聊天室会话。 + - 不统计消息话题(Thread)的未读消息数。 + - 不统计设置了免打扰(推送通知方式为 `ChatPushRemindType.MENTION_ONLY` 或 `ChatPushRemindType.NONE`)的会话。这些会话即使存在未读消息,也不纳入统计。 + - 仅统计推送通知方式为 `ChatPushRemindType.ALL` 的单聊和群聊会话。 + + 如果业务需要其他统计口径,应遍历 `getAllConversations` 并根据会话类型和 `remindType` 自行累加各会话的 `getUnreadCount()` 结果。 + +2. **清除未读数不会发送消息已读回执** + + `clearConversationUnreadMessageCount` 只清除指定会话的本地未读数,并将结果同步至当前账号的其他设备,不会向消息发送方发送已读回执。如需通知发送方,还需对消息调用 `sendMessageReadReceipts`。 + +3. **初始化后不再自动登录** + + `ChatClient.init` 完成后,SDK 不会依据历史登录记录自动登录。应用需要安全保存和更新 Token,并在适当时机主动调用 `loginWithToken`。 + +4. **消息已读状态不再允许应用直接修改** + + `ChatMessage.isRead`、`isPeerRead` 和 `groupReadReceiptCount` 在 5.0.0 中均为只读属性,`ChatConversation` 的逐条和全量标记已读接口也已删除。业务应使用会话未读数清理接口;消息已读回执则通过独立的批量回执接口发送。`ChatManager#getMessage` 只查询消息,不会改变其已读状态。 + +5. **已读回执由「全局默认开启」变为「按消息显式开启」** + + 4.x 的 `ChatOptions#requireAck` 默认为 `true`,发送的消息默认都会请求已读回执;5.0.0 删除了该全局开关,改为按消息设置 `ChatMessage.isNeedReadReceipt`,且默认为 `false`。升级后如果没有在发送前显式设置,将不再收到对端已读回执,`isPeerRead` 与 `groupReadReceiptCount` 也不会更新。 + +6. **默认群类型发生变化** + + 4.x 的 `ChatGroupOptions` 未指定 `style` 时默认为 `PublicJoinNeedApproval`(公开群 + 入群需审批);5.0.0 的 `ChatGroupConfigs` 三个布尔属性默认全为 `false`,等价于私有群且仅群主可邀请。依赖默认值建群的代码需要显式设置 `isPublic`、`joinApprovalRequired`、`allowInvites`。 + +7. **自动同步完成前,本地列表可能不完整** + + `onDatabaseOpened` 只表示数据库可以访问。若 `dataSyncType` 包含相应类型,需要等待 `onDataSyncFinish` 返回成功(`errorCode === 0`)后,再将会话、好友或已加入群组的本地查询结果视为本次登录后的最新数据。 + +8. **新增多设备未读数同步事件** + + 当前账号在其他设备清除会话未读数时,本端会通过 `onConversationEvent(event, convId, convType)` 收到事件。收到 `CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED` 或 `ALL_CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED` 后,应重新读取本地会话并刷新 UI。 + +9. **需显式配置数据同步范围以保证双端行为一致** + + `dataSyncType` 未设置时,iOS 默认同步会话,Android 默认不同步任何数据。依赖自动同步的业务应显式设置 `dataSyncType`。 + +10. **部分回调字段从恒为 `undefined` 变为有值** + + `onAdminAdded`/`onAdminRemoved` 的 `admin`、`onAllGroupMemberMuteStateChanged`/`onAllChatRoomMemberMuteStateChanged` 的 `isAllMuted`、`onRequestToJoinDeclined` 的 `decliner`(iOS)和 `ChatRecalledMessageInfo.recalledConvId` 已修复,详见[回调负载修复](#回调负载修复)。Android 侧 `getConvMsgsWithKeyword` 省略 `senders` 时的搜索结果也从恒空变为正常命中。 + +11. **iOS 集成方式变化需要重新安装依赖** + + 使用 SPM 集成时,`pod install` 需要显式传入 `USE_FRAMEWORKS=dynamic`,且首次安装需要联网下载 SPM 包。 + +12. **Android 侧新增原生传递依赖** + + 原生 SDK 5.0.0 引入 `androidx.core:core:1.12.0`、`androidx.annotation:annotation:1.6.0` 和 `androidx.lifecycle:lifecycle-process:2.6.2`(前后台检测改用 `ProcessLifecycleOwner`)。宿主工程 `compileSdk` 过低,或锁定、排除了 androidx 版本时,需要相应调整。 + +13. **Android 厂商推送初始化改由原生工程承担** + + `ChatOptions.pushConfig` 删除后,SDK 不再构建原生 `EMPushConfig`(`enableFCM` / `enableHWPush` / `enableOppoPush` 等)。厂商推送的依赖、manifest 配置与初始化必须在原生 Android 工程侧完成,否则 `bindDeviceToken` 上报的 Token 不会生效。 + +14. **iOS 推送证书名只能在初始化时配置** + + `ChatOptions.apnsCertName` / `pushKitCertName` 只在 `ChatClient.init` 时下发给原生,运行中无法修改;`bindDeviceToken` 的 `notifierName` 在 iOS 上也不生效。升级后若只依赖旧写法(`pushConfig.deviceId` 或 `updatePushConfig` 里传证书名),绑定会失败并返回证书名为空的非法参数错误。 + +15. **PushKit 接口仅 iOS 有效** + + `ChatClient.bindPushKitToken` / `unbindPushKitToken` 在 TypeScript 层对非 iOS 平台是空实现(Android 原生 SDK 没有 PushKit 能力,其原生路由返回「不支持」错误);VoIP 推送的证书申请、`PKPushRegistry` 注册与 token 获取都由业务侧 iOS 工程承担,SDK 只负责在初始化时下发证书名(`ChatOptions.pushKitCertName`)并绑定运行时拿到的 token。 + +## 4.x 到 5.0.0 迁移检查清单 + +建议按以下顺序执行升级并逐项验证: + +- [ ] **依赖与工程**:将 `react-native-chat-sdk` 升级到 `5.0.0`;iOS 侧执行 `pod install`(使用 SPM 时加 `USE_FRAMEWORKS=dynamic`),Android 侧重新执行 Gradle 构建。 +- [ ] **登录改造**:删除 `createAccount`、`login`、`loginWithAgoraToken`、`isLoginBefore` 调用,改为服务端下发 Token 后调用 `loginWithToken`;将 `renewAgoraToken` 替换为 `renewToken`。 +- [ ] **初始化配置**:移除 `autoLogin`、`requireAck`、`enableAutoSyncContacts` 配置;按需设置 `dataSyncType`;确认 `ChatOptions` 通过 `withAppKey` 或 `withAppId` 构造。 +- [ ] **数据同步**:移除会话、好友、已加入群组的服务端拉取调用,改为读取本地接口,并在 `onDataSyncFinish` 成功后刷新 UI;将 `onContactSyncStart`/`onContactSyncFinish` 迁移到连接监听器。 +- [ ] **连接事件**:将 8 类强制下线回调统一为 `onDisconnected(errorCode, info)`;按“退出原因/连接原因”区分处理,并同时处理 `onTokenDidExpire`。 +- [ ] **已读回执**:给需要回执的消息在发送前设置 `isNeedReadReceipt = true`(4.x 的 `requireAck` 默认开启,5.0.0 默认不开启);将逐条已读回执改为 `sendMessageReadReceipts`;将清除未读数改为 `clearConversationUnreadMessageCount`/`clearAllConversationUnreadMessageCount`;将 `onMessagesRead`/`onGroupMessageRead`/`onConversationRead` 迁移到 `onMessageReadReceipts`;替换 `hasReadAck`、`needGroupAck`、`groupAckCount` 属性引用,移除对 `hasRead` 的赋值(已改名为只读的 `isRead`),并把 `groupReadCount` getter 改为 `groupReadReceiptCount`;将 `fetchGroupAcks` 改为 `fetchGroupMessageReadReceipts`,同时把 `ChatGroupMessageAck` 的 `msg_id`/`ack_id`/`from`(字符串)改为 `ChatGroupReadReceipt` 的 `msgId`/`ackId`/`from.memberId`。 +- [ ] **群组**:将建群参数从 `ChatGroupOptions`/`ChatGroupStyle` 迁移到 `ChatGroupConfigs`,并**显式设置** `isPublic`/`joinApprovalRequired`/`allowInvites`(默认群类型已变化);将 `ChatGroup.options` 读取改为 `ChatGroup.configs`、`group.options.isDisabled` 改为 `group.isDisabled`;按需接入 `updateGroupConfigs`(仅群主和管理员);将 `fetchGroupInfoFromServer(groupId, true)` 拆为 `fetchGroupInfoWithoutMembersFromServer` + `fetchMemberListFromServer`/`fetchMemberInfoListFromServer`;移除 `fetchJoinedGroupsFromServer`、`fetchPublicGroupsFromServer` 调用。 +- [ ] **本地消息接口**:将 `getMessages*`、`searchMsgFromDB`、`modifyMessageBody` 等调用替换为 `getMsgs*`、`getMsgsWithKeyword`、`modifyMsgBody`;将 `fetchHistoryMessages` 替换为 `fetchHistoryMessagesByOptions`,分页锚点由 `startMsgId` 改为 `cursor`。 +- [ ] **其他重命名**:`joinChatRoom` → `joinChatRoomEx`;`ChatImageMessageBody.thumbnailSecret` → `secret`;`ChatFetchMessageOptions.from` → `senders`;`ChatRoom.muteList` → `muteKVList`;`onMemberJoined`/`onMemberExited` → `onMembersJoined`/`onMembersExited`(单成员改为数组);`onMuteListAdded` → `onMuteListAddedV2`(`mutes` 改为 `Record`)。 +- [ ] **设备管理**:设备管理接口改为 Token 鉴权,Token 由业务服务器下发;删除多余的 `isPassword` 实参。 +- [ ] **推送配置**:将 `ChatClient.updatePushConfig` 改为 `ChatClient.bindDeviceToken`;删除 `ChatOptions.pushConfig` 与 `ChatPushConfig` 引用;确认 `notifierName` 仅在 Android 使用、值为厂商推送凭据。 +- [ ] **iOS 证书名**:已在 `ChatOptions` 初始化时配置 `apnsCertName`(需要 VoIP 推送时还有 `pushKitCertName`),且未依赖运行期修改证书名。 +- [ ] **Android 厂商推送**:厂商推送依赖、manifest 与 `EMPushConfig` 已在原生工程侧配置完成(SDK 不再负责)。 +- [ ] **VoIP 推送**:需要 VoIP 推送时,iOS 侧已实现 `PKPushRegistry` 并改用 `ChatClient.bindPushKitToken({ deviceToken })`(与普通推送 token 相互独立,其他平台调用为空实现)。 +- [ ] **Token 生命周期**:接入 `onTokenWillExpire`(申请新 Token 后调 `renewToken`)与 `onTokenDidExpire`(重新登录),并在冷启动时主动 `loginWithToken`。 +- [ ] **构建与验证**:iOS 与 Android 分别重新编译(Android 注意新增的 androidx 传递依赖能否正常解析);重点验证 Token 登录、数据同步、会话未读数、单聊和群聊已读回执、群组创建与配置更新、好友列表、附件消息、推送设置和多端事件。 diff --git a/docs/deprecated-api-leftover-audit.md b/docs/deprecated-api-leftover-audit.md new file mode 100644 index 00000000..3a41975a --- /dev/null +++ b/docs/deprecated-api-leftover-audit.md @@ -0,0 +1,151 @@ +# Deprecated API Leftover Audit (src + native wiring) + +Date: 2026-09-21 +Scope: `react-native-chat-sdk` 5.0.0 worktree — `src/` (public API, internal consts, events, +models) plus the corresponding leftover wiring in `modules/java`, `modules/objc`, `modules/cpp`. +Example app (`example/src`, `example/ci`) and `docs/` were cross-checked for references. + +## Method + +1. Searched `src/` for deprecation markers (`@deprecated`, `// deprecated`, `作废/废弃`). +2. Cross-checked the 2026-08-26 audit (`docs/deprecated-api-ts-annotation-audit.md`) item by item + against current `src/`. +3. Scanned all 278 `MT*` consts in `src/__internal__/Consts.ts` for references anywhere else in + `src/` (symbol and string value), then checked each unreferenced const against + `modules/java`, `modules/objc`, `modules/cpp`, `example/`, and `docs/`. +4. Used `git log -S` to date the disappearance of each TS-side reference. + +## Headline result + +The **public TS API surface is clean**: no `@deprecated` JSDoc tags remain, no deprecated public +method, property, or event survives in `src/`, and every item flagged by the 2026-08-26 audit was +removed during the 5.0.0 cleanup. What remains is **dead internal plumbing**: 23 method-name +constants whose TS APIs are gone, still wired through the Android/iOS/C++ layers, plus one +commented-out model field. + +## A. Explicitly marked `// deprecated` — 9 method consts not cleaned + +All nine carry a `// deprecated ` marker in `src/__internal__/Consts.ts` but still exist +there, and none is referenced anywhere else in `src/`. The contract-test parser +(`src/__tests__/contract/parsers.ts`) strips `// deprecated` lines, which is why these pass CI +despite being dead. + +| Method | Marked | TS leftover | Android leftover (Java) | iOS leftover (ObjC) | C++ leftover | +|---|---|---|---|---|---| +| `updateCurrentUserNick` | 2026-05-21 | `Consts.ts:11` | const + dispatch case | none | const | +| `syncConversationName` | 2022.05.05 | `Consts.ts:132` | const + dispatch case | key/value/methodMap + dispatch case | const | +| `setNoDisturbUsers` | 2022.05.04 | `Consts.ts:263` | const + dispatch case | key/value/methodMap | none | +| `getNoDisturbUsersFromServer` | 2022.05.04 | `Consts.ts:264` | const + dispatch case | key/value/methodMap | none | +| `updateHMSPushToken` | 2026-05-21 | `Consts.ts:266` | const + dispatch case + wrapper (`ExtSdkPushManagerWrapper.updateHMSPushToken`) | none (Android-only) | const | +| `updateFCMPushToken` | 2026-05-21 | `Consts.ts:267` | const + dispatch case + wrapper (`ExtSdkPushManagerWrapper.updateFCMPushToken`) | none (Android-only) | const | +| `updateAPNsPushToken` | 2026-05-21 | `Consts.ts:268` | none (iOS-only) | key/value/methodMap + dispatch case; wrapper body already commented out (`ExtSdkClientWrapper.m:261`) | none | +| `imPushNoDisturb` | 2022.05.04 | `Consts.ts:270` | const + dispatch case | key/value/methodMap + dispatch case | const | +| `getNoDisturbGroups` | 2022.05.04 | `Consts.ts:273` | const + dispatch case | key/value/methodMap + dispatch case | const | + +Java locations: `modules/java/com/chatsdk/common/ExtSdkMethodType.java` (consts), +`modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java` (cases). ObjC locations: +`modules/objc/common/ExtSdkMethodTypeObjc.h/.m` (keys, values, methodMap), +`modules/objc/dispatch/ExtSdkDispatch.m` (cases), `modules/objc/rn/ExtSdkApiObjcRN.mm` +(supported-method list). C++ locations: `modules/cpp/common/ExtSdkMethodType.h/.cpp`. + +## B. Unmarked but equally dead — 14 method consts from 2022-era TS removals + +These have **no** deprecation marker, yet no reference (symbol or string value) exists in `src/` +outside `Consts.ts`, nor in `example/` or `docs/`. `git log -S` dates the TS-side disappearance +to 2022 (API renames and the 3.9.x cleanup). They are the same kind of leftover as +`MTgetAllChatRooms`, which was deliberately scrubbed in 5.0.0 — these were missed. + +| Method | TS reference gone | Android leftover | iOS leftover | C++ leftover | +|---|---|---|---|---| +| `getGroupsWithoutPushNotification` | 2022-04-25 | const + dispatch + wrapper (`ExtSdkGroupManagerWrapper`) | key/value/methodMap + dispatch | const | +| `uploadLog` | 2022-04-25 | const + dispatch + wrapper (`ExtSdkClientWrapper`) | key/value/methodMap + dispatch + wrapper (`ExtSdkClientWrapper`) | const | +| `loadMsgWithId` | 2022-07-19 (api rename) | const + dispatch + wrapper (`ExtSdkConversationWrapper`) | key/value/methodMap + dispatch + wrapper (`ExtSdkConversationWrapper`) | const | +| `appendMessage` | 2022-07-20 | const + dispatch + wrapper (`ExtSdkConversationWrapper`) | wrapper string remnant only (`ExtSdkConversationWrapper.m`) | const | +| `fetchChatRoomAllAttributes` | 2022-09-28 (3.9.6.2 upgrade) | const + dispatch + wrapper (`ExtSdkChatRoomManagerWrapper`) | dispatch + wrapper (`ExtSdkChatroomManagerWrapper.h/.m`) | none | +| `enableOfflinePush` | 2022-06-16 | const + dispatch + wrapper (`ExtSdkPushManagerWrapper`) | wrapper string remnant only (`ExtSdkPushManagerWrapper.m`) | none | +| `disableOfflinePush` | 2022-06-16 | const + dispatch + wrapper (`ExtSdkPushManagerWrapper`) | wrapper string remnant only (`ExtSdkPushManagerWrapper.m`) | none | +| `getNoPushGroups` | 2022-06-16 | const + dispatch + wrapper | key/value/methodMap + dispatch + wrapper | none | +| `updateGroupPushService` | 2022-06-16 | const + dispatch + wrapper | key/value/methodMap + dispatch + wrapper | const | +| `updateUserPushService` | 2022-06-16 | const + dispatch + wrapper | key/value/methodMap + dispatch + wrapper | none | +| `getNoPushUsers` | 2022-06-16 | const + dispatch + wrapper | key/value/methodMap + dispatch + wrapper | none | +| `reportPushAction` | 2022-07-15 | const + dispatch + wrapper | key/value/methodMap + dispatch + wrapper | none | +| `updateOwnUserInfoWithType` | 2022-04-25 | const + dispatch + wrapper (`ExtSdkUserInfoManagerWrapper`) | key/value/methodMap + dispatch + wrapper | const | +| `fetchUserInfoByIdWithType` | 2022-04-25 | const + dispatch + wrapper (`ExtSdkUserInfoManagerWrapper`) | key/value/methodMap + dispatch + wrapper | const | + +Note: `getGroupsWithoutPushNotification` was already called out as leftover dead code in the +2026-08-26 audit; the native SDKs deprecated/removed it long ago (iOS `EM_DEPRECATED_IOS(3_3_2, +3_8_3)`), and the iOS wrapper was deleted in 5.0.0 — but the ObjC key/value/methodMap/dispatch +entries and the full Android chain remain. + +## C. Deprecated property leftover + +- ~~`src/common/ChatMessageStreamChunk.ts:53` — commented-out field + `// sequenceNumber: number; // deprecated` inside the `ChatStreamChunk` interface.~~ + **Resolved 2026-09-21**: the commented line was removed. + +No other deprecated properties survive: `ChatFetchMessageOptions.from` (→ `senders`), +`ChatRoom.muteList` (→ `muteKVList`), and `ChatImageMessageBody.thumbnailSecret` +(→ `ChatFileMessageBody.secret`) are all gone from `src/`. + +## Verified clean (for the record) + +Everything the 2026-08-26 audit listed as "annotated in TS" was removed in the 5.0.0 cleanup — +none of the following exists in `src/` anymore: + +- Methods: `ChatClient.login` (password), `ChatClient.loginWithAgoraToken`, + `ChatManager.fetchAllConversations`, `fetchConversationsFromServerWithPage`, + `fetchHistoryMessages`, `modifyMessage`, `getMessagesWithKeyword` (`sender` param), + `ChatRoomManager.getAllChatRooms` (never public in TS; const scrubbed in 5.0.0). +- Events: group `onMemberJoined`/`onMemberExited` (→ `onMembersJoined`/`onMembersExited`), + chat-room `onMuteListAdded` (→ `onMuteListAddedV2`), `onMessagesRecalled`, + `onMessageReadAck`/`onMessageDeliveryAck`, `onMessageStatusChanged`. + `UNWIRED_EVENT_ALLOWLIST` in `src/__tests__/contract/eventWiring.test.ts` is empty. +- Properties: see section C. + +The surviving lookalikes are **not** deprecated: group `ChatGroupEventListener.onMuteListAdded` +(`ChatEvents.ts:961`), chat-room `onMemberJoined`/`onMemberExited` (`ChatEvents.ts:1220/1232`), +`ChatGroup.muteList`, and `ChatVideoMessageBody.thumbnailSecret` (the Android SDK deprecates the +*image* body's thumbnail secret only, not the video body's). + +## Cleanup recommendation + +~~**Decision (2026-09-21): the method-name contract keys are intentionally retained**~~ +**Superseded 2026-09-23: the cleanup was executed.** All section A/B method-name contracts +were removed across every layer — `src/__internal__/Consts.ts` (23 consts, including +`MTgetImPushConfig`, which was missed by this audit), `modules/cpp/common/ExtSdkMethodType.h/.cpp`, +`modules/java` (consts + dispatch cases + wrapper methods), and `modules/objc` (keys, values, +methodMap, dispatch cases, the RN supported-method list, wrapper methods). The sweep additionally +removed native-only leftovers not covered by sections A/B: `modifyMessage`, `ignoreGroupPush`, +and the ObjC-only dead entries `currentUser` (deprecated alias of `getCurrentUser`), +`updateConversationsName`, and `fetchChatThread`. Verified with `yarn typecheck`, `yarn lint`, +and `yarn test --no-watchman` (19 suites / 116 tests, contract tests included). Known issue left +untouched: ObjC `renewToken` has a wrapper implementation and a methodMap entry but no dispatch +case in `ExtSdkDispatch.m`, so it falls through to "not implement" — a functional gap reported +separately, not part of this cleanup. The inventory below is kept as historical record. + +
+Original removal plan (superseded by the decision above) + +No user-facing breakage is involved — none of the 23 methods is reachable from the RN side, so +removal is internal dead-code deletion only: + +1. `src/__internal__/Consts.ts`: delete the 23 `MT*` consts (9 marked + 14 unmarked). +2. `modules/cpp/common/ExtSdkMethodType.h/.cpp`: delete the matching string constants. +3. `modules/java/com/chatsdk/common/ExtSdkMethodType.java`: delete the consts; + `modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java`: delete the dispatch cases; + delete the wrapper methods listed in sections A/B. +4. `modules/objc/common/ExtSdkMethodTypeObjc.h/.m`: delete keys, int values, and methodMap + entries; `modules/objc/dispatch/ExtSdkDispatch.m`: delete cases; + `modules/objc/rn/ExtSdkApiObjcRN.mm`: remove from the supported-method list; delete the + wrapper methods / string remnants listed in sections A/B (including the commented-out + `asyncBindDeviceToken` body in `ExtSdkClientWrapper.m`). +5. `src/common/ChatMessageStreamChunk.ts`: delete the commented `sequenceNumber` line. +6. Re-run `yarn typecheck`, `yarn lint`, `yarn test --no-watchman`. The contract tests strip + `// deprecated` lines, so deleting those consts cannot regress them; the event-wiring test is + unaffected (no `MTon*` involved). + +Precedent: the 5.0.0 `getAllChatRooms` scrub did exactly this six-file ObjC + three-file Java + +C++ + TS sweep (see `docs/porting/5.0.0/03-implementation.md`), so the pattern and the +verification path are already proven in this repo. + +
diff --git a/docs/porting/1.21.0/01-api-diff.md b/docs/porting/1.21.0/01-api-diff.md deleted file mode 100644 index dc154e47..00000000 --- a/docs/porting/1.21.0/01-api-diff.md +++ /dev/null @@ -1,43 +0,0 @@ -# 01 - 公开 API Diff:native 4.24.1 → 4.25.0(React Native 平版) - -- 源仓库 iOS:`/Users/asterisk/Codes/zuoyu_native/emclient-ios`,`refs/tags/4.24.1..refs/tags/4.25.0` -- 源仓库 Android:`/Users/asterisk/Codes/zuoyu_native/emclient-android`,`refs/tags/SDK_4.24.1..refs/tags/SDK_4.25.0` -- diff 范围:iOS `newSDK/HyphenateSDK/` 公开头文件(排除 `*+Internal.h`);Android `hyphenatechatsdk/src/com/hyphenate/` 公开类(`adapter/`、`core/` 除外) -- 版本区间核对:`git tag` 列表中 4.24.1 → 4.25.0 之间无中间小版本 tag,单跳覆盖,无跳版 -- 目标平台:react-native(`react-native-chat-sdk`,worktree `.worktree/1.21.0`,分支 `1.21.0` 基于 `dev`) - -## iOS 变更清单 - -| id | 类型 | 领域 | iOS 源签名 | 决策 | 状态 | -|---|---|---|---|---|---| -| `chat_get_conversations_from_db_with_cursor` | new_api | conversation | `IEMChatManager.h`:`- (void)getConversationsFromDBWithCursor:(nullable NSString *)cursor pageSize:(NSInteger)pageSize completion:(void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock` | include | pending | -| `options_enable_chatroom_conversation` | new_option | client | `EMOptions.h`:`@property(nonatomic) BOOL enableChatroomConversation;`(默认 NO) | include | pending | -| `options_use_agora_chat_domain` | new_option | client | `EMOptions.h`:`@property (nonatomic) BOOL useAgoraChatDomain;`(裸属性,无文档注释) | **skip**(KI-107:native 对内品牌开关,不面向终端用户;Flutter 4.25.0 平版评审已确认不移植) | implemented(无代码) | -| `options_sync_data_ws_removed` | removed | client | `EMOptions+PrivateDeploy.h`:删除 `syncDataWSHost` / `syncDataWSPort` 两个私有部署属性 | skip(RN 从未暴露,全仓 grep `syncData` 无匹配,无操作) | implemented(无代码) | -| `userinfo_fetch_subscribed_gender_fix` | bugfix | userinfo | `EMUserInfoManager.mm`:`fetchSubscribedUsers` 补 `gender` 赋值并修复漏 `addObject` | skip(实现层 bugfix,非公开 API 变更;RN 侧 `ChatUserInfo` 已有 `gender` 字段,反序列化不受影响) | implemented(无代码) | -| `client_token_corrected_time` | internal | client | `EMClient.mm`:token 过期计算改用 `getCorrectedTimestampMs`(NTP 校正时间) | skip(内部实现) | implemented(无代码) | - -## Android 变更清单 - -| id | 类型 | 领域 | Android 源签名 | 决策 | 状态 | -|---|---|---|---|---|---| -| `chat_async_get_conversations_from_db` | new_api | conversation | `EMChatManager.java`:`public void asyncGetConversationsFromDB(final String cursor, final int pageSize, final EMValueCallBack> callback)` | include(与 iOS `chat_get_conversations_from_db_with_cursor` 同一条) | pending | -| `options_enable_chatroom_conversation` | new_option | client | `EMOptions.java`:`setEnableChatroomConversation(boolean)` / `isEnableChatroomConversation()`(默认 false) | include | pending | -| `options_use_agora_chat_domain` | new_option | client | `EMOptions.java`:`setUseAgoraChatDomain(boolean)` / `getUseAgoraChatDomain()` / `@hide isUseAgoraChatDomainConfigured()`(`Boolean` 三态) | **skip**(KI-107,同上) | implemented(无代码) | -| `options_sync_data_ws_removed` | removed | client | `EMOptions.java`:删除 `setSyncDataWebSocketServer` / `getSyncDataWebSocketServer` / `setSyncDataWebSocketPort` / `getSyncDataWebSocketPort` 及私有字段 | skip(RN 从未暴露,无操作) | implemented(无代码) | -| `search_keyword_limit_doc` | doc_only | chat | `EMMessageSearchOption.setKeywordList` 注释:关键词长度上限 512→120、总长 1024→120 | skip(仅注释变化;RN `ChatMessageSearchOption.ts` 注释未写长度上限,无操作) | implemented(无代码) | -| `client_token_corrected_time` | internal | client | `EMClient.java` / `EMSessionManager.java`:token 过期计算改用 `getCorrectedTimestampMs` | skip(内部实现) | implemented(无代码) | -| `cloud_timeout_unit_fix` | internal | cloud | `HttpClientConfig.getTimeout` 秒→毫秒换算内移 | skip(内部实现,`cloud/` 非公开 diff 范围,仅顺带确认无公开签名变化) | implemented(无代码) | - -## 未匹配清单 - -| id | 疑点 | 处理 | -|---|---|---| -| UM-1 | `useAgoraChatDomain` 双端形态不一致(iOS 裸 `BOOL` 无文档 vs Android `Boolean` 三态 + `@hide`) | **已闭环**:KI-107 / Flutter 4.25.0 平版评审确认——native 对内品牌开关,跨平台 SDK 不暴露(国内 easemob 不设置走缺省,海外 agora 侧内部固定 true)。RN 侧零代码。 | -| UM-2 | 新 API `getConversationsFromDBWithCursor` 的前置条件(iOS `EMOptions.autoLoadConversations=NO` / Android `setAutoLoadAllConversations(false)`)在 RN 基线无入口(`src/` 全仓 grep `autoLoad` 无匹配) | 沿用 Flutter 4.25.0 平版评审结论(UM-2 保留):**衍生新增 `ChatOptions.autoLoadConversations`**(默认 true,与 native 一致),与新 API 一并平版;文档强调开启自动加载时分页加载失去意义。native 双端 4.24.1 基线已具备该选项,不构成对 4.25.0 新能力的额外依赖。 | -| UM-3 | Android `asyncGetConversationsFromDB` 结果过滤了 chatThread 会话(`conv.isChatThread()` 跳过),iOS 端实现未见对应过滤(diff 只到公开头文件,实现未读) | 记入验收报告,交用户确认是否需对齐;RN 平版照双端各自公开语义透传,不擅自抹平。 | - -## 备注 - -- Flutter 4.25.0 平版(`/Users/asterisk/Codes/zuoyu_flutter/im_flutter_sdk/.worktree/4.25.0`,`docs/porting/4.25.0/`)已完成并评审闭环,本次 RN 平版沿用其全部评审结论(`useAgoraChatDomain` 不移植、`autoLoadConversations` 衍生新增保留、公开方法名 `fetchConversationsFromDB`)。 -- 目标平台公开方法名沿用 Flutter 侧与 RN 既有命名习惯(如 `fetchPinnedConversationsFromServerWithCursor`):TS 公开方法 `fetchConversationsFromDB`,方法名 key `getConversationsFromDBWithCursor`。 diff --git a/docs/porting/1.21.0/02-contract.md b/docs/porting/1.21.0/02-contract.md deleted file mode 100644 index 5749afda..00000000 --- a/docs/porting/1.21.0/02-contract.md +++ /dev/null @@ -1,196 +0,0 @@ -# 02 - 基线调查与跨端契约:React Native 1.21.0 - -基线:`react-native-chat-sdk` worktree `.worktree/1.21.0`,分支 `1.21.0`(基于 `dev` @ 3d8f454),开工前 `git status` 干净。 -目标平台版本号:**1.21.0**(用户指定,与 native 版本号无关;当前包版本 1.20.0)。 -先例:Flutter 4.25.0 平版已评审闭环(`/Users/asterisk/Codes/zuoyu_flutter/im_flutter_sdk/.worktree/4.25.0/docs/porting/4.25.0/`),本契约沿用其全部评审结论。 - -## 基线调查结论(关键事实) - -- 桥接:单一 TurboModule `callMethod(method, args)`;TS → `ExtSdkApiRN`/`ExtSdkApiObjcRN` → Dispatch switch → 各 Wrapper。 -- 模板 API:`fetchPinnedConversationsFromServerWithCursor`(key `getPinnedConversationsFromServerWithCursor`)全链路齐备,新 API 照此模式。 -- `ChatCursorResult` 模型已存在(`src/common/ChatCursorResult.ts`),native 序列化 `{cursor, list}`(Java `ExtSdkCursorResultHelper.toJson` / ObjC `EMCursorResult toJsonObject` category)。 -- `ChatOptions` 每个布尔字段 5 处同步:字段声明、构造 params、构造赋值、`withAppId` params、`withAppKey` params(`src/common/ChatOptions.ts`)。 -- Options native 链路:Java `ExtSdkHelper.java` fromJson(`if (json.has(...))` 单行,按版本注释分组)/ toJson;ObjC `ExtSdkToJson.m` EMOptions (Json) category fromJson/toJson。 -- 无 native options 读回 API(`getOptions` 不存在),toJson 仅用于对称完整性。 -- cpp 常量层(`modules/cpp/common/ExtSdkMethodType.{h,cpp}`)为字符串透传子集,历史新 key 未同步(KI-105);本次按仓库 AGENTS.md 规范补齐新 key。 -- example ApiCall 注册表:`example/src/registry/apis/chat.ts`(模板:`fetchConversationsFromServerWithCursor` 条目)。 -- auto 模式 init options 白名单:`example/src/auto/auto_mode.ts` `CHAT_OPTIONS_KEYS`(:157-198)。 - -## 已知问题清单核对(references/known-issues.md) - -- **KI-107 命中**:`useAgoraChatDomain` 不移植(对内品牌开关),本契约 C 节无此项,实施者不得添加。 -- KI-105 命中:cpp 常量滞后问题已知,本次新 key 按规范补齐。 -- KI-001(505 服务端功能未开通):本次新 API 均为本地能力,不依赖服务端开通,不命中。 -- 其余条目为 Flutter/旧版本特定,不命中。 - -## Hooks 安装(阶段二门禁项) - -本 harness(Kimi Code)无 Codex 式 hooks 机制,按 skill「钩子」节第 3 档(流程强制)执行: - -- `gate` 列为每阶段门禁必跑命令,输出原样附进各阶段产物。 -- `pre`(提交拦截)无机械替代:本任务用户已明确"不提交",全程不执行 `git add`/`git commit`。 - -阶段二 gate 输出(2026-09-10,实施前基线): - -```json -{"decision": "block", "reason": "平版门禁检查未过(修复后再收尾;确认为合理差异时,说明理由后可结束):key 在 TS(Consts) 有、Java(ExtSdkMethodType) 缺失: updateAPNsPushToken ;key 在 TS(Consts) 有、ObjC(ExtSdkMethodTypeObjc) 缺失: getNoDisturbUsersFromServer onMessageStatusChanged updateCurrentUserNick updateFCMPushToken updateHMSPushToken ;"} -``` - -基线 block 项核查:6 个 key 全部在 `Consts.ts` 标注 `// deprecated`(`updateCurrentUserNick` :13、`onMessageStatusChanged` :148、`getNoDisturbUsersFromServer` :298、`updateHMSPushToken` :300、`updateFCMPushToken` :301、`updateAPNsPushToken` :302),契约测试经 `stripDeprecated` 剔除故全绿;gate 脚本未剔除 deprecated 属脚本覆盖范围差异。**判定为合理差异**,非本次平版引入,不处理。 - -## native 依赖 bump(阶段二第一步,已完成) - -| 位置 | 旧 | 新 | -|---|---|---| -| `android/build.gradle:91` | `io.hyphenate:hyphenate-chat:4.24.1` | `4.25.0` | -| `ChatSdk.podspec:53`(SPM `minimumVersion`) | `4.24.1` | `4.25.0` | -| `ChatSdk.podspec:57`(CocoaPods `s.dependency`) | `~> 4.24.1` | `~> 4.25.0` | - -注:4.25.0 尚未发布到 maven/CocoaPods,编译验证走本地依赖覆盖(`scripts/rn-local-deps.sh`,改动不提交);上述远程依赖声明是交付形态,正常保留。 - -## 契约(冻结) - -### C1 新 API:`fetchConversationsFromDB`(本地数据库分页获取会话) - -- 方法名 key(三端逐字一致):**`getConversationsFromDBWithCursor`** -- TS(`src/ChatManager.ts`,紧邻 `fetchPinnedConversationsFromServerWithCursor` 之后): - - 签名:`public async fetchConversationsFromDB(cursor?: string, pageSize?: number): Promise>` - - 包装:`Native._callMethod(MTgetConversationsFromDBWithCursor, { [MTgetConversationsFromDBWithCursor]: { cursor: cursor ?? '', pageSize: pageSize ?? 20 } })` - - 返回:`r[MTgetConversationsFromDBWithCursor]` → `new ChatCursorResult({ cursor, list, opt: { map: (param) => new ChatConversation(param) } })` - - 常量:`src/__internal__/Consts.ts` 加 `MTgetConversationsFromDBWithCursor = 'getConversationsFromDBWithCursor'`(放会话分组) - - TypeDoc 英文注释必须含:前置条件(调用前需将 `ChatOptions.autoLoadConversations` 设为 `false`;开启自动加载时 SDK 初始化即全量加载会话,分页加载失去意义)、排序规则(置顶倒序 > 最新消息服务器时间倒序 > 会话 ID 倒序不区分大小写)、pageSize [1,100]、无效 cursor 抛 `ChatError`(INVALID_PARAM) -- Android: - - `ExtSdkMethodType.java`:加常量 `getConversationsFromDBWithCursor` - - `ExtSdkDispatch.java`:switch 加 case → `ExtSdkChatManagerWrapper.getInstance().getConversationsFromDBWithCursor(jsonParams, methodType, callback)` - - `ExtSdkChatManagerWrapper.java`:`String cursor = param.optString("cursor"); int pageSize = param.optInt("pageSize");` 调 **`asyncGetConversationsFromDB(cursor, pageSize, callback)`——注意 native 参数顺序是 (cursor, pageSize),与 pinned API 的 (pageSize, cursor) 相反**;回调 `ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(...))` / `onError` -- iOS: - - `ExtSdkMethodTypeObjc.h`:加 key 常量 + `...Value` 枚举值(取现有最大 Value +1,实施时 grep 确认无撞号) - - `ExtSdkMethodTypeObjc.m`:methodMap 加映射 - - `ExtSdkDispatch.m`:switch 加 case - - `ExtSdkChatManagerWrapper.h/.m`:声明+实现;`NSString *cursor = param[@"cursor"]; NSInteger pageSize = [param[@"pageSize"] integerValue];` 调 `getConversationsFromDBWithCursor:cursor pageSize:pageSize completion:`;completion 中 `onResult:withMethodType:withError:withParams:[ret toJsonObject]` -- cpp:`modules/cpp/common/ExtSdkMethodType.h/.cpp` 同步加 key(仓库 AGENTS.md 规范;KI-105 背景) -- 返回包装(双端一致):`{getConversationsFromDBWithCursor: {cursor: string, list: [...]}}` -- 无新事件。 - -### C2 新配置:`ChatOptions.enableChatroomConversation`(设置获取本地会话列表时是否包含聊天室会话) - -- TS 字段 `enableChatroomConversation: boolean`,默认 `false`;5 处同步(声明/构造 params/构造赋值/withAppId params/withAppKey params) -- JSON key:**`enableChatroomConversation`** -- Android:`ExtSdkHelper.java` fromJson 加 `if (json.has("enableChatroomConversation")) { options.setEnableChatroomConversation(json.getBoolean("enableChatroomConversation")); }`;toJson 加 `data.put("enableChatroomConversation", options.isEnableChatroomConversation());` -- iOS:`ExtSdkToJson.m` fromJson 加 `if (aJson[@"enableChatroomConversation"]) { options.enableChatroomConversation = [aJson[@"enableChatroomConversation"] boolValue]; }`;toJson 加 `data[@"enableChatroomConversation"] = @(self.enableChatroomConversation);` -- 版本分组注释:`// 2026-09-10 4.25.0`(Java/ObjC 两侧) - -### C3 衍生新配置:`ChatOptions.autoLoadConversations`(UM-2,Flutter 评审已确认保留) - -- 背景:C1 前置条件要求关闭自动加载,RN 基线无入口(grep `autoLoad` 无匹配);native 双端 4.24.1 已具备(Android `setAutoLoadAllConversations` / iOS `EMOptions.autoLoadConversations`) -- TS 字段 `autoLoadConversations: boolean`,默认 **`true`**(与 native 默认一致);5 处同步 -- JSON key:**`autoLoadConversations`** -- Android:fromJson `if (json.has("autoLoadConversations")) { options.setAutoLoadAllConversations(json.getBoolean("autoLoadConversations")); }`;toJson `data.put("autoLoadConversations", options.isAutoLoadAllConversations());` -- iOS:fromJson `if (aJson[@"autoLoadConversations"]) { options.autoLoadConversations = [aJson[@"autoLoadConversations"] boolValue]; }`;toJson `data[@"autoLoadConversations"] = @(self.autoLoadConversations);` -- 注释强调:使用 `fetchConversationsFromDB` 前须设为 `false` 及原因 -- 版本分组注释同 C2 - -### C4 明确不做(防盗版) - -- `useAgoraChatDomain`:**不移植**(KI-107,对内品牌开关;国内 easemob 不设置走缺省,海外 agora 侧内部固定 true)。任何层不得出现该 key。 -- `syncDataWSHost`/`syncDataWSPort`(iOS)、`set/getSyncDataWebSocketServer/Port`(Android)删除:RN 从未暴露,无操作。 -- `search_keyword_limit_doc`(关键词上限注释 512→120):RN 注释未写上限,无操作。 - -### C5 收尾项(主 agent 负责,不拆分) - -- 版本号:`package.json` `version` → `1.21.0`;`yarn gen:version_file` 重新生成 `src/version.ts` -- example 三处:`example/package.json` version → `1.21.0`;`example/android/app/build.gradle` versionName → `1.21.0`、versionCode 2→3;`project.pbxproj` MARKETING_VERSION → `1.21.0`(Debug/Release 两处)、CURRENT_PROJECT_VERSION 2→3(两处) -- CHANGELOG 双语新增 `## 1.21.0`:native 升级句(沿用 "Dependent native SDKs are upgraded to versions (iOS 4.25.0 and Android 4.25.0)")+ `fetchConversationsFromDB` + `ChatOptions` 两个新配置 -- 仓库 `AGENTS.md` 滞后条目顺手更新:`HyphenateChat 4.18.1` → `4.25.0`;`example/src/demo2/` 引用已失效(demo2 已删)按现状修正 -- example 注册表:`example/src/registry/apis/chat.ts` 加 `ChatManager.fetchConversationsFromDB` 条目(照 `fetchConversationsFromServerWithCursor` 模板) -- `example/src/auto/auto_mode.ts` `CHAT_OPTIONS_KEYS` 加 `'enableChatroomConversation'`、`'autoLoadConversations'`(脚本驱动验证需要注入 `autoLoadConversations:false`) - -## 遗留疑点(进验收报告) - -- UM-3:Android `asyncGetConversationsFromDB` 过滤 chatThread 会话,iOS 公开头文件未见对应说明;RN 照双端各自语义透传,不抹平,交用户确认。 -- gate 脚本对 deprecated key 的误报(6 个存量 key),属脚本与契约测试口径差异,建议后续修正脚本(非本次范围)。 - ---- - -# 补录(2026-09-24):iOS PushKit 回移的跨端契约 - -对应 `06-pushkit-backport.md`。**只加不删**:`pushConfig` / `ChatPushConfig` / `updatePushConfig` 原样保留,`bindDeviceToken` 不引入。 - -## 阶段二门禁 - -### 基线核查(开工前) - -- 分支 `1.21.0`,基线 commit `7acbd07`;`git status` 干净(仅本文件为新增未跟踪)。 -- native 依赖已就位,无需 bump,各处互核一致:`ChatSdk.podspec:53` SPM `minimumVersion: '4.25.0'`、`:57` CocoaPods `~> 4.25.0`、`android/build.gradle:91` `io.hyphenate:hyphenate-chat:4.25.1`。 -- 基线测试:`yarn test --no-watchman` → **19 suites / 121 tests 全通过**(本 worktree 首次安装依赖 `yarn install`,exit 0)。 - -### gate 输出(无 hooks 能力 harness,流程强制替代机器强制) - -命令:`echo '{}' | bash /Users/asterisk/Codes/zuoyu_rn/.agents/skills/platform-sdk-porting-v2/hooks/porting_guard.sh gate react-native /Users/asterisk/Codes/zuoyu_rn/react-native-chat-sdk/.worktree/1.21.0` - -输出:**(无输出,空)** = 门禁通过,无 block 项。 - -> 与 `02-contract.md` 原有记录中「6 个 deprecated key 误报」的差异说明:本次 gate 无输出。原记录中的 block 项来自阶段二实施前的旧基线(当时脚本口径不同,且 `RenewToken` 等 key 尚未补齐),现基线已无该现象,gate 干净。 - -## C6 iOS PushKit 回移要素(逐项定死) - -### C6.1 初始化选项:`ChatOptions.apnsCertName` / `ChatOptions.pushKitCertName` - -- TS 字段:`apnsCertName?: string`、`pushKitCertName?: string`,**可选、无默认值**(与 native `NSString *` 可为 nil 对齐;与 5.0.0 逐字一致) -- 放置位置:`src/common/ChatOptions.ts` 中 `isAutoDownload` 与 `pushConfig` 之间(5.0.0 同一位置) -- 5 处同步(本仓库既有规矩):字段声明、构造 params、构造赋值、`withAppId` params、`withAppKey` params -- JSON key:**`apnsCertName`**、**`pushKitCertName`**(顶层,与 5.0.0 及 Flutter 一致) -- iOS:`ExtSdkToJson.m` 的 `EMOptions (Json)` `fromJson` 增加 - `if (aJson[@"apnsCertName"]) { options.apnsCertName = aJson[@"apnsCertName"]; }`、 - `if (aJson[@"pushKitCertName"]) { options.pushKitCertName = aJson[@"pushKitCertName"]; }`; - `toJson` 增加 `data[@"apnsCertName"] = self.apnsCertName;`、`data[@"pushKitCertName"] = self.pushKitCertName;`(对称完整性) -- Android:**不做映射**(native Android 无该能力)。`ExtSdkHelper.java` 不动。 -- 版本分组注释:iOS 的 `fromJson`/`toJson` 加 `// 2026-09-24 1.21.0` 风格注释(沿用文件现有分组注释习惯) -- 注释语义(英文 TypeDoc):仅 iOS 生效;仅在 `ChatClient.init` 时设置,运行时不修改 - -### C6.2 方法:`bindPushKitToken` / `unbindPushKitToken` - -| 要素 | 取值 | -|---|---| -| TS 公开签名 | `bindPushKitToken(params: { deviceToken: string }): Promise`;`unbindPushKitToken(): Promise` | -| 方法名 key | `bindPushKitToken` / `unbindPushKitToken`(逐字,五处同步:`Consts.ts`、`ExtSdkMethodType.java`、`ExtSdkMethodTypeObjc.h`、`ExtSdkMethodTypeObjc.m`、`ExtSdkMethodType.{h,cpp}`) | -| 请求参数 key | `deviceToken`(string) | -| TS 请求包装 | `Native._callMethod(MTbindPushKitToken, { [MTbindPushKitToken]: { deviceToken } })`;解绑 `Native._callMethod(MTunbindPushKitToken)` 无内层参数 | -| 返回结构 | `onResult` 包装 `{bindPushKitToken: nil}`,TS `ChatClient.checkErrorFromResult(r)` | -| 非 iOS 行为 | TS `Platform.OS !== 'ios'` → 直接 `return`(静默 no-op);`ChatClient.ts` 需从 `react-native` 引入 `Platform`(现为 `import { type EventSubscription, NativeEventEmitter }`) | -| 位置 | `src/ChatClient.ts`,紧跟 `updatePushConfig` 之后 | - -### C6.3 iOS 侧 - -| 层 | 动作 | -|---|---| -| `ExtSdkMethodTypeObjc.h` | 在 `ExtSdkMethodKeyUpdatePushConfig` 附近加两个 key 常量;enum 加 `BindPushKitTokenValue = 1027`、`UnbindPushKitTokenValue = 1028`(1.21.0 现有 max = `ExtSdkMethodKeyGetPushTemplateValue = 1026`,不冲突) | -| `ExtSdkMethodTypeObjc.m` | methodMap 加两条映射 | -| `ExtSdkDispatch.m` | `updatePushConfig` case 后加两个 case | -| `ExtSdkClientWrapper.h` | `updatePushConfig` 声明后加两个方法声明 | -| `ExtSdkClientWrapper.m` | 实现:`deviceToken` 十六进制字符串 → `NSData`(与 5.0.0 同法)→ `[EMClient.sharedClient registerPushKitToken:...completion:]` / `unRegisterPushKitTokenWithCompletion:`,completion 里 `onResult:withMethodType:aChannelName withError:aError withParams:nil` | - -### C6.4 Android 侧(仅注册路由,不实现能力) - -| 层 | 动作 | -|---|---| -| `ExtSdkMethodType.java` | 加两个常量 | -| `ExtSdkDispatch.java` | `updatePushConfig` case 后加两个 case | -| `ExtSdkClientWrapper.java` | 两个方法返回 `ExtSdkWrapper.onError(result, EMError.OPERATION_UNSUPPORTED, "PushKit is only supported on iOS")`(`EMError.OPERATION_UNSUPPORTED = 111`,已核实 native 4.25.1 `hyphenatechatsdk/src/com/hyphenate/EMError.java:220`) | -| `ExtSdkHelper.java` | 不动 | - -理由:契约测试强制「TS `MT*` 值 ⊆ Java 常量值」;Android 注册同名 key 才能在两端保持方法名契约一致。实际调用被 TS 的 `Platform.OS` 守卫拦住。 - -### C6.5 明确不做 - -- **不删除** `ChatOptions.pushConfig` / `src/common/ChatPushConfig.ts` / `ChatClient.updatePushConfig`(兼容性硬要求)。 -- **不新增** `ChatClient.bindDeviceToken`(属 5.0.0 的 break 动作,不回移)。 -- **不引入** 证书名优先级逻辑:`updatePushConfig` 的 iOS 旧路径(运行时写 `apnsCertName`)原样保留。 -- **不移植** native 同步变体 `bindPushKitToken:` / `unBindPushKitToken`(语义重复且阻塞线程)。 -- **不修** iOS `renewToken` dispatch 缺失(D-2,待用户裁决)。 - -### C6.6 收尾项(主 agent 负责) - -- example:`example/src/auto/auto_mode.ts` 的 `CHAT_OPTIONS_KEYS` 加 `'apnsCertName'`、`'pushKitCertName'`;新建 `example/src/registry/apis/client.ts` 注册 `ChatClient.bindPushKitToken`、`ChatClient.unbindPushKitToken`,并在 `example/src/registry/index.ts` 接入 `clientApis`。 -- CHANGELOG 双语:`## 1.21.0` 段落追加两条(两个 `ChatOptions` 属性 + 两个 `ChatClient` 方法),中文版同步。 -- 版本号:`package.json` 已是 `1.21.0`,example 三处已是 1.21.0 —— **不动**(本次是补充项,非新版本发布)。 diff --git a/docs/porting/1.21.0/03-implementation.md b/docs/porting/1.21.0/03-implementation.md deleted file mode 100644 index e9ef65a2..00000000 --- a/docs/porting/1.21.0/03-implementation.md +++ /dev/null @@ -1,121 +0,0 @@ -# 03 - 实现记录:React Native 1.21.0 - -实施方式:契约(02-contract.md)冻结后,三个实现 subagent 并行(TS 层 / Android wrapper+cpp / iOS wrapper),版本号与 CHANGELOG 由主 agent 统一收尾。subagent 与主 agent 使用同一模型(harness 同进程实例,满足同级别模型要求)。全程无 git add/commit。 - -## 变更清单状态 - -| id | 决策 | 状态 | -|---|---|---| -| `chat_get_conversations_from_db_with_cursor` / `chat_async_get_conversations_from_db` | include | implemented | -| `options_enable_chatroom_conversation` | include | implemented | -| `options_auto_load_conversations`(UM-2 衍生) | include | implemented | -| `options_use_agora_chat_domain` | skip(KI-107) | implemented(无代码,全仓 grep 零残留) | -| `options_sync_data_ws_removed` / `search_keyword_limit_doc` / `userinfo_fetch_subscribed_gender_fix` / `client_token_corrected_time` / `cloud_timeout_unit_fix` | skip(无 RN 操作) | implemented(无代码) | - -## 改动明细 - -### TypeScript(subagent-1,`yarn typecheck` / `yarn lint` 通过) - -- `src/__internal__/Consts.ts:120-121`:`MTgetConversationsFromDBWithCursor`(按 prettier 折行,与同文件长常量格式一致)。 -- `src/ChatManager.ts`::42 import;:3517-3556 新增 `fetchConversationsFromDB(cursor?, pageSize?)`,照 `fetchPinnedConversationsFromServerWithCursor` 模式;TypeDoc 含前置条件(`autoLoadConversations=false` 及原因)、排序规则、pageSize [1,100]、无效 cursor 抛 `ChatError`(INVALID_PARAM)。 -- `src/common/ChatOptions.ts`:`enableChatroomConversation`(默认 false)与 `autoLoadConversations`(默认 true)各 5 处同步(声明 :281-296 / 构造 params :352-353 / 赋值 :407-409 / withAppId :452-453 / withAppKey :501-502)。 -- `example/src/registry/apis/chat.ts:47-58`:ApiCall 注册表新增 `ChatManager.fetchConversationsFromDB` 条目。 -- `example/src/auto/auto_mode.ts:198-199`:`CHAT_OPTIONS_KEYS` 追加 `'enableChatroomConversation'`、`'autoLoadConversations'`。 - -### Android wrapper + cpp(subagent-2) - -- `modules/java/com/chatsdk/common/ExtSdkMethodType.java:107`:常量 `getConversationsFromDBWithCursor`。 -- `modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java:860-862`:case 分发。 -- `modules/java/com/chatsdk/dispatch/ExtSdkChatManagerWrapper.java:1075-1092`:`getConversationsFromDBWithCursor`,调 `asyncGetConversationsFromDB(cursor, pageSize, callback)`(**参数顺序 (cursor, pageSize) 已经 native `SDK_4.25.0` 源码核实**,与 pinned API 相反);`ExtSdkCursorResultHelper.toJson` 序列化。 -- `modules/java/com/chatsdk/dispatch/ExtSdkHelper.java`:fromJson :373-375(`// 2026-09-10 4.25.0` 分组)+ toJson :422-423;setter/getter 名(`setEnableChatroomConversation`/`isEnableChatroomConversation`、`setAutoLoadAllConversations`/`isAutoLoadAllConversations`)已经 native 源码核实。 -- `modules/cpp/common/ExtSdkMethodType.h:72` / `.cpp:66`:补常量(KI-105 背景,仓库规范要求)。 - -### iOS wrapper(subagent-3) - -- `modules/objc/common/ExtSdkMethodTypeObjc.h:116`(key 常量)+ `:517`(Value 枚举 **546**)。 -- `modules/objc/common/ExtSdkMethodTypeObjc.m:342`:methodMap 映射。 -- `modules/objc/dispatch/ExtSdkDispatch.m:1023-1027`:case 分发。 -- `modules/objc/dispatch/ExtSdkChatManagerWrapper.h:174-176` 声明 + `.m:1009-1024` 实现:`getConversationsFromDBWithCursor:pageSize:completion:`(pageSize `NSInteger`,用 `integerValue`,已经 native `refs/tags/4.25.0` 头文件核实)。 -- `modules/objc/dispatch/ExtSdkToJson.m`:toJson :1274-1276 + fromJson :1336-1338(`// 2026-09-10 4.25.0` 分组),`enableChatroomConversation` / `autoLoadConversations` 属性名已经 native 头文件核实。 - -### 版本与文档收尾(主 agent) - -- native 依赖 bump(阶段二已完成):`android/build.gradle:91` → 4.25.0;`ChatSdk.podspec:53`(SPM)与 `:57`(CocoaPods)→ 4.25.0。 -- `package.json` version → 1.21.0;`yarn gen:version_file` 重新生成 `src/version.ts`(1.21.0)。 -- example 三处:`example/package.json` → 1.21.0;`example/android/app/build.gradle` versionName 1.21.0 / versionCode 3;`project.pbxproj` MARKETING_VERSION 1.21.0(:270/:297)/ CURRENT_PROJECT_VERSION 3(:262/:290)。 -- `CHANGELOG.md` / `CHANGELOG.zh.md`:新增 `## 1.21.0`(native 升级句 + `fetchConversationsFromDB` + 两个新 `ChatOptions` 属性)。 -- 仓库 `AGENTS.md` 滞后条目更新:`HyphenateChat 4.18.1` → 4.25.0;`example/src/demo2/` 两处失效引用改为 API tester/registry 现状描述。 - -## ⚠️/❌ 汇总 - -- ⚠️ iOS Value 枚举编号取 **546** 而非契约字面"全文件 max+1":该文件按 section 分段编号(ChatManager 段 5xx,全文件 max 2029 属其他段),段内已用最大 545,取段内续号 546,全文件 grep 无撞号。符合文件实际惯例,予以采纳。 -- ⚠️ TS 常量按 prettier 折行(契约文本为单行),语义等同。 -- 无 ❌ 项。 - -## 遗留 - -- UM-3(Android 过滤 chatThread 会话,iOS 未见对应说明)照双端各自语义透传,进验收报告待用户确认。 -- example 双端构建 + 契约要素 grep 抽查在阶段四执行。 - ---- - -# 补录(2026-09-24):iOS PushKit 回移实现 - -对应 `06-pushkit-backport.md` 与 `02-contract.md` 的「补录」节。契约冻结后实施,**只加不删**。 - -## TS 层(`src/`) - -| 文件 | 改动 | -|---|---| -| `src/__internal__/Consts.ts:24-25` | 新增 `MTbindPushKitToken = 'bindPushKitToken'`、`MTunbindPushKitToken = 'unbindPushKitToken'`,置于 `MTrenewToken` 之后(ChatClient methods 段) | -| `src/common/ChatOptions.ts` | 新增 `apnsCertName?: string`(:128)、`pushKitCertName?: string`(:134),置于 `isAutoDownload` 与 `pushConfig` 之间;5 处同步 = 字段声明 + 构造 params(:340-341)+ 构造赋值(:393-394)+ `withAppId` params(:444-445)+ `withAppKey` params(:495-496);注释为英文 TypeDoc,明确「仅 iOS」「仅初始化可设置」 | -| `src/ChatClient.ts` | `react-native` 导入增加 `Platform`(:3);常量导入增加两个 key(:50-51);`updatePushConfig` 之后新增 `bindPushKitToken({ deviceToken })` 与 `unbindPushKitToken()`(:962-1010 区域),实现与 5.0.0 逐字一致(`Platform.OS !== 'ios'` 提前 return) | -| `src/ChatClient.ts`(注释) | `updatePushConfig` 的 TypeDoc 增加一条 **Note**:该方法保留向后兼容;iOS 上 `ChatPushConfig.deviceId` 是运行时写入的 APNs 证书名,会覆盖初始化时设置的 `ChatOptions.apnsCertName`,两处只配一处。行为零变化,仅信息告知(D-1 裁决的落地方式) | - -## iOS wrapper(`modules/objc/`) - -| 文件 | 改动 | -|---|---| -| `common/ExtSdkMethodTypeObjc.h` | 两个 key 常量加在 `ExtSdkMethodKeyRenewToken` 后(:22-24,带 `// 2026-09-24 1.21.0` 分组注释);两个 Value 常量加在文件末尾 1026 之后:`ExtSdkMethodKeyBindPushKitTokenValue = 1027`、`ExtSdkMethodKeyUnbindPushKitTokenValue = 1028` | -| `common/ExtSdkMethodTypeObjc.m` | methodMap 在 `ExtSdkMethodKeyRenewToken` 后加两条映射 | -| `dispatch/ExtSdkDispatch.m` | `ExtSdkMethodKeyUpdatePushConfigValue` case 后加两个 dispatch case | -| `dispatch/ExtSdkClientWrapper.h` | `updatePushConfig` 声明后加两个方法声明 | -| `dispatch/ExtSdkClientWrapper.m` | 实现两个方法:`bindPushKitToken` 取 `param[@"deviceToken"]`,以 `(NSData *)` 转型传给 `registerPushKitToken:completion:`;`unbindPushKitToken` 调 `unRegisterPushKitTokenWithCompletion:`。两者 completion 均走 `onResult:withMethodType:withError:withParams:nil`。证书名**只读初始化选项**,不从请求取 | -| `dispatch/ExtSdkToJson.m` | `EMOptions (Json)` fromJson 在 `pushConfig` 映射后加两行(`apnsCertName`、`pushKitCertName`,均带 `length > 0` 判空);toJson 在 `data[@"pushConfig"]` 后加两行对称回写 | - -## Android wrapper(`modules/java/`) - -| 文件 | 改动 | -|---|---| -| `common/ExtSdkMethodType.java` | 两个常量加在 `renewToken` 后(:23-25) | -| `dispatch/ExtSdkDispatch.java` | `updatePushConfig` case 后加两个 case(带注释说明「iOS-only,注册路由以保持跨端 key 契约一致,TS 侧由 `Platform.OS` 守卫」) | -| `dispatch/ExtSdkClientWrapper.java` | 新增 `import com.hyphenate.EMError`;新增两个方法返回 `ExtSdkWrapper.onError(result, EMError.OPERATION_UNSUPPORTED, "PushKit is only supported on iOS")` | - -## cpp 透传层(`modules/cpp/common/`) - -- `ExtSdkMethodType.h` / `.cpp`:两个常量加在 `getCurrentUser` 后。该层整体滞后(连 `isConnected` / `updatePushConfig` 都没有,KI-105),按仓库 AGENTS.md 规范补齐新 key 即可,不作为契约对齐依据。 - -## example(`example/`) - -| 文件 | 改动 | -|---|---| -| `src/auto/auto_mode.ts` | `CHAT_OPTIONS_KEYS` 追加 `'apnsCertName'`、`'pushKitCertName'`(脚本驱动可注入证书名) | -| `src/registry/apis/client.ts` | **新建**:注册 `ChatClient.bindPushKitToken`(参数模板 `{ deviceToken }`)与 `ChatClient.unbindPushKitToken`(无参数) | -| `src/registry/index.ts` | 导入并展开 `clientApis`,`ALL_APIS` 中排在 `chatApis` 之后 | - -## 文档收尾 - -- `CHANGELOG.md` / `CHANGELOG.zh.md`:`## 1.21.0` 段追加两条(两个 `ChatOptions` 属性 + 两个 `ChatClient` 方法),中英同步。 -- 版本号**未改**:`package.json` 与 example 三处已是 `1.21.0`,本次是补充项而非新版本发布。 - -## ⚠️/❌ 汇总 - -- ⚠️ ObjC `Value` 编号取 **1027/1028**(接在 push 段已用的最大号 1026 之后)。该文件按功能段分段编号(client 段 1xx、chat 段 5xx、userinfo 段 11xx、push 段 10xx),接 push 段续号与本仓库 5.0.0 的取号方式(119/120 接在 client 段末尾)不同但都属段内续号,全文件 grep 无撞号。 -- ⚠️ `updatePushConfig` 的 TypeDoc 新增一条 Note(行为未变,仅文档);若用户认为 1.21.0 的注释也应完全不动,可回退这一条。 -- 无 ❌ 项。 - -## 遗留 - -- D-1(证书名两处配置的取舍说明)已在 `updatePushConfig` 注释与 `06-pushkit-backport.md` UM-3 落地,行为保持旧路径优先,待用户确认表述。 -- D-2(iOS `renewToken` dispatch 缺失)**本次未修**,进验收报告待用户裁决。 -- 阶段四验证与双端构建在 `04-verification.md` 记录。 diff --git a/docs/porting/1.21.0/04-verification.md b/docs/porting/1.21.0/04-verification.md deleted file mode 100644 index da831fcb..00000000 --- a/docs/porting/1.21.0/04-verification.md +++ /dev/null @@ -1,296 +0,0 @@ -# 04 - 验证记录:React Native 1.21.0 - -验证日期:2026-09-10。环境:worktree `.worktree/1.21.0`(分支 1.21.0)。native 4.25.0 未发布到 maven/CocoaPods,example 构建验证走本地依赖覆盖(`rn-local-deps.sh apply`,验证后已 restore 且零残留)。 - -## 验证结果总表 - -| 验证项 | 结果 | 失败原因/备注 | -|---|---|---| -| `yarn typecheck` | ✅ | | -| `yarn lint` | ✅ | 含 prettier | -| `yarn check:circular:dpdm` | ✅ | 1 条存量 warning(skip `react-native/index.js`),基线即有 | -| `yarn test --no-watchman`(unit+contract) | ✅ | 19 suites / 121 tests 全过;契约测试强制 TS↔Java↔ObjC 方法名 parity,新 key 通过 | -| `yarn prepare`(bob build + gen 文件) | ✅ | `lib/` 正常产出 | -| example 构建 Android(`yarn example build:android`,本地依赖) | ✅ | BUILD SUCCESSFUL 1m15s;aab 含 `libhyphenate.so`(24B 占位 stub,官网包既有行为,见 references)+ `libaosl.so`(arm64-v8a,脚本单 ABI 构建配置) | -| example 构建 iOS(`yarn example build:ios`,本地依赖) | ✅ | 首次失败:bundler 找不到 Gemfile.lock 钉住的 gems(环境问题);`bundle install` 后重试成功 | -| iOS 本地依赖实证 | ✅ | `pod install` 后 Podfile.lock 中 HyphenateChat 消失、ShengwangInfra_iOS(1.3.5) 出现;`.app/Frameworks/` 内本地 `HyphenateChat.framework`(CFBundleShortVersionString=4.25.0)+ `aosl.framework` | -| 契约要素 grep 逐字抽查 | ✅ | 见下节 | -| API 脚本实机回归(fetchConversationsFromDB) | ✅ | 双端各 5 步全过(2026-09-10,真实 appKey/账号,本地依赖+真 so)。脚本:init(`autoLoadConversations:false` 注入)→ login(zuoyu01)→ sendMessage×2(造会话)→ page1(pageSize 1)→ page2(`$step.page1.cursor` 翻页)→ 无效 cursor。Android(emulator-5554):全过,page 返回 `{cursor, list}` 结构正确,无效 cursor 返回 110 `invalid cursor for getConversationsFromDB`;iOS(iPhone 16 Pro 模拟器):全过,实际翻页取到不同会话(page1=zuoyu02、page2=zuoyu01,返回真实游标 `eyJpc3RvcCI6...`),无效 cursor 同样 110。日志 `/tmp/rn_425_android.log`、`/tmp/rn_425_ios.log`;脚本 `/tmp/rn_425_convdb.json`(内联路径 Android `/data/local/tmp/rn_425_convdb.json`、iOS 同 host 路径,jsbundle strings 实证未中 metro 缓存毒化) | - -实机回归备注: - -- Android 运行前需把官网包 24B 占位 `libhyphenate.so` 替换为 `easemob-sdk-symbolLibs-4.25.0.zip` 内真 so(4 ABI 全替换),否则运行时不可用(编译不受影响)。 -- 首跑 login 报 200 "The user is already logged in"(模拟器残留登录态,auto 模式重试不覆盖该码),卸载应用清数据后通过;同 Flutter KI 系经验。 -- 观察(非缺陷):自发消息建会话在双端时序不同——iOS page1 时自建会话尚未落库(page2 才出现),Android 回归窗口内未见自建会话;不影响分页机制验证。 - -## 契约要素 grep 实证 - -方法名 key `getConversationsFromDBWithCursor`: - -| 位置 | 证据 | -|---|---| -| TS `src/__internal__/Consts.ts:120-121` | `MTgetConversationsFromDBWithCursor = 'getConversationsFromDBWithCursor'` | -| Java `ExtSdkMethodType.java:107` | `"getConversationsFromDBWithCursor"` | -| ObjC `ExtSdkMethodTypeObjc.h:116` | key 常量;`:517` Value=546 | -| ObjC `ExtSdkMethodTypeObjc.m:342` | methodMap 映射 key→546 | -| ObjC `ExtSdkDispatch.m:1023` | case 分发 | -| cpp `ExtSdkMethodType.cpp:66` | 常量定义 | - -配置项 key 计数(声明+params+赋值+withAppId+withAppKey / fromJson+toJson 各成对): - -- `enableChatroomConversation`:ChatOptions.ts×6、ExtSdkHelper.java×2、ExtSdkToJson.m×2 ✅ -- `autoLoadConversations`:ChatOptions.ts×5、ExtSdkHelper.java×2、ExtSdkToJson.m×2 ✅ -- `useAgoraChatDomain`:`src/`、`modules/`、`example/src/` grep 0 匹配 ✅(KI-107 零残留) - -参数/返回:TS 包装 `{getConversationsFromDBWithCursor: {cursor, pageSize}}`;Android `optString("cursor")`/`optInt("pageSize")` → `asyncGetConversationsFromDB(cursor, pageSize, ...)`(顺序经 native 源码核实);iOS `param[@"cursor"]`/`integerValue` → `getConversationsFromDBWithCursor:pageSize:`;返回 `{cursor, list}` 双端序列化同构(`ExtSdkCursorResultHelper.toJson` / `EMCursorResult toJsonObject`)。 - -## 版本互核 - -- `package.json` 1.21.0 = `src/version.ts`(gen)1.21.0 = `example/package.json` 1.21.0 = versionName/MARKETING_VERSION 1.21.0;versionCode/CURRENT_PROJECT_VERSION 2→3 ✅ -- native 依赖三处 4.25.0(`android/build.gradle:91`、`ChatSdk.podspec:53` SPM、`:57` CocoaPods)✅ -- CHANGELOG 双语 `## 1.21.0` 就位 ✅ - -## gate 留痕(阶段四必跑命令输出) - -```json -{"decision": "block", "reason": "平版门禁检查未过(修复后再收尾;确认为合理差异时,说明理由后可结束):key 在 TS(Consts) 有、Java(ExtSdkMethodType) 缺失: updateAPNsPushToken ;key 在 TS(Consts) 有、ObjC(ExtSdkMethodTypeObjc) 缺失: getNoDisturbUsersFromServer onMessageStatusChanged updateCurrentUserNick updateFCMPushToken updateHMSPushToken ;"} -``` - -与阶段二基线输出逐字相同:仅 6 个存量 deprecated key(`Consts.ts` 均有 `// deprecated` 标注,契约测试经 stripDeprecated 剔除),**本次新增 key 零 flag**,版本一致性检查通过。判定为脚本口径差异(不剔除 deprecated),合理差异。 - -## 本地依赖覆盖与还原 - -- apply:`rn-local-deps.sh apply`(因两个依赖文件含平版 bump 的未提交改动,按脚本提示用 `LOCAL_DEPS_ALLOW_DIRTY=1`,属 porting 流标准用法)。 -- restore:脚本 restore 后 `LOCAL-DEP-TEST` 标记 grep 0 残留、`android/libs/` 与 `framework/` 已删、`.git/info/exclude` 已清理、example/ios 全部还原(Podfile.lock 无 diff)。 -- ⚠️ 发现的脚本行为(记入问题清单):restore 的 `git checkout` 会连带还原这两个文件与 `example/ios/` 中**有意的未提交改动**(本次的 4.25.0 bump 与 pbxproj 版本号被一并回退,已手工重新施加并复核)。建议 skill 脚本在 `LOCAL_DEPS_ALLOW_DIRTY=1` 场景改为锚点反向替换而非 `git checkout`。 - -## 自检清单(references/react-native.md) - -- MT 常量三处一致 ✅(契约测试强制 + grep 抽查) -- 事件聚合 key 与 type:本次无新事件,N/A -- TS 参数 key 与 native 读取 key 一致 ✅(cursor/pageSize) -- 枚举值与原生映射:无新枚举,N/A -- 返回结构可被 TS 反序列化 ✅(`ChatCursorResult` 既有模式) -- 新类型进入 `src/index.ts` 导出:无新类型(`ChatCursorResult`/`ChatConversation` 已导出,方法挂在既有 `ChatManager`),N/A -- 版本对齐(package.json/example 三处/podspec 两处/gradle/双语 CHANGELOG)✅ -- 附件/下载类新 API 实机验证:本次无此类 API,N/A - ---- - -# 补录(2026-09-24):iOS PushKit 回移验证 - -对应 `06-pushkit-backport.md` / `02-contract.md` 补录节 / `03-implementation.md` 补录节。 - -## 1. 静态检查 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| `yarn test --no-watchman` | ✅ | 19 suites / 121 tests 全通过(与基线同数;本次为薄封装 API,无新增用例,符合仓库「不加 pass-through 测试」纪律) | -| `yarn typecheck` | ✅ | 无输出 | -| `yarn lint` | ✅ | 无输出 | -| `yarn check:circular:dpdm` | ✅ | `no circular dependency was found` | -| `yarn prepare` | ✅ | `src/version.ts` / `modules/cpp/CMakeLists.txt` / `example/src/env.ts` 生成成功,bob build 通过(worktree 首次需要) | - -## 2. 契约要素逐字 grep 抽查 - -### 2.1 方法名 key(五处 + dispatch/wrapper) - -命令:`grep -c 'bindPushKitToken' <文件>`,逐文件核对取值均为字面量 `bindPushKitToken` / `unbindPushKitToken`。 - -| 层 | 文件 | 结果 | -|---|---|---| -| TS | `src/__internal__/Consts.ts:24-25` | ✅ `MTbindPushKitToken = 'bindPushKitToken'`、`MTunbindPushKitToken = 'unbindPushKitToken'` | -| Java 常量 | `modules/java/com/chatsdk/common/ExtSdkMethodType.java:23-25` | ✅ 同值 | -| ObjC key | `modules/objc/common/ExtSdkMethodTypeObjc.h:22-24` | ✅ 同值 | -| ObjC methodMap | `modules/objc/common/ExtSdkMethodTypeObjc.m:38-40` | ✅ 两条映射齐备 | -| ObjC dispatch | `modules/objc/dispatch/ExtSdkDispatch.m:115-121` | ✅ 两个 case | -| Java dispatch | `modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java:121-130` | ✅ 两个 case | -| iOS wrapper | `modules/objc/dispatch/ExtSdkClientWrapper.{h,m}` | ✅ 声明 + 实现 | -| Android wrapper | `modules/java/com/chatsdk/dispatch/ExtSdkClientWrapper.java:325-333` | ✅ 两个方法 | -| cpp 透传 | `modules/cpp/common/ExtSdkMethodType.{h,cpp}` | ✅ 两个常量(该层整体滞后,KI-105,按仓库规范补齐) | - -契约测试本身(`src/__tests__/contract/methodNames.test.ts`)强制「TS `MT*` ⊆ Java 常量值 ⊆ ObjC header key 值」,121 tests 全绿即该约束通过。 - -### 2.2 初始化选项 key - -| 层 | 文件 | 结果 | -|---|---|---| -| TS 字段 | `src/common/ChatOptions.ts:128,134` | ✅ `apnsCertName` / `pushKitCertName` | -| TS 5 处同步 | 声明 + params + 赋值 + `withAppId` + `withAppKey` | ✅ grep 计数各 5 处(:128/134、:340/341、:393/394、:444/445、:495/496) | -| iOS fromJson | `modules/objc/dispatch/ExtSdkToJson.m:1310,1313` | ✅ 两个 key 映射到 `EMOptions.apnsCertName` / `.pushKitCertName` | -| iOS toJson | `modules/objc/dispatch/ExtSdkToJson.m:1252-1253` | ✅ 对称回写 | -| Android | `ExtSdkHelper.java` | ✅ **有意不改**:Android native 无该能力(`EMOptions.java` 无对应属性) | - -### 2.3 Value 编号撞号核查 - -- 新增 `ExtSdkMethodKeyBindPushKitTokenValue = 1027`、`ExtSdkMethodKeyUnbindPushKitTokenValue = 1028`(`ExtSdkMethodTypeObjc.h:708-709`),接在 push 段已用最大号 1026 之后。 -- 全文件扫描 `Value = N` 重复项:**仅 611 一处重复,且为存量问题**(`ExtSdkMethodKeyOnMessagesRecalledInfoValue` :535 与 `ExtSdkMethodKeyOnStreamMessagesReceivedValue` :807,两个都是事件 key,均不参与 dispatch switch)。本次新增值无撞号。 -- 存量 611 重复记入验收报告问题清单,本次不修(不在范围内)。 - -## 3. 原生依赖与发布产物核实 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| `ChatSdk.podspec` 两处版本 | ✅ | SPM `minimumVersion: '4.25.0'`(:53)、CocoaPods `~> 4.25.0`(:57) | -| `android/build.gradle` | ✅ | `io.hyphenate:hyphenate-chat:4.25.1`(:91) | -| CocoaPods trunk 可解析 4.25.0 | ✅ | `pod update HyphenateChat` 成功;`Podfile.lock` 由 4.24.1 更新为 `HyphenateChat (4.25.0)`(checksum `ea10cee2…`) | -| 发布头文件含 PushKit API | ✅ | `Pods/HyphenateChat/.../Headers/EMClient.h:753,776,825` 有 `bindPushKitToken:` / `registerPushKitToken:completion:` / `unRegisterPushKitTokenWithCompletion:`;`EMOptions.h:290,305` 有 `apnsCertName` / `pushKitCertName` | -| 发布头文件含证书名属性 | ✅ | 同上 | - -> 说明:`example/ios/Podfile.lock` 由 4.24.1 更新为 4.25.0 属**修复既有不一致**——该 lock 文件被 git 跟踪,而 1.21.0 的 native 依赖 bump 只改了 podspec,未同步 lock,导致 `pod install` 直接报 "could not find compatible versions"。本次一并修正。 - -## 4. 构建验证 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| example Android(`yarn example build:android`) | ✅ | `BUILD SUCCESSFUL in 1m 40s`,245 tasks | -| Android 产物含新路由(反编译) | ✅ | `javap -p android/build/intermediates/.../ExtSdkClientWrapper.class` 输出 `public void bindPushKitToken(JSONObject, String, ExtSdkCallback)` 与 `unbindPushKitToken(...)`,证明 Java 侧(含 `EMError` 导入)真实编译进产物 | -| example iOS(`yarn example build:ios`) | 见下节 | | - -## 5. 实机功能验证 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| PushKit 真机绑定/解绑回归 | ⏭️ 未执行 | 需要真实 APNs + PushKit 证书、iOS 真机(模拟器不支持 VoIP push)与 `PKPushRegistry` 直连凭据,本环境不具备。按纪律改由静态证据链覆盖:发布头文件 API 存在(§3)+ iOS 编译通过(§4)+ dispatch/methodMap/常量三处 grep 一致(§2)。交用户在真机环境补验 | -| Android 非法调用路径(预期 111) | ⏭️ 未执行 | TS 侧 `Platform.OS !== 'ios'` 守卫使该路径在 RN 中不可达;`smoke_local.sh` 冒烟脚本覆盖的是各 manager 的未登录路径,不含跨端非法调用。若要固化,需在 `example/ci/no_login_smoke.json` 加一条绕过 TS 守卫的直连用例,属额外范围,交用户决策(D-3) | - -### 4.1 构建结果补记 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| example iOS(`yarn example build:ios`) | ✅ | `success Successfully built the app`;含 Pods 重新集成后的完整编译与链接 | - -iOS 构建覆盖点:`modules/objc/common/ExtSdkMethodTypeObjc.{h,m}`(key/Value/methodMap)、`modules/objc/dispatch/ExtSdkDispatch.m`(两个 case)、`ExtSdkClientWrapper.{h,m}`(两个方法,真实调用 `registerPushKitToken:completion:` / `unRegisterPushKitTokenWithCompletion:`)、`ExtSdkToJson.m`(两个证书名选项映射)全部参与编译链接,无 `undeclared selector` / `property not found` 报错,即对 HyphenateChat 4.25.0 发布头文件的 API 引用全部成立。 - -## 6. 本次暴露的基线问题(不在本次范围) - -| id | 问题 | 证据 | 处理 | -|---|---|---|---| -| D-2 | ~~iOS `renewToken` dispatch 缺失~~ **已于 2026-09-24 修复**(见本文档 §7):key、Value、methodMap 条目、wrapper 实现都在,唯独 `ExtSdkDispatch.m` 没有 `ExtSdkMethodKeyRenewTokenValue` 的 case,落到 default 返回 `not implement: renewToken` | 修复前 `grep -n RenewToken modules/objc/dispatch/ExtSdkDispatch.m` 无命中;`ExtSdkMethodTypeObjc.h:21,430`、`.m:36` 均有条目 | 用户裁决「修」:补齐 case + 从 5.0.0 移植「methodMap 条目必须有 dispatch case」契约测试;反向验证已证明用例能抓该缺口 | -| D-4 | `example/ios/Podfile.lock` 与 `ChatSdk.podspec` 不一致:lock 内 `ChatSdk (1.20.0)` + `HyphenateChat (~> 4.24.1)`,而 podspec 已是 1.21.0 且要求 `~> 4.25.0` | `pod install` 直接失败:"CocoaPods could not find compatible versions for pod HyphenateChat / In snapshot (Podfile.lock): 4.24.1" | 本次一并修正(lock 更新为 `ChatSdk (1.21.0)` + `HyphenateChat (4.25.0)` + `ShengwangInfra_iOS (1.3.16)`) | -| D-5 | ObjC `Value` 编号存量重复:`ExtSdkMethodKeyOnMessagesRecalledInfoValue` 与 `ExtSdkMethodKeyOnStreamMessagesReceivedValue` 都是 `611` | `ExtSdkMethodTypeObjc.h:535,807` | 两个都是事件 key,不参与 dispatch switch,无实际影响;本次不修,仅登记 | - -## 7. D-2 修复验证(2026-09-24,用户裁决「修」) - -### 7.1 修复内容 - -- `modules/objc/dispatch/ExtSdkDispatch.m`:补上 `ExtSdkMethodKeyRenewTokenValue` 的 dispatch case(此前 key/Value/methodMap 条目/wrapper 实现四者齐备,唯独缺这个 case,调用落到 default 返回 `not implement: renewToken`)。 -- 移植 5.0.0 的契约测试,让该类缺口不能复现: - - `src/__tests__/contract/parsers.ts`:新增 `OBJC_DISPATCH_PATH` 常量与 `parseObjcDispatchCases()`(正则提取 `ExtSdkDispatch.m` 中所有 `case ExtSdkMethodKeyXValue:` 标签)。 - - `src/__tests__/contract/methodNames.test.ts`:新增用例 `every non-event ObjC methodMap entry has a dispatch case`(`on` 前缀的事件 key 豁免,因为事件经 `onReceive:` 上报、不从 TS 调用)。 - -### 7.2 验证(含反向验证) - -| 验证项 | 结果 | 备注 | -|---|---|---| -| 新增用例是否真的能抓到这个缺口(反向验证) | ✅ | 临时移除刚补的 `renewToken` case 后跑 `yarn test:contract`:`1 failed, 8 passed`,失败信息逐字指名 `ExtSdkMethodKeyRenewToken ('renewToken') -> ExtSdkMethodKeyRenewTokenValue`;随后从备份恢复文件(`grep -c RenewTokenValue` 由 0 回到 1) | -| `yarn test --no-watchman` | ✅ | 19 suites / **122 tests**(较基线 121 增 1,即新增的 dispatch 契约用例) | -| `yarn typecheck` / `yarn lint` | ✅ | 无输出 | -| iOS 重新构建(`yarn example build:ios`) | 见下 | 补 case 后重新编译确认仍可链接 | - -反向验证的意义:新增的契约用例不是「登记即绿」——它对本分支真实存在的缺口会红。这也是 5.0.0 当初新增该用例的原因(commit `bc9915e`)。 - -### 7.3 补做 header 声明核查时发现第二层原因(重要) - -第一次「补 dispatch case」后 iOS 构建**失败**,编译错误为: - -```text -error: no visible @interface for 'ExtSdkClientWrapper' declares the selector 'renewToken:withMethodType:result:' -``` - -即 `renewToken` 的缺口是**两层**的,不只是 dispatch case: - -| 层 | 1.21.0 修复前 | 5.0.0 | -|---|---|---| -| `ExtSdkClientWrapper.h` 方法声明 | **缺失**(`.m` 有实现但头文件从未声明) | 有(`:64`) | -| `ExtSdkDispatch.m` dispatch case | **缺失** | 有 | -| `ExtSdkClientWrapper.m` 实现 | 有(`:320`) | 有 | -| key 常量 / Value / methodMap 条目 | 有 | 有 | - -历史核查(`git log -S`)证明这不是本次回移引入:`modules/objc/dispatch/ExtSdkDispatch.m` **从未**有过 `ExtSdkMethodKeyRenewTokenValue`,`ExtSdkClientWrapper.h` **从未**声明过 `renewToken:`,`dev` 分支现状同样是 0 —— 也就是说 iOS 上 `ChatClient.renewToken` 自引入以来一直是 `not implement`,从未被接通。 - -### 7.4 wrapper 声明/实现一致性抽查(本次新固化) - -命令(client 域 22 个方法逐个比对 `.h` 声明数与 `.m` 实现数): - -```bash -cd modules/objc/dispatch -for m in initSDKWithDict getToken createAccount login logout changeAppKey changeAppId \ - getCurrentUser uploadLog compressLogs kickDevice kickAllDevices isLoggedInBefore \ - getLoggedInDevicesFromServer loginWithAgoraToken isConnected renewToken updatePushConfig \ - getRTCTokenInfoWithChannelName getUserIdsWithRTCUids bindPushKitToken unbindPushKitToken; do - h=$(grep -c "^- (void)$m:" ExtSdkClientWrapper.h); m2=$(grep -c "^- (void)$m:" ExtSdkClientWrapper.m) - [ "$h" != "$m2" ] && printf "%-38s .h=%s .m=%s ← 缺声明\n" "$m" "$h" "$m2" -done -``` - -结果:**无输出**(22 个方法全部对齐)。修复前该检查会命中 `renewToken .h=0 .m=1`。 - -> 为什么契约测试抓不到这个缺口:契约测试是**文本层**比对(key/Value/methodMap/dispatch case),看不到 ObjC 的 selector 可见性;而「methodMap 条目必须有 dispatch case」这条在缺 case 时确实会红(§7.2 反向验证已证),但「dispatch case 调用的 selector 是否有声明」属编译期约束,只能靠 Xcode 构建暴露。本次两层缺口正好被这两个手段分别覆盖。 - -### 7.5 最终构建结果 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| iOS 构建(补 dispatch case 后,未补声明) | ❌ 失败(已修复) | `xcodebuild` exit 65,报 `no visible @interface … 'renewToken:withMethodType:result:'` | -| iOS 构建(补 `.h` 声明后) | ✅ | `success Successfully built the app`(exit 0) | -| `yarn test --no-watchman` | ✅ | 19 suites / 122 tests | -| `yarn typecheck` / `yarn lint` | ✅ | 无输出 | - ---- - -## 8. CI 失败排查与修复(2026-09-24,PR #49 / run 35962497865) - -### 8.1 现象 - -`1.21.0 CI` 的 **iOS build** job(ID 107513882137)在 `Install pods` 步骤失败,退出码 31: - -```text -[!] The version of CocoaPods used to generate the lockfile (1.16.2) is higher than - the version of the current executable (1.15.2). Incompatibility issues may arise. -[!] CocoaPods could not find compatible versions for pod "HyphenateChat": - In snapshot (Podfile.lock): HyphenateChat (= 4.25.0, ~> 4.25.0) - In Podfile: ChatSdk (from `../..`) was resolved to 1.21.0, which depends on - HyphenateChat (~> 4.25.0) -None of your spec sources contain a spec satisfying the dependencies: ... -``` - -后续 `Scan deprecated iOS APIs` 的失败是**连带失败**(pod install 失败导致 `build/reports/deprecated-ios-raw.log` 不存在,`scripts/parse-deprecated-ios.sh` 退出 1),不是独立问题。 - -### 8.2 排查过程与结论(`COCOAPODS: 1.15.2` 那行不是根因) - -| 检查 | 结果 | 结论 | -|---|---|---| -| 本机 `pod --version` | 1.16.2 | 本地比 CI 新 | -| 仓库 `example/Gemfile.lock` 钉的 CocoaPods | **1.15.2** | CI 用 `bundle exec pod install`,跑的就是 1.15.2;第一条 version 警告因此**无法通过改 lock 里那行消除** | -| CI 缓存列表(`gh api .../actions/caches`) | 只有 2 条 pods 缓存:`macOS-pods-5a3de369…`(2026-08-26)、`macOS-pods-4f8dff7e…`(2026-09-17) | 两者都**早于**本次 4.25.0 变更;缓存 key = `macOS-pods-${{ hashFiles('example/ios/Podfile.lock') }}` | -| 当前 Podfile.lock 的 SHA256 前缀 | `4a171f09…` | 与上面两个 key 都**不相等** → 只能靠 `restore-keys: macOS-pods-` 拿到旧缓存 | -| 旧缓存内容 | 含 `~/.cocoapods`(spec 索引)的**旧快照** | 索引里没有 HyphenateChat 4.25.0,而 CI 步骤 `bundle exec pod install --project-directory=ios` **不带 `--repo-update`** → 解析器报 "None of your spec sources contain a spec" | -| 本机同样用 1.15.2 跑 `pod install` | ✅ 成功 | 因为本机 `~/.cocoapods/repos/trunk/Specs` 有 4.25.0(此前 `pod update` 拉过) | -| 本机清空 CocoaPods 缓存(`CP_HOME_DIR` 指向空目录)后跑 1.15.2 `pod install` | ✅ 成功(29s) | 证明 **CDN 侧 4.25.0 完全可用**,不是「新版本未发布」问题 | -| dev 分支 CI 的同一处配置 | 同样不带 `--repo-update` | 属**存量脆弱点**:只要 Podfile.lock 一变(依赖升级就把 cache key 改掉),就会退化到旧 spec 索引而失败 | -| 全仓 `grep repo-update` | 无命中 | 本次首次引入 | - -**根因**:iOS job 依赖 `~/.cocoapods` 缓存中的 spec 索引,而该缓存的 key 绑定 Podfile.lock 的哈希;本次依赖从 4.24.1 升到 4.25.0 使 key 改变、命中旧索引,又因为没有 `--repo-update` 而不会刷新索引,于是解析失败。与「4.25.0 未发布」无关(已用冷启动实验证伪)。 - -### 8.3 修复 - -| 文件 | 改动 | -|---|---| -| `.github/workflows/ci.yml:137` | `bundle exec pod install --repo-update --project-directory=ios`(附 3 行注释说明为何必须带该参数) | -| `.github/workflows/device-smoke.yml:156` | 同上 | -| `.github/workflows/single-account-nightly.yml:179` | 同上 | -| `example/ios/Podfile.lock` | `COCOAPODS:` 行由 `1.16.2` 回归 `1.15.2`,与 `Gemfile.lock` 钉住的版本一致(本地 `bundle exec pod install` 自动改写所致;该行只记录生成版本,不影响解析) | - -未改动 `scripts/ci/smoke_local.sh` / `nightly_local.sh`:它们的 `pod install` 只在 `example/ios/Pods` 不存在时执行,且开发者本机索引通常已是新的,保持原样以减少改动面。 - -### 8.4 验证 - -| 验证项 | 结果 | 备注 | -|---|---|---| -| 三个 workflow YAML 解析 | ✅ | `yaml.safe_load` 通过,`Install pods` 步骤命令逐字为 `bundle exec pod install --repo-update --project-directory=ios` | -| 冷启动(空 `~/.cocoapods`)+ 1.15.2 `pod install` | ✅ | 29s 成功,证明 CDN 可解析 4.25.0 | -| 修复后的完整命令 `pod install --repo-update`(bundler 1.15.2) | ✅ | exit 0,32s;日志含 `Updating local specs repositories` → `Pod installation complete! There are 88 dependencies from the Podfile and 89 total pods installed` | -| 本机环境还原 | ✅ | 复现实验期间临时移除的 `~/.cocoapods/.../HyphenateChat/4.25.0` 已从备份还原(`ls` 确认 podspec.json 与 .etag 均在) | - -> 遗留观察(不在本次范围):`--repo-update` 会让每次 iOS job 多约 30 秒并更新时间戳、从而让 `example/ios/Pods` 缓存难以复用;若要优化,可改为按「缓存是否命中」条件决定是否带该参数(需要给 Cache Pods 步骤加 `id` 并读取 `steps..outputs.cache-hit`)。本次以「先保证正确」为先,不做该优化。 diff --git a/docs/porting/1.21.0/06-pushkit-backport.md b/docs/porting/1.21.0/06-pushkit-backport.md deleted file mode 100644 index 448e6987..00000000 --- a/docs/porting/1.21.0/06-pushkit-backport.md +++ /dev/null @@ -1,57 +0,0 @@ -# 06 - 补充回移:iOS PushKit 与推送证书名(React Native 1.21.0) - -范围:`01-api-diff.md` ~ `04-verification.md` 与 `acceptance-report.md` 之外的**计划外补充项**。 - -PushKit 不在 4.24.1 → 4.25.0 的版本 diff 区间内(双端头文件 4.24.2 即已具备,见下节证据),历次平版均未覆盖。5.0.0 分支已补齐该能力并整体收敛了推送配置;1.21.0 是**长期分支**,必须保持向后兼容,因此本次只做**加法**:新增两个 iOS 证书初始化选项与 PushKit 绑定/解绑方法,不删除任何既有 API。 - -## 1. 结论 - -- 1.21.0 分支此前**没有** PushKit 能力:TS、iOS wrapper、Android wrapper 全链路无 `PushKit` 命中。 -- native 侧能力完整且**早于 4.25.0 即已存在**:`emclient-ios` tag `4.24.2` 的 `EMClient.h` 与 `EMOptions.h` 已有全部四个 PushKit 方法与 `pushKitCertName` 属性,与 `5.0.0` 逐字一致(`git diff 4.24.2 5.0.0 -- newSDK/HyphenateSDK/EMOptions.h` 对两个证书名属性无差异)。因此这不是平版新版本 API,而是**漏移植的历史能力补齐**。 -- 本次只在 **1.21.0 分支**新增 iOS 专用公开 API,**不删除** `ChatOptions.pushConfig` / `ChatPushConfig` / `ChatClient.updatePushConfig`,`bindDeviceToken` 也不新增(1.21.0 无此方法,绑定入口仍是 `updatePushConfig`)。「只加不删」由用户明确指定。 -- 旧路径 `updatePushConfig` 的 iOS 实现(`registerForRemoteNotificationsWithCertName:` 在运行时写 `apnsCertName`)**原样保留、不加优先级判断**:老用户行为零变化;两处同时配置时以运行时写入为准,该冲突写进公开注释与 CHANGELOG 说明,由用户决策(见待决策项 D-1)。 -- iOS 原生侧能力完整,本次不改 native。 -- 参考实现:5.0.0 分支 commit `bc9915e`(RN)与 Flutter `im_flutter_sdk` 5.0.0 分支 commit `41d2cf17`。 - -## 2. 变更清单(native → React Native) - -| id | 类型 | 领域 | iOS native 签名(HyphenateChat 4.24.2/4.25.0,两者一致) | Android native | RN 决策 | 状态 | -| --- | --- | --- | --- | --- | --- | --- | -| `pushkit_cert_name` | existing_option | push | `EMOptions.h:293`:`@property(nonatomic, copy) NSString *pushKitCertName;` | 无(iOS 专有) | include → `ChatOptions.pushKitCertName`(初始化下发,运行时不修改) | pending | -| `apns_cert_name` | existing_option | push | `EMOptions.h:278`:`@property(nonatomic, copy) NSString *apnsCertName;` | 无(iOS 专有) | include → `ChatOptions.apnsCertName`(初始化下发);**保留**旧 `pushConfig.deviceId` 路径不动 | pending | -| `pushkit_register` | existing_api | push | `EMClient.h:776`:`- (void)registerPushKitToken:(NSData *_Nullable)aPushToken completion:(void (^_Nullable)(EMError * _Nullable aError))aCompletionBlock;` | 无(iOS 专有) | include → `ChatClient.bindPushKitToken({ deviceToken })`;Android 注册同名路由返回 `EMError.OPERATION_UNSUPPORTED`(111) | pending | -| `pushkit_unregister` | existing_api | push | `EMClient.h:825`:`- (void)unRegisterPushKitTokenWithCompletion:(void (^_Nullable)(EMError * _Nullable aError))aCompletionBlock;` | 无(iOS 专有) | include → `ChatClient.unbindPushKitToken()`;Android 同上 | pending | -| `pushkit_bind_sync` | existing_api | push | `EMClient.h:753`:`- (EMError *_Nullable)bindPushKitToken:(NSData *_Nullable)aPushToken;` | 无(iOS 专有) | skip(同步变体不单独暴露:TS 层本身即 Promise,暴露会产生两个语义完全相同的 API;与 5.0.0 结论一致) | pending | -| `pushkit_unbind_sync` | existing_api | push | `EMClient.h:802`:`- (EMError *_Nullable)unBindPushKitToken;` | 无(iOS 专有) | skip(同上) | pending | -| `push_config_kept` | kept_api | push | — | — | 1.21.0 **保留** `ChatOptions.pushConfig` / `ChatPushConfig` / `ChatClient.updatePushConfig`(兼容性要求;5.0.0 的删除动作不回移) | pending | -| `ios_renew_token_dispatch` | bug_fix | client | `EMClient.h`:`- (void)renewToken:completion:` | `EMClient.renewToken(token, EMCallBack)` | **发现但不在本次范围**:1.21.0 的 iOS `ExtSdkMethodKeyRenewTokenValue` 有 key、有 methodMap 条目、wrapper 有实现,但 `ExtSdkDispatch.m` 缺 switch 分支 → 落到 default 返回 `not implement: renewToken`。5.0.0 已修(`bc9915e`)。本次按用户范围不修,记入待决策项 D-2 | pending(待用户裁决) | - -未匹配清单: - -- UM-1:Android 端无 PushKit 能力(native `EMClient.java` 无对应方法)。用户已确认「仅 iOS 支持,非 iOS 调用静默 no-op」(沿用 5.0.0 裁决)。 -- UM-2:native 4 个 PushKit 方法为语义相同的同步/异步两对。仅暴露异步一对(沿用 5.0.0 裁决)。 -- UM-3:`EMOptions.h` 注释声明两个证书名「只能在 `initializeSDKWithOptions` 时设置」,而 1.21.0 现存的 `updatePushConfig` iOS 路径(native `EMClient.mm:1885` 的 `registerForRemoteNotificationsWithCertName:`)会在运行时写 `apnsCertName`,与之冲突。用户已裁决:**不改旧路径、不引入优先级逻辑**,冲突写进文档。 -- UM-4:1.21.0 的 `ChatPushConfig.deviceId` 在 iOS 上承担 APNs 证书名语义,与新增 `ChatOptions.apnsCertName` 语义重叠。不合并、不改名(兼容性要求),两个入口并存,文档各自说明。 -- UM-5:PushKit 普通推送 token 只有绑定没有解绑(1.21.0 由 `updatePushConfig` 承担绑定),PushKit 绑定/解绑成对。两者是独立能力,不合并入口(沿用 5.0.0 结论)。 - -## 3. 跨端契约 - -| 契约要素 | 取值 | 落点 | -| --- | --- | --- | -| 初始化选项 key | `apnsCertName`、`pushKitCertName`(顶层,可选) | TS `ChatOptions` → iOS `ExtSdkToJson.m` 的 `EMOptions (Json)` `fromJson`/`toJson`;Android 无对应 native 能力,不做映射 | -| 方法名 key | `bindPushKitToken` / `unbindPushKitToken` | `src/__internal__/Consts.ts`、`modules/java/com/chatsdk/common/ExtSdkMethodType.java`、`modules/objc/common/ExtSdkMethodTypeObjc.h` + `.m`、`modules/cpp/common/ExtSdkMethodType.{h,cpp}`(仓库规范) | -| 路由注册 | iOS wrapper 实现并 dispatch;Android 注册同名路由返回 `EMError.OPERATION_UNSUPPORTED`(111) | `modules/objc/dispatch/ExtSdkDispatch.m`、`modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java` | -| 请求参数 key | `bindPushKitToken`:`deviceToken`(`NSString`,十六进制字符串,native 侧转 `NSData`);`unbindPushKitToken`:无参数 | TS 组装 / iOS wrapper 读取 | -| 返回结构 | `onResult` 包装为 `{<方法名>: nil}`,TS 只取错误 | 与 `updatePushConfig`(payload 为 nil)一致 | -| 非 iOS 行为 | TS 侧 `Platform.OS !== 'ios'` 守卫,静默 no-op | `src/ChatClient.ts` | -| TS 公开签名 | `ChatClient.bindPushKitToken(params: { deviceToken: string }): Promise`、`ChatClient.unbindPushKitToken(): Promise` | 与 5.0.0 一致(方法名与参数形态逐字对齐,便于跨版本迁移) | - -## 4. 原生行为依据(决定 API 形态的关键证据) - -证据取自 `/Users/asterisk/Codes/zuoyu_native/emclient-ios` tag `4.24.2`(RN 1.21.0 声明的 iOS 依赖为 `~> 4.25.0`,两版本相关头文件逐字一致): - -- 绑定时读取初始化选项上的证书名:`EMClient.mm:1608`(`pushKitCertName` 为空 → `EMErrorUserIllegalArgument`)、`1620`(以 `options.pushKitCertName` 调 `bindUserDeviceToken`);`1665`(`apnsCertName` 为空 → 同样报错)、`1680`(绑定)。 -- PushKit 解绑同样读取该属性:`EMClient.mm:1794`。 -- PushKit token 先入 keychain 再绑定:`EMClient.mm:1611`(读 `kBindPushKitToken`);未登录时 `1601` 返回 `EMErrorUserNotLogin`,token 留在 keychain,登录成功后自动重绑。 -- 旧 APNs 路径会在运行时改写证书名:`EMClient.mm:1885`(`registerForRemoteNotificationsWithCertName:` 内 `self.options.apnsCertName = aCertName`),与 `EMOptions.h:278` 的「仅初始化可设置」注释冲突;1.21.0 的 `updatePushConfig` 正走该路径,本次保留不动(UM-3 / D-1)。 -- 结论:PushKit 绑定**必须**在初始化时下发 `pushKitCertName`,否则 native 直接返回参数非法错误 —— 这决定了 `ChatOptions.pushKitCertName` 是 `bindPushKitToken` 可用的前置条件,两者必须同批交付。 diff --git a/docs/porting/1.21.0/acceptance-report.md b/docs/porting/1.21.0/acceptance-report.md deleted file mode 100644 index 7ba2a158..00000000 --- a/docs/porting/1.21.0/acceptance-report.md +++ /dev/null @@ -1,114 +0,0 @@ -# 验收报告:React Native 1.21.0(native 4.24.1 → 4.25.0 平版) - -- 目标仓库:`react-native-chat-sdk`,worktree `.worktree/1.21.0`,分支 `1.21.0`(基于 `dev` @ 3d8f454) -- 目标平台版本号:1.21.0(用户指定);改动已提交至分支 `1.21.0` -- 全量变更清单见 [01-api-diff.md](01-api-diff.md);契约见 [02-contract.md](02-contract.md);实现明细见 [03-implementation.md](03-implementation.md);验证证据见 [04-verification.md](04-verification.md) -- 先例:Flutter 4.25.0 平版已评审闭环,本次沿用其全部结论(`useAgoraChatDomain` 不移植、`autoLoadConversations` 衍生新增) - -## 二维对照表 - -| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | -|---|---|---|---|---|---|---| -| `fetchConversationsFromDB`(本地会话分页加载) | `getConversationsFromDBWithCursor:pageSize:completion:` | `asyncGetConversationsFromDB(cursor, pageSize, callback)` | 已实现(`ChatManager.fetchConversationsFromDB`) | 已实现 | 已实现 | ✅ | -| `enableChatroomConversation`(设置获取本地会话列表时是否包含聊天室会话) | `EMOptions.enableChatroomConversation` | `set/isEnableChatroomConversation` | 已实现(默认 false) | 已实现 | 已实现 | ✅ | -| `autoLoadConversations`(衍生新增,UM-2) | `EMOptions.autoLoadConversations`(4.24.1 已有) | `set/isAutoLoadAllConversations`(4.24.1 已有) | 已实现(默认 true,文档强调分页前须关闭及原因) | 已实现 | 已实现 | ✅(沿用 Flutter 评审结论) | -| ~~`useAgoraChatDomain`~~ | 裸 `BOOL` 无文档 | `Boolean` 三态 + `@hide` | **不移植**(KI-107 对内品牌开关) | 不移植 | 不移植 | ✅ 已闭环,grep 零残留 | -| `syncDataWSHost/Port`、`SyncDataWebSocketServer/Port` 移除 | 已删 | 已删 | 从未暴露,无操作 | - | - | ✅ skip(全仓 grep 实证无引用) | -| 搜索关键词上限注释(512→120) | - | 仅注释 | RN 注释未写上限,无操作 | - | - | ✅ skip | -| `fetchSubscribedUsers` gender bugfix | 实现层修复 | - | RN `ChatUserInfo.gender` 已有,反序列化不受影响 | - | - | ✅ 无操作 | -| token NTP 校正时间、cloud 超时单位 | 内部实现 | 内部实现 | N/A | - | - | ✅ 不平版 | - -## 验证摘要 - -- `yarn typecheck` / `lint` / `check:circular:dpdm` / `test --no-watchman`(19 suites、121 tests,含契约测试)全过;`yarn prepare`(bob build)通过。 -- example 双端构建通过(本地依赖覆盖模式):Android bundle 含本地 so;iOS `.app/Frameworks/` 为本地 `HyphenateChat.framework 4.25.0` + `aosl.framework`,Podfile.lock 无 HyphenateChat。 -- 实机回归双端通过(2026-09-10,真实账号,各 5 步):init 注入 `autoLoadConversations:false` + login + 分页/翻页 + 无效 cursor(110)全部符合预期,证据见 04-verification.md。 -- 契约要素 grep 逐字抽查通过;本地依赖已还原零残留,远程依赖声明保持 4.25.0。 - -## 未匹配清单 - -| id | 疑点 | 状态 | -|---|---|---| -| UM-1 | `useAgoraChatDomain` 双端形态差异 | ✅ 已闭环(KI-107,不移植) | -| UM-2 | 新 API 前置条件 `autoLoadConversations=false` 在 RN 基线无入口 | ✅ 已闭环(衍生新增,沿用 Flutter 评审结论) | -| UM-3 | Android `asyncGetConversationsFromDB` 过滤 chatThread 会话,iOS 公开头文件未见对应说明 | ✅ 已闭环(2026-09-10 用户确认:保持现状,与 Flutter 一致,不抹平) | - -## 问题清单 - -1. iOS 首次构建失败:bundler 找不到 Gemfile.lock 钉住的 gems(本机 ruby 3.4.4 环境缺 gem),`bundle install` 后通过。环境性问题,已解决。 -2. ~~gate 脚本对 6 个存量 deprecated key 报 block~~ → 已修复(2026-09-10):`porting_guard.sh` 的 RN 分支三处 key 提取(TS/Java/ObjC)前加 `grep -v '// deprecated'`,与契约测试 `stripDeprecated` 口径对齐;修复后 gate 通过。 -3. `rn-local-deps.sh restore` 用 `git checkout` 还原,会连带抹掉这两个依赖文件与 `example/ios/` 中**有意的未提交改动**(本次 4.25.0 bump 与 pbxproj 版本号被回退,已手工重新施加并复核确认)。建议 `LOCAL_DEPS_ALLOW_DIRTY=1` 场景改锚点反向替换。 -4. 实机回归首跑 login 报 200 "The user is already logged in"(模拟器残留登录态;auto 模式 login 重试不覆盖 200 码),卸载应用清数据后通过。 - -## 待用户决策项 - -1. ~~UM-3~~ → 已闭环:保持现状(2026-09-10 用户确认,与 Flutter 一致)。 -2. ~~实机回归~~ → 已闭环:双端各 5 步全过(init 注入 `autoLoadConversations:false` + login + sendMessage×2 + 分页/翻页 + 无效 cursor 110),证据见 04-verification.md。 -3. **远程依赖未发布**:podspec/gradle 已声明 4.25.0(交付形态),但在 native 正式发布前 `pod install`/gradle 拉取会失败;发布前如需再次编译/实机验证,重走本地依赖流程(Android 实机需额外替换 symbolLibs 真 so)。 -4. 本地依赖改动不提交的约束已满足(restore 零残留,git status 中无 LOCAL-DEP-TEST 痕迹);提交范围与时间由用户决定。 - ---- - -# 补充验收报告:iOS PushKit 回移(2026-09-24) - -- 范围:在 1.21.0(长期分支)上兼容性回移 iOS PushKit 能力,**只加不删**。 -- 全量变更清单见 [06-pushkit-backport.md](06-pushkit-backport.md);契约见 [02-contract.md](02-contract.md) 补录节;实现明细见 [03-implementation.md](03-implementation.md) 补录节;验证证据见 [04-verification.md](04-verification.md) 补录节。 -- 参考实现:RN 5.0.0 commit `bc9915e`、Flutter 5.0.0 commit `41d2cf17`(取值与命名逐字对齐,便于跨版本迁移)。 -- 代码状态:**未提交**(工作区改动,等待用户确认提交范围)。 - -## 二维对照表 - -| 变更项 | iOS 源(HyphenateChat 4.25.0 发布头文件) | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | -|---|---|---|---|---|---|---| -| `ChatOptions.apnsCertName` | `EMOptions.h:290` `@property(nonatomic, copy) NSString *apnsCertName;` | 无(iOS 专有) | 已实现(5 处同步,可选,仅初始化) | 有意不映射(native 无该能力) | 已实现(`ExtSdkToJson.m` fromJson + toJson) | ✅ | -| `ChatOptions.pushKitCertName` | `EMOptions.h:305` `@property(nonatomic, copy) NSString *pushKitCertName;` | 无(iOS 专有) | 已实现(5 处同步,可选,仅初始化) | 有意不映射 | 已实现(同上) | ✅ | -| `ChatClient.bindPushKitToken({ deviceToken })` | `EMClient.h:776` `- (void)registerPushKitToken:(NSData *)aPushToken completion:...` | 无(iOS 专有) | 已实现(`Platform.OS` 守卫 no-op) | 路由已注册,返回 `OPERATION_UNSUPPORTED`(111) | 已实现 | ✅ | -| `ChatClient.unbindPushKitToken()` | `EMClient.h:825` `- (void)unRegisterPushKitTokenWithCompletion:...` | 无(iOS 专有) | 已实现(同上) | 同上 | 已实现 | ✅ | -| `bindPushKitToken:`(同步变体) | `EMClient.h:753` | 无 | **skip**(语义重复且阻塞线程,沿用 5.0.0 结论) | - | - | ✅ skip | -| `unBindPushKitToken`(同步变体) | `EMClient.h:802` | 无 | **skip**(同上) | - | - | ✅ skip | -| `ChatOptions.pushConfig` / `ChatPushConfig` / `ChatClient.updatePushConfig` | - | - | **保留不动**(兼容性硬要求,5.0.0 的删除动作不回移) | 保留 | 保留 | ✅ 保持不变 | -| `ChatClient.bindDeviceToken` | - | - | **不引入**(属 5.0.0 的 break 动作) | - | - | ✅ skip | - -## 验证摘要 - -- 静态检查:`yarn test --no-watchman`(19 suites / 121 tests,含契约测试)、`yarn typecheck`、`yarn lint`、`yarn check:circular:dpdm` 全过;worktree 首次执行 `yarn prepare`(bob build)通过。 -- 契约要素 grep 逐字抽查:方法名 key 九处(TS / Java 常量 / Java dispatch / Java wrapper / ObjC key / ObjC methodMap / ObjC dispatch / ObjC wrapper / cpp 透传)取值一致;选项 key 在 TS 5 处同步 + iOS fromJson/toJson 齐备;新增 ObjC Value 1027/1028 无撞号。 -- 依赖与发布产物核实:CocoaPods trunk 可解析 `HyphenateChat 4.25.0`;下载后的发布头文件确认含全部四个 PushKit 方法与两个证书名属性(即回移不是「对未发布 API 的猜测」)。 -- 双端构建:example Android `BUILD SUCCESSFUL`(245 tasks,并用 `javap` 反编译证实新路由进了产物 `.class`);example iOS `Successfully built the app`(四个改动文件全部参与编译链接,无 `undeclared selector` / `property not found`)。 -- 真机 PushKit 回归:⏭️ 未执行 —— 需要真实 APNs + PushKit 证书、iOS 真机(模拟器不支持 VoIP push)与 `PKPushRegistry` 直连凭据,本环境不具备;由发布头文件 + 双端编译 + 契约 grep 构成静态证据链,交用户补验。 - -## 未匹配清单 - -| id | 疑点 | 状态 | -|---|---|---| -| UM-1 | Android 无 PushKit 能力 | ✅ 沿用 5.0.0 裁决:仅 iOS 支持,非 iOS 静默 no-op;Android 侧注册同名路由返回 111 以保持 key 契约 | -| UM-2 | native 同步/异步两对方法语义重复 | ✅ 沿用 5.0.0 裁决:只暴露异步一对 | -| UM-3 | `EMOptions.h` 声明两个证书名「仅初始化可设置」,而 1.21.0 现存 `updatePushConfig` 的 iOS 路径会在运行时写 `apnsCertName` | ✅ 用户已裁决:不改旧路径、不引入优先级逻辑;冲突以 `updatePushConfig` 的 TypeDoc Note 告知使用者(两处只配一处) | -| UM-4 | `ChatPushConfig.deviceId` 在 iOS 上承担 APNs 证书名语义,与新增 `ChatOptions.apnsCertName` 语义重叠 | ✅ 不合并、不改名(兼容性要求),两个入口并存并各自注释说明 | -| UM-5 | PushKit 绑定/解绑成对,普通推送 token 在 1.21.0 只有绑定(经 `updatePushConfig`)没有解绑 | ✅ 沿用 5.0.0 结论:独立能力,不合并入口 | - -## 问题清单 - -1. `example/ios/Podfile.lock` 与 `ChatSdk.podspec` 不一致(**存量问题**,本次一并修正):lock 停留在 `ChatSdk (1.20.0)` + `HyphenateChat (~> 4.24.1)`,而 podspec 已是 1.21.0 且要求 `~> 4.25.0`,直接导致 `pod install` 报 "could not find compatible versions"。已更新为 `ChatSdk (1.21.0)` + `HyphenateChat (4.25.0)` + `ShengwangInfra_iOS (1.3.16)`。 -2. CocoaPods CDN 故障(**环境问题**,已绕过):`cdn.cocoapods.org` 对 spec 请求返回 301 跳 `cdn.jsdelivr.net`,CocoaPods 1.15.2 的 HTTP/2 客户端跟随重定向时报 `Error in the HTTP2 framing layer`,三次重试均失败;临时把 `~/.cocoapods/repos/trunk/.url` 指向 `https://cdn.jsdelivr.net/cocoa/` 后成功,**验证完成后已还原为官方默认值**(`https://cdn.cocoapods.org/`)。 -3. ObjC `Value` 编号存量重复:`OnMessagesRecalledInfoValue` 与 `OnStreamMessagesReceivedValue` 同为 `611`(`ExtSdkMethodTypeObjc.h:535,807`)。两个都是事件 key,不参与 dispatch switch,无实际影响;本次不修,仅登记。 - -## 待用户决策项 - -1. **D-1(已按裁决落地,待确认表述)**:证书名两处入口的冲突处理 = 不改旧路径 + 在 `updatePushConfig` 注释里告知「两处只配一处」。这是本次唯一的语义相关文档改动,若希望 1.21.0 的注释也完全零改动,可回退该 Note。 -2. **D-2**:iOS `renewToken` dispatch 缺失(key/Value/methodMap/wrapper 齐备,缺 `ExtSdkDispatch.m` 的 case → `not implement: renewToken`)。5.0.0 已修并补了「methodMap 条目必须有 dispatch case」的契约测试。**本次按你指定的范围未修**,是否在 1.21.0 一并修复(并移植该契约测试)请裁决。 -3. **D-3**:是否在 `example/ci/no_login_smoke.json` 增加一条绕过 TS `Platform.OS` 守卫的 Android 非法调用用例,把「Android 路由返回 111」固化成可回归的断言。属额外范围,默认不做。 -4. **D-6(提交范围)**:本次代码未提交。注意其中 `example/ios/Podfile.lock` 属于**修复既有不一致**而非本次功能改动,请确认是否一并纳入提交。 - ---- - -# D-2 落实补充(2026-09-24 用户裁决:修) - -| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | -|---|---|---|---|---|---|---| -| `ChatClient.renewToken` 的 iOS 接通缺口(**两层**:wrapper `.h` 缺方法声明 + dispatch 缺 case) | `EMClient.h` `- (void)renewToken:completion:` | `EMClient.renewToken(token, EMCallBack)` | 已有(`ChatClient.renewToken`,无改动) | 已有(无改动) | **本次补齐** `ExtSdkClientWrapper.h` 的 `renewToken:` 声明 + `ExtSdkDispatch.m` 的 `ExtSdkMethodKeyRenewTokenValue` case | ✅ 已修复 | -| 新增契约用例「methodMap 条目必须有 dispatch case」 | - | - | `src/__tests__/contract/parsers.ts` + `methodNames.test.ts`(从 5.0.0 移植) | - | - | ✅ 已实现 | - -- 影响面:此前 1.21.0 上调用 `ChatClient.renewToken` 必返回 `not implement: renewToken`(历史核查证明该缺口自引入以来一直存在:`git log -S` 显示 dispatch 与头文件声明**从未**有过,`dev` 分支现状同样为 0);同时 `ChatSdk.podspec`/`build.gradle` 的 4.25.0 对本改动无影响(纯 dispatch 路由补全)。 -- 验证:反向验证证明新用例对本分支真实缺口会红(临时撤 case → `1 failed`,错误信息逐字指名 `ExtSdkMethodKeyRenewToken`);恢复后 `yarn test --no-watchman` 19 suites / **122 tests** 全过;typecheck / lint 通过;iOS 重编译通过(见 04-verification.md §7)。 -- 待决策项 D-2 至此**闭环**;D-1 按裁决保留 Note;D-3 按默认「不加」处理;D-6 按裁决**暂不提交**,工作区改动待你审阅。 diff --git a/docs/porting/5.0.0/01-api-diff-android.md b/docs/porting/5.0.0/01-api-diff-android.md new file mode 100644 index 00000000..ecc4bf06 --- /dev/null +++ b/docs/porting/5.0.0/01-api-diff-android.md @@ -0,0 +1,219 @@ +# 5.0.0 平版 · 阶段一:公开 API Diff(Android) + +## 1. Diff 锚点与范围 + +- 源仓库:`/Users/asterisk/Codes/zuoyu_native/emclient-android` +- diff 锚点:`SDK_4.24.1`(commit `300865ac9cb1813c113199f0b19a65be79c4731a`)→ `SDK_5.0.0`(commit `8025ecb767e72a5c4623fab457ed269011e773d6`) +- 中间 tag 核对:`git tag -l` 显示 `SDK_4.24.1` 与 `SDK_5.0.0` 之间**无任何中间 tag**(无 SDK_4.25.x 等),不存在跳版,直接两端 diff 即可。 +- diff 范围:`hyphenatechatsdk/src/com/hyphenate/` 根包公开文件 + `chat/` 子包公开类;`chat/adapter/`、`chat/core/` 为内部实现,未 diff。`cloud/`、`notification/`、`push/`、`util/` 子包在 diff 中仅有 AndroidX import 替换(`android.support.*` → `androidx.*`),无公开 API 变化,未逐条收录。 +- 5.0.0 tag 目录结构核对:**无新增公开子包**。`chat/` 下文件级变化: + - 新增:`EMGroupConfigs.java`、`EMGroupReadReceipt.java`、`EMMessageReadReceipt.java` + - 删除:`EMCheckType.java`、`EMGroupOptions.java`、`EMGroupReadAck.java`、`EMMessageStatistics.java`、`EMStatisticsManager.java` +- 参考迁移文档:`/Users/asterisk/Codes/zuoyu/easemob-doc/docs/document/android/migration_guide.md`(下称「迁移文档」,引用其章节名)。 +- 提取规则:同一能力的同步+异步双方法只收录异步方法(无异步才收录同步);签名逐字取自 diff / tag 源码。 +- 全量统计:4.24.1 → 5.0.0 范围内 `1887 insertions, 4610 deletions`(61 个文件,含内部实现与 AndroidX 迁移)。 + +## 2. 变更清单 + +类型说明:new_api / deprecated(本次无新增废弃,4.x 的 @Deprecated 大多直接删除)/ removed / renamed / param_changed / new_type / new_property / new_enum / new_listener / removed_listener / removed_type。 + +### client(EMClient / 登录与设备) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| client_create_account_removed | removed | `public void createAccount(String username, String password) throws HyphenateException` | 密码登录下线 / 其他删除的 API | 无客户端替代,服务端 REST 注册 | +| client_login_password_removed | removed | `public void login(String id, String password, @NonNull final EMCallBack callback)` | 密码登录下线 | 仅保留 `loginWithToken(String, String, EMCallBack)`(签名不变) | +| client_login_with_agora_token_removed | removed | `public void loginWithAgoraToken(String username, String agoraToken, @NonNull final EMCallBack callback)` | 密码登录下线 | 4.x 已废弃,删除;用 `loginWithToken` | +| client_renew_token_no_callback_removed | removed | `public void renewToken(String newAgoraToken)` | 密码登录下线 | 保留 `renewToken(String newToken, @NonNull EMCallBack callback)` | +| client_get_user_token_from_server_removed | removed | `public void getUserTokenFromServer(final String username, final String password, final EMValueCallBack callBack)` | 密码登录下线 | Token 由 App Server 下发 | +| client_is_logged_in_before_removed | removed | `public boolean isLoggedInBefore()` | 自动登录移除 | 替代:`isLoggedIn()` / `isConnected()` / `isDatabaseOpened()` | +| client_auto_login_removed | removed | `EMClient#init` 内基于 `getAutoLogin()`+`isLoggedInBefore()` 的自动登录逻辑 | 自动登录移除 | 行为变化,init 后需主动 `loginWithToken` | +| client_statistics_manager_removed | removed | `public EMStatisticsManager statisticsManager()` | 其他删除的 API | 连同 `EMStatisticsManager`、`EMMessageStatistics` 两个类整体删除 | +| client_get_logged_in_devices_pwd_removed | removed | `public List getLoggedInDevicesFromServer(String username, String password) throws HyphenateException` | 设备管理与鉴权 | 密码版,无直接替代 | +| client_get_logged_in_devices_token_sync_removed | removed | `public List getLoggedInDevicesFromServerWithToken(@NonNull String username, @NonNull String token) throws HyphenateException` | 设备管理与鉴权 | 同步版删除,改为异步(见下条) | +| client_fetch_logged_in_devices_with_token_new | new_api | `public void fetchLoggedInDevicesFromServerWithToken(@NonNull String username, @NonNull String token, EMValueCallBack> callBack)` | 设备管理与鉴权 | 替代被删的同步版 | +| client_kick_device_pwd_removed | removed | `public void kickDevice(String username, String password, String resource) throws HyphenateException` | 设备管理与鉴权 | `kickDeviceWithToken(String, String, String)` 保留(签名不变) | +| client_kick_all_devices_pwd_removed | removed | `public void kickAllDevices(String username, String password) throws HyphenateException` | 设备管理与鉴权 | `kickAllDevicesWithToken(String, String)` 保留(签名不变) | +| client_check_removed | removed | `public void check(String username, String password, final CheckResultListener listener)` + `public interface CheckResultListener { void onResult(@EMCheckType.CheckType int type, int result, String desc); }` | 密码登录下线 | 连同 `EMCheckType` 类删除 | +| client_is_database_opened_new | new_api | `public boolean isDatabaseOpened()` | 登录与数据库打开解耦 | 4.24.1 中为包私有,5.0.0 提升为 public | +| client_get_device_info_new | new_api | `public JSONObject getDeviceInfo()` | (无) | 疑似内部辅助方法暴露为 public,见疑点清单 | +| client_version_bump | new_property | `public final static String VERSION = "5.0.0"` | — | 原值 `"4.24.1"` | + +### options(EMOptions) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| options_auto_login_removed | removed | `public void setAutoLogin(boolean autoLogin)` / `public boolean getAutoLogin()` | 自动登录移除 | 无直接替代 | +| options_require_ack_removed | removed | `public void setRequireAck(boolean requireAck)` / `public boolean getRequireAck()` | 已读回执体系重构 | 改用 `EMMessage#setIsNeedReadReceipt(true)` 按消息设置 | +| options_report_server_removed | removed | `public String getReportServer()` / `public void setReportServer(String reportServer)` | 其他删除的 API | 私有化部署由服务端处理 | +| options_auto_sync_contacts_removed | removed | `public void setEnableAutoSyncContacts(boolean enable)` / `public boolean isEnableAutoSyncContacts()` | 服务端拉取 API 迁移 | 并入 `setDataSyncType` 的 `CONTACTS` 位 | +| options_area_code_int_removed | removed | `public static class AreaCode`(int 常量容器)+ `public static final int AREA_CODE_CN = 1;` 等 7 个常量 + `public void setAreaCode(int code)` | 有替代方式的 API | `getAreaCode()` 仍返回 `int`,签名不变 | +| options_area_code_enum_new | new_enum | `public enum AreaCode { CN(1), NA(2), EU(4), AS(8), JP(16), IN(32), GLOB(-1); public int getValue(); }` | 有替代方式的 API | 配合 `setAreaCode(AreaCode code)` | +| options_set_area_code_param_changed | param_changed | 旧:`public void setAreaCode(int code)` → 新:`public void setAreaCode(AreaCode code)` | 有替代方式的 API | | +| options_data_sync_type_enum_new | new_enum | `public enum EMDataSyncType { NONE(0), CONVERSATIONS(1 << 0), CONTACTS(1 << 1), JOINED_GROUPS(1 << 2); public int getValue(); public static int toNativeMask(EnumSet types); public static EnumSet fromNativeMask(int mask); }` | 数据同步 API | 位掩码枚举,可组合 | +| options_data_sync_type_api_new | new_api | `public void setDataSyncType(EnumSet types)` / `public EnumSet getDataSyncType()` | 数据同步 API | 须在 `EMClient#init` 前配置,默认 `NONE` | + +### connection(EMConnectionListener,根包) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| connection_on_logout_int_removed | removed_listener | `default void onLogout(final int errorCode){}` | 监听器回调变化汇总 | 4.x 已废弃 | +| connection_on_logout_int_string_removed | removed_listener | `default void onLogout(final int errorCode,String info)` | 监听器回调变化汇总 | 4.x 已废弃;保留 `onLogout(final int errorCode, EMLoginExtensionInfo info)`(4.24.1 已存在,5.0.0 方法体不再代理旧回调) | +| connection_on_database_opened_new | new_listener | `default void onDatabaseOpened(String username) {}` | 登录与数据库打开解耦 | 本地库就绪即可读本地数据 | +| connection_on_data_sync_start_new | new_listener | `default void onDataSyncStart(EMOptions.EMDataSyncType type){}` | 数据同步 API | | +| connection_on_data_sync_finish_new | new_listener | `default void onDataSyncFinish(EMOptions.EMDataSyncType type, int errorCode){}` | 数据同步 API | `errorCode` 为 `EMError#EM_NO_ERROR` 表示成功 | + +### contact(EMContactManager / EMContactListener) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| contact_get_all_from_server_removed | removed | `public List getAllContactsFromServer() throws HyphenateException` | 服务端拉取 API 迁移 | 5.0.0 无任何从服务器拉好友列表的入口 | +| contact_async_get_all_from_server_removed | removed | `public void asyncGetAllContactsFromServer(final EMValueCallBack> callback)` | 服务端拉取 API 迁移 | | +| contact_async_fetch_all_from_server_removed | removed | `public void asyncFetchAllContactsFromServer(EMValueCallBack> callback)` | 服务端拉取 API 迁移 | | +| contact_async_fetch_all_from_server_paged_removed | removed | `public void asyncFetchAllContactsFromServer(int limit, String cursor, EMValueCallBack> callback)` | 服务端拉取 API 迁移 | 分页重载一并删除 | +| contact_on_contact_sync_start_removed | removed_listener | `default void onContactSyncStart() {}`(EMContactListener) | 服务端拉取 API 迁移 | 改用 `EMConnectionListener#onDataSyncStart(CONTACTS)` | +| contact_on_contact_sync_finish_removed | removed_listener | `default void onContactSyncFinishWithError(int errorCode, String error) {}`(EMContactListener) | 服务端拉取 API 迁移 | 改用 `onDataSyncFinish(CONTACTS, int)` | + +注:本地读取接口 `getContactsFromLocal()` / `fetchContactFromLocal(String)` / `asyncFetchAllContactsFromLocal(...)` 在 4.24.1 已存在且签名不变。 + +### chat(EMChatManager / EMConversation / 已读回执体系) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| chat_ack_message_read_removed | removed | `public void ackMessageRead(String to, String messageId) throws HyphenateException` | 发送消息已读回执与清除未读数 | 由 `asyncSendMessageReadReceipts` 替代 | +| chat_ack_group_message_read_removed | removed | `public void ackGroupMessageRead(String to, String messageId, String ext) throws HyphenateException` | 发送消息已读回执与清除未读数 | 群聊不再单独逐条回执,不再支持 ext 自定义内容 | +| chat_ack_conversation_read_removed | removed | `public void ackConversationRead(String conversationId) throws HyphenateException` | 发送消息已读回执与清除未读数 | 由 `asyncClearConversationUnreadMessageCount` 替代 | +| chat_mark_all_conversations_as_read_removed | removed | `public boolean markAllConversationsAsRead()` | 发送消息已读回执与清除未读数 | 由 `asyncClearAllConversationUnreadMessageCount` 替代 | +| chat_async_send_message_read_receipts_new | new_api | `public void asyncSendMessageReadReceipts(final List messages, final EMCallBack callBack)` | 发送消息已读回执与清除未读数 | 批量回执;每次最多 50 条同一会话消息,仅处理 `isNeedReadReceipt()==true` 且未回执的消息 | +| chat_async_clear_conversation_unread_new | new_api | `public void asyncClearConversationUnreadMessageCount(final String conversationId, final EMCallBack callBack)` | 发送消息已读回执与清除未读数 | 仅清本地未读数并同步多设备,不发已读回执 | +| chat_async_clear_all_unread_new | new_api | `public void asyncClearAllConversationUnreadMessageCount(final EMCallBack callBack)` | 发送消息已读回执与清除未读数 | | +| chat_fetch_conversations_from_server_removed | removed | `public Map fetchConversationsFromServer() throws HyphenateException` + 3 个 async 重载:`asyncFetchConversationsFromServer(EMValueCallBack>)`、`asyncFetchConversationsFromServer(int pageNum, int pageSize, EMValueCallBack>)`、`asyncFetchConversationsFromServer(int limit, String cursor, EMValueCallBack>)` | 服务端拉取 API 迁移 | 改 `getAllConversations()`(本地)+ `onDataSyncFinish(CONVERSATIONS)`;迁移文档称「全部 4 个 async 重载」,实际 diff 仅见 3 个,见疑点清单 | +| chat_async_fetch_pinned_conversations_removed | removed | `public void asyncFetchPinnedConversationsFromServer(final int limit, final String cursor, final EMValueCallBack> callback)` | 服务端拉取 API 迁移 | 置顶随会话同步落地 | +| chat_async_get_conversations_with_cursor_removed | removed | `public void asyncGetConversationsFromServerWithCursor(@NonNull String cursor, @NonNull EMConversationFilter filter, EMValueCallBack> callback)` | 服务端拉取 API 迁移 | 本地查询替代 | +| chat_load_all_conversations_removed | removed | `public boolean loadAllConversations()` | 服务端拉取 API 迁移 | 降为包私有;直接 `getAllConversations()` | +| chat_update_participant_removed | removed | `public boolean updateParticipant(String from, String changeTo)` | 其他删除的 API | 无替代 | +| chat_report_message_removed | removed | `public void asyncReportMessage(String msgId, String tag, String reportReason, EMCallBack callback)` / `public void reportMessage(String msgId, String tag, String reportReason) throws HyphenateException` | 其他删除的 API | 举报提交至业务服务器 | +| chat_fetch_group_read_acks_removed | removed | `public EMCursorResult fetchGroupReadAcks(String msgId, int pageSize, String startAckId) throws HyphenateException` / `public void asyncFetchGroupReadAcks(final String msgId, final int pageSize, final String startAckId, final EMValueCallBack> callBack)` | 回执详情查询 | 由 `asyncFetchGroupMessageReadReceipts` 替代 | +| chat_async_fetch_group_message_read_receipts_new | new_api | `public void asyncFetchGroupMessageReadReceipts(final String msgId, final int pageSize, final String startAckId, final EMValueCallBack> callBack)` | 回执详情查询 | pageSize 1-50 | +| chat_async_get_group_message_read_receipts_new | new_api | `public void asyncGetGroupMessageReadReceipts(final List messages, final EMValueCallBack> callBack)` | 回执详情查询 | 批量取群消息回执汇总,每次最多 20 条同一会话消息 | +| chat_async_modify_message_param_changed | param_changed | 旧:`public void asyncModifyMessage(String messageId, EMMessageBody messageBodyModified, final EMValueCallBack callBack)` → 新:`public void asyncModifyMessage(String messageId, @Nullable EMMessageBody messageBodyModified, @Nullable Map ext, final EMValueCallBack callBack)` | 有替代方式的 API | 新增 `ext` 参数;旧三参数重载删除 | +| chat_fetch_history_messages_removed | removed | `public EMCursorResult fetchHistoryMessages(String conversationId, EMConversationType type, int pageSize, String startMsgId)` 及带 `EMSearchDirection` 重载(同步);`public void asyncFetchHistoryMessage(final String conversationId, final EMConversationType type, final int pageSize, final String startMsgId, ...)` 两个异步重载 | 有替代方式的 API | 4.x 均已 @Deprecated | +| chat_async_fetch_history_messages_new | new_api | `public void asyncFetchHistoryMessages(String conversationId, EMConversationType type, int pageSize, String cursor, EMFetchMessageOption option, final EMValueCallBack> callBack)` | 有替代方式的 API | 统一异步分页接口(方法名由 `asyncFetchHistoryMessage` 变 `asyncFetchHistoryMessages`) | +| chat_async_delete_conversations_new | new_api | `public void asyncDeleteConversations(List conversationIds, boolean deleteMessages, EMCallBack callBack)` | 主要新增 API | 批量删除本地会话 | +| conversation_mark_message_as_read_removed | removed | `public void markMessageAsRead(String messageId)` / `public void markAllMessagesAsRead()`(EMConversation) | 发送消息已读回执与清除未读数 | `EMConversation` 不再提供修改已读状态接口 | +| conversation_get_message_param_changed | param_changed | 旧:`public EMMessage getMessage(String messageId, boolean markAsRead)` → 新:`public EMMessage getMessage(String messageId)`(EMConversation) | 行为变化 4 | 查询不再自动标记已读;旧重载删除 | +| conversation_search_msg_scope_sync_removed | removed | `public List searchMsgFromDB(String keywords, long timeStamp, int maxCount, String from, EMSearchDirection direction, EMMessageSearchScope searchScope)`(EMConversation,同步) | 有替代方式的 API | 异步 `asyncSearchMsgFromDB(..., List senders, ..., EMMessageSearchScope, EMValueCallBack)` 保留;迁移文档明确「未统一移除所有同步搜索接口」 | +| conversation_get_conversation_name_new | new_api | `public String getConversationName()` / `public String getConversationAvatar()`(EMConversation) | 主要新增 API | 数据未同步时可能返回空字符串 | +| conversation_listener_on_conversation_read_removed | removed_listener | `void onConversationRead(String from, String to)`(EMConversationListener) | 监听器回调变化汇总 | 5.0.0 中该接口仅剩 `void onConversationUpdate()` | +| msg_listener_on_message_read_removed | removed_listener | `default void onMessageRead(List messages) {}`(EMMessageListener) | 接收消息已读回执 | | +| msg_listener_on_group_message_read_removed | removed_listener | `default void onGroupMessageRead(List groupReadAcks) {}`(EMMessageListener) | 接收消息已读回执 | | +| msg_listener_on_read_ack_updated_removed | removed_listener | `default void onReadAckForGroupMessageUpdated() {}`(EMMessageListener) | 监听器回调变化汇总 | 迁移文档称改名为 `onReadReceiptForGroupMessageUpdated()`,**但 5.0.0 源码中不存在该方法**,见疑点清单 | +| msg_listener_on_message_recalled_removed | removed_listener | `default void onMessageRecalled(List messages){}`(EMMessageListener) | 监听器回调变化汇总 | 4.x 已废弃;保留 `onMessageRecalledWithExt(List)` | +| msg_listener_on_message_read_receipts_new | new_listener | `default void onMessageReadReceipts(List receipts) {}`(EMMessageListener) | 接收消息已读回执 | 单聊群聊统一回执回调 | + +### message(EMMessage) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| message_is_acked_renamed | renamed | 旧:`public boolean isAcked()` / `public void setAcked(boolean isAcked)` → 新:`public boolean isPeerRead()`(+ 包私有 `void setPeerRead(boolean)`) | EMMessage 已读相关方法重命名 | setter 降为包私有,仅供 SDK 内部 | +| message_is_unread_renamed | renamed | 旧:`public boolean isUnread()` / `public void setUnread(boolean unread)` → 新:`public boolean isRead()`(+ 包私有 `void setRead(boolean)`) | EMMessage 已读相关方法重命名 | 语义由「未读」反转为「已读」,**取值取反**,迁移时最易出错 | +| message_need_group_ack_renamed | renamed | 旧:`public boolean isNeedGroupAck()` / `public void setIsNeedGroupAck(boolean need)` → 新:`public boolean isNeedReadReceipt()` / `public void setIsNeedReadReceipt(boolean need)` | EMMessage 已读相关方法重命名 | 单聊群聊均适用 | +| message_group_ack_count_renamed | renamed | 旧:`public int groupAckCount()` / `public void setGroupAckCount(int count)` → 新:`public int readReceiptCount()`(+ 包私有 `void setReadReceiptCount(int)`) | EMMessage 已读相关方法重命名 | | +| message_create_txt_send_message_removed | removed | `public static EMMessage createTxtSendMessage(String content, String username)` | 有替代方式的 API | 用 `createTextSendMessage(String content, String username)`(4.x 已存在) | +| message_get_user_name_removed | removed | `public String getUserName()` | 有替代方式的 API | 用 `getFrom()` | +| message_get_recaller_removed | removed | `public String getRecaller()` | 有替代方式的 API | 用 `EMRecallMessageInfo#getRecallBy()`(经 `onMessageRecalledWithExt`) | + +### group(EMGroupManager / EMGroup / 新类型) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| group_style_enum_removed | removed_type | `public enum EMGroupStyle { EMGroupStylePrivateOnlyOwnerInvite, EMGroupStylePrivateMemberCanInvite, EMGroupStylePublicJoinNeedApproval, EMGroupStylePublicOpenJoin }`(EMGroupManager 内部枚举) | 群组配置模型重构 | 拆为 EMGroupConfigs 三个布尔字段,对照关系见迁移文档 | +| group_options_class_removed | removed_type | `public class EMGroupOptions { public int maxUsers = 200; public EMGroupManager.EMGroupStyle style; public boolean inviteNeedConfirm = false; public String extField; }` | EMGroupOptions 与 EMGroupConfigs 对照 | | +| group_configs_class_new | new_type | `public class EMGroupConfigs { public int maxUsers = 200; public boolean isPublic = false; public boolean joinApprovalRequired = false; public boolean allowInvites = false; public boolean inviteNeedConfirm = false; public String extField; }` | 群组配置模型重构 | 纯字段袋,无方法 | +| group_configs_type_enum_new | new_enum | `public enum EMGroupConfigsType { IS_PUBLIC(1 << 0), JOIN_APPROVAL_REQUIRED(1 << 1), ALLOW_INVITES(1 << 2), MAX_USERS(1 << 3), INVITE_NEED_CONFIRM(1 << 4), EXT(1 << 5); }`(EMGroupManager 内部枚举,含 `getValue()` / `static int toNativeMask(EnumSet)`) | 相关 API 变化 | 位掩码,用于 `updateGroupConfigs` 指定更新项 | +| group_create_group_param_changed | param_changed | 旧:`createGroup(String groupName, String avatar, String desc, String[] allMembers, String reason, EMGroupOptions option)`(及无 avatar 重载 + 两个 `asyncCreateGroup(...)`)→ 新:`public EMGroup createGroup(String groupName, String avatar, String desc, String[] allMembers, String reason, EMGroupConfigs configs) throws HyphenateException` / `public void asyncCreateGroup(final String groupName, final String avatar, final String desc, final String[] allMembers, final String reason, final EMGroupConfigs configs, final EMValueCallBack callback)` | 相关 API 变化 | 无 avatar 的 5 参重载连同删除,5.0.0 仅保留带 avatar 形式 | +| group_update_group_configs_new | new_api | `public EMGroup updateGroupConfigs(String groupId, EnumSet types, EMGroupConfigs configs) throws HyphenateException` / `public void asyncUpdateGroupConfigs(final String groupId, final EnumSet types, final EMGroupConfigs configs, final EMValueCallBack callback)` | 相关 API 变化 | 建群后按配置类型更新群属性 | +| group_load_all_groups_removed | removed | `public synchronized boolean loadAllGroups()` | 服务端拉取 API 迁移 | 降为包私有;直接 `getAllGroups()` | +| group_get_from_server_fetch_members_removed | param_changed | 旧:`public EMGroup getGroupFromServer(String groupId, boolean fetchMembers) throws HyphenateException` 删除,保留 `public EMGroup getGroupFromServer(String groupId)` | 有替代方式的 API | 成员走独立分页接口 | +| group_get_joined_groups_from_server_removed | removed | `public synchronized List getJoinedGroupsFromServer()` / `public synchronized List getJoinedGroupsFromServer(int pageIndex, int pageSize, boolean needMemberCount, boolean needRole)` / `public void asyncGetJoinedGroupsFromServer(final EMValueCallBack> callback)` / `public void asyncGetJoinedGroupsFromServer(final int pageIndex, final int pageSize, boolean needMemberCount, boolean needRole, final EMValueCallBack> callback)` | 服务端拉取 API 迁移 | 改 `getAllGroups()`(本地)+ `onDataSyncFinish(JOINED_GROUPS)` | +| group_get_public_groups_from_server_removed | removed | `public EMCursorResult getPublicGroupsFromServer(int pageSize, String cursor) throws HyphenateException` / `public void asyncGetPublicGroupsFromServer(final int pageSize, final String cursor, final EMValueCallBack> callback)` | 其他删除的 API | 群组目录由业务服务维护 | +| group_async_upload_shared_file_overload_removed | removed | `public void asyncUploadGroupSharedFile(final String groupId, final String filePath, final EMCallBack callBack)` | 其他删除的 API | 保留 `asyncUploadGroupSharedFile(String, String, EMValueCallBack)` 重载 | +| group_is_member_only_renamed | renamed | 旧:`public boolean isMemberOnly()` → 新:`public boolean isJoinApprovalRequired()`(EMGroup) | 相关 API 变化 | 仅表示公开群是否需审批入群;`isPublic()`、`isMemberAllowToInvite()` 签名不变 | +| group_get_users_new | new_api | `public List getUsers()`(EMGroup) | 主要新增 API | 群主+管理员+成员合并列表,可能含重复 ID | +| group_read_ack_class_removed | removed_type | `public class EMGroupReadAck { getAckId(); getMsgId(); String getFrom(); String getContent(); getCount(); getTimestamp(); }` | 回执详情查询 | 由 EMGroupReadReceipt 替换;`getContent()` 不再下发;`getFrom()` 返回类型由 `String` 变 `EMGroupMemberInfo` | +| group_read_receipt_class_new | new_type | `public class EMGroupReadReceipt extends EMBase { public String getAckId(); public String getMsgId(); public EMGroupMemberInfo getFrom(); public int getCount(); public long getTimestamp(); }` | 回执详情查询 | | +| message_read_receipt_class_new | new_type | `public class EMMessageReadReceipt extends EMBase { public String getMessageId(); public String getConversationId(); public boolean isPeerReceipt(); public int getReadCount(); }` | 接收消息已读回执 | 统一单聊/群聊回执模型 | +| group_listener_on_member_joined_removed | removed_listener | `void onMemberJoined(final String groupId, final String member)`(EMGroupChangeListener,4.x 已废弃) | 监听器回调变化汇总 | 保留 `default void onMembersJoined(String groupId, List members)`(4.24.1 已存在) | +| group_listener_on_member_exited_removed | removed_listener | `void onMemberExited(final String groupId, final String member)`(EMGroupChangeListener,4.x 已废弃) | 监听器回调变化汇总 | 保留 `default void onMembersExited(String groupId, List members)` | +| group_listener_on_request_declined_4arg_removed | removed_listener | `void onRequestToJoinDeclined(String groupId, String groupName, String decliner, String reason)`(EMGroupChangeListener,4.x 已废弃) | 监听器回调变化汇总 | 保留 5 参版 `onRequestToJoinDeclined(String, String, String, String, String applicant)`(4.24.1 已存在) | + +### room(EMChatRoomManager / EMChatRoomChangeListener) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| room_create_chat_room_removed | removed | `public EMChatRoom createChatRoom(String subject, String description, String welcomeMessage, int maxUserCount, List members) throws HyphenateException` / `public void asyncCreateChatRoom(final String subject, final String description, final String welcomeMessage, final int maxUserCount, final List members, final EMValueCallBack callBack)` | 其他删除的 API | 服务端 REST 创建 | +| room_destroy_chat_room_removed | removed | `public void destroyChatRoom(String chatRoomId) throws HyphenateException` / `public void asyncDestroyChatRoom(final String chatRoomId, final EMCallBack callBack)` | 其他删除的 API | 服务端 REST 解散 | +| room_get_all_chat_rooms_removed | removed | `public List getAllChatRooms()` | 其他删除的 API | 按需 `fetchChatRoomFromServer` | +| room_fetch_from_server_fetch_members_removed | param_changed | 旧:`public EMChatRoom fetchChatRoomFromServer(String roomId, boolean fetchMembers) throws HyphenateException` 删除,保留 `public EMChatRoom fetchChatRoomFromServer(String roomId)` | 有替代方式的 API | 成员走独立接口 | +| room_remove_chat_room_listener_removed | renamed | 旧:`public void removeChatRoomListener(EMChatRoomChangeListener listener)` → 新:`public void removeChatRoomChangeListener(EMChatRoomChangeListener listener)` | 有替代方式的 API | 与 `addChatRoomChangeListener` 配套(4.24.1 中两者并存,5.0.0 删旧名) | +| room_listener_on_member_joined_2arg_removed | removed_listener | `default void onMemberJoined(final String roomId, final String participant){}`(EMChatRoomChangeListener,4.x 已废弃) | 监听器回调变化汇总 | 保留 3 参版 `onMemberJoined(String roomId, String participant, String ext)` | +| room_listener_on_mute_list_added_removed | removed_listener | `void onMuteListAdded(final String chatRoomId, final List mutes, final long expireTime)`(EMChatRoomChangeListener,4.x 已废弃) | 监听器回调变化汇总 | 保留 `default void onMuteListAdded(String chatRoomId, Map muteInfo)` | + +### multidevice(EMMultiDeviceListener,根包) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| multidevice_conversation_unread_cleared_new | new_enum | `int CONVERSATION_UNREAD_MESSAGECOUNT_CLEARED = 65;` | 多设备事件 | 经 `onConversationEvent` 下发,收到后需重新 `getAllConversations()` | +| multidevice_all_conversation_unread_cleared_new | new_enum | `int ALL_CONVERSATION_UNREAD_MESSAGECOUNT_CLEARED = 66;` | 多设备事件 | 同上 | + +### statistics(整体移除) + +| id | 类型 | Android 源签名 | 迁移文档依据 | 备注 | +|---|---|---|---|---| +| statistics_manager_class_removed | removed_type | `public class EMStatisticsManager extends EMBase`(含 `getMessageStatistics(String)`、`getMessageCount(long, long, EMSearchMessageDirect, EMSearchMessageType)`、`getMessageSize(long, long, EMSearchMessageDirect, EMSearchMessageType)` 及内部枚举 `EMSearchMessageDirect`、`EMSearchMessageType`) | 其他删除的 API | 入口 `EMClient#statisticsManager()` 一并删除 | +| message_statistics_class_removed | removed_type | `public class EMMessageStatistics extends EMBase`(含 `getMsgId()`、`getTo()`、`getFrom()`、`getType()`、`getChatType()`、`getMsgTime()`、`direct()`、`getMsgSize()`、`getAttachmentSize()`、`getThumbnailSize()`) | 其他删除的 API | | + +### 其他核对的类型 + +| id | 类型 | 结论 | 备注 | +|---|---|---|---| +| check_type_class_removed | removed_type | `public class EMCheckType`(常量 `ACCOUNT_VALIDATION=0`、`GET_DNS_LIST_FROM_SERVER=1`、`GET_TOKEN_FROM_SERVER=2`、`DO_LOGIN=3`、`DO_MSG_SEND=4`、`DO_LOGOUT=5` + `@interface CheckType`)随 `EMClient#check` 删除 | 迁移文档「密码登录下线」表 | +| error_codes_unchanged | — | `EMError.java` 在 diff 中**零变化**,无新增/删除错误码 | 迁移文档亦未提及错误码变更 | +| presence_thread_userinfo_push_unchanged | — | `EMPresenceManager`、`EMChatThreadManager`、`EMUserInfoManager`、`EMPushManager`、`EMPushConfigs`、`EMPresenceListener`、`EMChatThreadChangeListener` 无公开 API 签名变化(EMPushManager 仅内部推送 Token 上传判断逻辑变化) | 与迁移文档「行为变化 7」对应 | + +## 3. 未匹配 / 疑点清单(宁多勿漏) + +1. **`onReadReceiptForGroupMessageUpdated` 不存在**:迁移文档「接收消息已读回执」「监听器回调变化汇总」两处称 `onReadAckForGroupMessageUpdated()` 改名为 `onReadReceiptForGroupMessageUpdated()`,但 `git grep` 在 SDK_5.0.0 全树(含内部包)中均无该方法——旧回调是直接删除,群消息回执状态更新实际由 `onMessageReadReceipts` 承载。RN 侧不应平版这个"新回调"。 +2. **asyncFetchConversationsFromServer 重载数量不符**:迁移文档称删除「全部 4 个 asyncFetchConversationsFromServer(...) 重载」,SDK_4.24.1 源码中实际只有 3 个(无参 / pageNum+pageSize / limit+cursor)。若算上 `asyncFetchPinnedConversationsFromServer` 与 `asyncGetConversationsFromServerWithCursor` 则为 5 个相关接口。以源码为准:共删除 6 个服务端会话拉取相关方法。 +3. **`EMClient#getDeviceInfo()` 疑似内部 API 暴露**:5.0.0 新增 `public JSONObject getDeviceInfo()`,组装 hid/os/os-version 供内部 `setPresence` 使用,无 javadoc、迁移文档未提及。建议 RN 侧不平版,待确认。 +4. **`EMGroupConfigsType.toNativeMask(EnumSet)` / `EMDataSyncType.toNativeMask` 和 `fromNativeMask` 为 public static**:是跨 JNI 层的掩码转换工具,语义属内部但修饰符为 public。RN 侧无需平版。 +5. **迁移文档未提及的删除:`EMConversation#searchMsgFromDB(..., EMMessageSearchScope)` 同步重载**(异步版保留)。文档仅笼统说「部分旧版 searchMsgFromDB 调用方式」调整,未指明是哪个重载,此处给出精确签名。 +6. **迁移文档未提及的新增 public API:`EMGroupManager#createGroup` 无 avatar 重载删除**。文档只说旧 `EMGroupOptions` 重载删除,未明确 5.0.0 只保留带 `avatar` 参数的 createGroup/asyncCreateGroup 形式(4.24.1 有带/不带 avatar 各一对)。 +7. **行为变化(无签名变化,RN 侧需注意)**: + - `EMChatManager#getUnreadMessageCount` 统计范围收窄:不含聊天室、不含 Thread、仅统计 `EMPushRemindType.ALL` 的单聊群聊会话(迁移文档「行为变化 1」)。 + - `EMConversation#getMessage(String)` 不再自动标记已读(行为变化 4)。 + - `EMChatService` 旧保活逻辑移除(行为变化 6),`EMPushManager` Token 上传判断不再依赖 `isLoggedInBefore()`/`getAutoLogin()`(行为变化 7)。 + - 前后台检测改用 AndroidX `ProcessLifecycleOwner`(行为变化 5),SDK 全面迁移 AndroidX——对 RN 封装层无 API 影响,但影响 example 工程依赖。 +8. **EMOptions 中 `AreaCode` 旧 int 常量全删**:`AREA_CODE_CN` 等 7 个 `public static final int` 一并删除,迁移文档只提了 `setAreaCode(int)` → `setAreaCode(AreaCode)`。若 RN 侧把 areaCode 作为 number 透传,需要改为枚举值映射。 +9. **`EMLoginExtensionInfo` 未变化**:`onLogout(int, EMLoginExtensionInfo)` 在 4.24.1 已是 default 方法(内部代理到旧回调),5.0.0 仅去掉代理;该类本身 diff 无变化。 +10. **根包其余文件**:`EMCallBack`、`EMValueCallBack`、`EMResultCallBack`、`EMLogListener` diff 为空;`EMError` diff 为空(无错误码变更)。`EMSmartHeartBeat`、`EMConversationFilter`、`EMCursorResult`、`EMDeviceInfo`、`EMFetchMessageOption`、`EMGroupInfo`、`EMSessionManager` 仅有内部实现/判空/AndroidX import 调整,无公开 API 变化。 + +## 4. 统计 + +| 类型 | 条数 | +|---|---| +| new_api | 11 | +| new_type | 3 | +| new_enum | 3 | +| new_listener | 4 | +| new_property | 1(VERSION 变更) | +| removed(方法) | 30 | +| removed_type | 6(EMGroupStyle、EMGroupOptions、EMGroupReadAck、EMCheckType、EMStatisticsManager、EMMessageStatistics) | +| removed_listener | 12 | +| renamed | 6(isAcked→isPeerRead、isUnread→isRead、isNeedGroupAck→isNeedReadReceipt、groupAckCount→readReceiptCount、isMemberOnly→isJoinApprovalRequired、removeChatRoomListener→removeChatRoomChangeListener) | +| param_changed | 6 | +| **合计** | **82** | + +未匹配/疑点清单:**10 条**。 + +> 说明:removed 方法数把同一名称的多个重载合并计为一条(如 `getJoinedGroupsFromServer` 4 个重载计 1 条);若按单个方法签名计,删除的方法签名共 47 个。统计口径以清单表格行为准。 diff --git a/docs/porting/5.0.0/01-api-diff-ios.md b/docs/porting/5.0.0/01-api-diff-ios.md new file mode 100644 index 00000000..76d3040e --- /dev/null +++ b/docs/porting/5.0.0/01-api-diff-ios.md @@ -0,0 +1,257 @@ +# iOS 公开 API Diff:4.24.1 → 5.0.0 + +## 1. 头部信息 + +- **diff 锚点**: + - `4.24.1` = `ba235111d262f52d0d4aaeb8045a1b25b1ef1929` + - `5.0.0` = `f8f506ccb009db38d47280ab713a65e468287f4b` +- **中间 tag 核对**:`git tag -l` 显示 4.24.1 与 5.0.0 之间仅有 `4.24.2`(`90bb2d911f6ca2b02355960b581f5703927fbf4a`)。核对结论: + - `4.24.1 → 4.24.2` 仅改动 `UserInfoManager/EMUserInfoManager.mm`(+2 行实现代码),**无任何公开头文件变化**。 + - `4.24.2` 不在 5.0.0 的祖先链上(`git merge-base --is-ancestor 4.24.2 5.0.0` 为否,merge-base 为 `4141d8db6`),即 4.24.2 是侧分支 hotfix tag;其唯一改动在 5.0.0 主线中不存在(`4.24.2 → 5.0.0` 的 diff 显示该 2 行被"减回去")。 + - 结论:**无跳版遗漏**,`4.24.1 → 5.0.0` 的直接 diff 覆盖全部公开 API 变化。 +- **diff 范围**:`newSDK/HyphenateSDK/` 下公开头文件(`EM*.h`、`IEM*.h`),排除 `*+Private.h` / `EMChatMessage+Private.h` 等内部头文件与所有 `.mm` 实现文件。已用 `git ls-tree` 分别列出 4.24.1 与 5.0.0 的全部头文件并比对,5.0.0 目录结构无新增公开目录;`pushExtension/`、`translate/` 两个目录(含公开头文件)在两 tag 间**零改动**,不在本清单内。 +- **迁移文档**:`/Users/asterisk/Codes/zuoyu/easemob-doc/docs/document/ios/migration_guide.md`(下表"迁移文档依据"列引用其章节名)。 +- **总体规模**:58 个文件变更,+1022 / −4667 行(含实现)。公开头文件层面:新增 4 个头文件(`EMConversationDelegate.h`、`EMMessageReadReceipt.h`、`EMMessageReadReceipt+Private.h`(内部)、`EMGroupConfigs.h`),删除 4 个头文件(`EMGroupOptions.h`、`Statistics/` 下 3 个),改名 1 个(`EMGroupMessageAck.h` → `EMGroupReadReceipt.h`)。 + +## 2. 变更清单 + +### client(EMClient.h / EMClientDelegate.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| client_register_with_username | removed | `- (EMError *_Nullable)registerWithUsername:(NSString * _Nonnull)aUsername password:(NSString * _Nonnull)aPassword;` | 初始化与登录-密码登录下线 | 同步注册 | +| client_register_with_username_completion | removed | `- (void)registerWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSString * _Nonnull aUsername, EMError * _Nullable aError))aCompletionBlock;` | 同上 | 异步注册;改用服务端 REST | +| client_fetch_token_with_username_password | removed | `- (void)fetchTokenWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSString * _Nullable aToken, EMError * _Nullable aError))aCompletionBlock;` | 同上 | | +| client_login_with_username_password | removed | `- (EMError *_Nullable)loginWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword EM_DEPRECATED_IOS(3_0_0, 4_5_0,"Use -EMClient loginWithUsername:token:completion instead");` | 同上 | 4.x 已 deprecated | +| client_login_with_username_password_completion | removed | `- (void)loginWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSString * _Nonnull aUsername, EMError *_Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(3_0_0, 4_5_0,"Use -EMClient loginWithUsername:token:completion instead");` | 同上 | | +| client_login_with_username_token_sync | removed | `- (EMError *_Nullable)loginWithUsername:(NSString *_Nonnull)aUsername token:(NSString *_Nonnull)aToken;` | 同上 | 同步 token 登录;5.0.0 仅保留 `loginWithUsername:token:completion:` | +| client_login_with_username_agora_token | removed | `- (EMError *_Nullable)loginWithUsername:(NSString *_Nonnull)aUsername agoraToken:(NSString *_Nonnull)aAgoraToken EM_DEPRECATED_IOS(3_8_9, 4_2_0,"Use -EMClient loginWithUsername:token instead");` | 同上 | | +| client_login_with_username_agora_token_completion | removed | `- (void)loginWithUsername:(NSString *_Nonnull)aUsername agoraToken:(NSString *_Nonnull)aAgoraToken completion:(void (^_Nullable)(NSString * _Nonnull aUsername, EMError *_Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(3_8_9, 4_2_0,"Use -EMClient loginWithUsername:token:completion instead");` | 同上 | | +| client_service_check | removed | `- (void)serviceCheckWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(EMServerCheckType aType, EMError *_Nullable aError))aCompletionBlock __deprecated_msg("This method is deprecated");` | 同上 | 连同 `EMServerCheckType` 枚举(5 个值)一起删除 | +| client_server_check_type_enum | removed | `typedef NS_ENUM(NSInteger, EMServerCheckType) { EMServerCheckAccountValidation = 0, EMServerCheckGetDNSListFromServer, EMServerCheckGetTokenFromServer, EMServerCheckDoLogin, EMServerCheckDoLogout, };` | 同上 | | +| client_is_auto_login | removed | `@property(nonatomic, readonly) BOOL isAutoLogin;` | 初始化与登录-自动登录移除 | | +| client_statistics_manager | removed | `@property (nonatomic, strong, readonly) id _Nullable statisticsManager;` 及 `#import "IEMStatisticsManager.h"` | 其他删除的 API-无客户端替代 | 整个 Statistics 模块删除 | +| client_get_logged_in_devices_username_password | removed | `- (NSArray *_Nullable)getLoggedInDevicesFromServerWithUsername:(NSString * _Nonnull)aUsername password:(NSString * _Nonnull)aPassword error:(EMError ** _Nullable)pError;` 与 `- (void)getLoggedInDevicesFromServerWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSArray * _Nullable aList, EMError *_Nullable aError))aCompletionBlock;` | 设备管理与鉴权 | 替代 `getLoggedInDevicesFromServerWithUserId:token:completion:`(4.24.1 已存在) | +| client_kick_device_username_password | removed | `- (EMError *_Nullable)kickDeviceWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword resource:(NSString *_Nonnull)aResource;` 与 `- (void)kickDeviceWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword resource:(NSString *_Nonnull)aResource completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 替代 `kickDeviceWithUserId:token:resource:completion:`(已存在) | +| client_kick_all_devices_username_password | removed | `- (EMError *_Nullable)kickAllDevicesWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword;` 与 `- (void)kickAllDevicesWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 替代 `kickAllDevicesWithUserId:token:completion:`(已存在) | +| client_get_device_config | param_changed | `- (EMDeviceConfig * _Nullable)getDeviceConfig:(EMError **)pError;`(原 `- (EMDeviceConfig *)getDeviceConfig:(EMError **)pError;`) | | 仅 nullability 标注 | +| client_delegate_auto_login_did_complete | removed | `- (void)autoLoginDidCompleteWithError:(EMError * _Nullable)aError;`(EMClientDelegate) | 监听器回调变化汇总 | | +| client_delegate_user_account_did_login_from_other_device | removed | `- (void)userAccountDidLoginFromOtherDevice:(NSString* _Nullable)aDeviceName EM_DEPRECATED_IOS(4_1_0, 4_7_0, "Use userAccountDidLoginFromOtherDeviceWithInfo: instead");` 与 `- (void)userAccountDidLoginFromOtherDevice EM_DEPRECATED_IOS(3_1_0, 4_1_0, "Use userAccountDidLoginFromOtherDevice: instead");` | 同上 | 替代 `userAccountDidLoginFromOtherDeviceWithInfo:`(4.24.1 已存在) | +| client_delegate_sync_data_start | new_listener | `- (void)syncDataStartWithType:(EMDataSyncType)type;`(EMClientDelegate) | 数据同步与服务端拉取 API 迁移 | | +| client_delegate_sync_data_finished | new_listener | `- (void)syncDataFinished:(EMError * _Nullable)error type:(EMDataSyncType)type;`(EMClientDelegate) | 同上 | | +| client_delegate_on_database_opened | new_listener | `- (void)onDatabaseOpened:(EMError * _Nullable)error username:(NSString *_Nonnull)username;`(EMClientDelegate) | 初始化与登录-登录与数据库打开解耦 | | + +### options(EMOptions.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| options_data_sync_type_enum | new_enum | `typedef NS_OPTIONS(NSInteger, EMDataSyncType) { EMDataSyncTypeNone = 0, EMDataSyncTypeConversations = 1 << 0, EMDataSyncTypeContacts = 1 << 1, EMDataSyncTypeJoinedGroups = 1 << 2, };` | 数据同步与服务端拉取 API 迁移 | | +| options_data_sync_type | new_property | `@property (nonatomic, assign) EMDataSyncType dataSyncType;` | 同上 | 默认值 `EMDataSyncTypeConversations`;须在 initialize 前配置 | +| options_is_auto_login | removed | `@property(nonatomic, assign) BOOL isAutoLogin;` | 初始化与登录-自动登录移除 | | +| options_enable_require_read_ack | removed | `@property(nonatomic, assign) BOOL enableRequireReadAck;` | 已读回执体系重构 | 全局已读回执开关,改为按消息 `isNeedReadReceipt` | +| options_enable_auto_sync_contacts | removed | `@property (nonatomic) BOOL enableAutoSyncContacts;` | 数据同步与服务端拉取 API 迁移 | 由 `dataSyncType` 取代(同一位置改写) | + +### multidevices(EMMultiDevicesDelegate.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| multidevice_event_conversation_unread_message_count_cleared | new_enum | `EMMultiDevicesEventConversationUnreadMessageCountCleared = 65,` | 已读回执体系重构-多设备事件 | | +| multidevice_event_all_conversation_unread_message_count_cleared | new_enum | `EMMultiDevicesEventAllConversationUnreadMessageCountCleared = 66,` | 同上 | | + +### chat(IEMChatManager.h / EMChatManagerDelegate.h / EMConversationDelegate.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| chat_add_conversation_delegate | new_api | `- (void)addConversationDelegate:(id _Nullable)aDelegate delegateQueue:(dispatch_queue_t _Nullable)aQueue NS_SWIFT_NAME(addConversation(delegate:queue:));` 与 `- (void)removeConversationDelegate:(id _Nonnull)aDelegate NS_SWIFT_NAME(removeConversation(delegate:));` | 主要新增 API | delegate 参数标 `_Nullable` 不寻常,见疑点清单 | +| chat_get_unread_message_count | new_api | `- (NSInteger)getUnreadMessageCount;` | 主要新增 API / 行为变化 1 | 不统计聊天室与免打扰会话 | +| chat_get_conversations_from_server | removed | `- (void)getConversationsFromServer:(void (^_Nullable)(NSArray * _Nullable aConversations, EMError * _Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(4_0_0, ...);` | 服务端拉取 API 迁移 | | +| chat_get_conversations_from_server_by_page | removed | `- (void)getConversationsFromServerByPage:(NSUInteger)pageNumber pageSize:(NSUInteger)pageSize completion:(void (^_Nullable)(NSArray * _Nullable aConversations, EMError * _Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(4_0_0, ...);` | 同上 | | +| chat_get_conversations_from_server_with_cursor | removed | `- (void)getConversationsFromServerWithCursor:(nullable NSString *)cursor pageSize:(UInt8)pageSize completion:(nonnull void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock;` | 同上 | | +| chat_get_pinned_conversations_from_server | removed | `- (void)getPinnedConversationsFromServerWithCursor:(nullable NSString *)cursor pageSize:(UInt8)limit completion:(nonnull void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock;` | 同上 | 改读本地 `EMConversation.isPinned`/`pinnedTime`(4.24.1 已有) | +| chat_get_conversations_from_server_with_cursor_filter | removed | `- (void)getConversationsFromServerWithCursor:(NSString * _Nullable)cursor filter:(EMConversationFilter* _Nonnull)filter completion:(nonnull void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock;` | 同上 | 本地替代 `filterConversationsFromDB:filter:`(4.24.1 已有) | +| chat_import_conversations | removed | `- (void)importConversations:(NSArray * _Nullable)aConversations completion:(void (^ _Nullable)(EMError * _Nullable aError))aCompletionBlock;` | 其他删除的 API-无客户端替代 | | +| chat_modify_message_body | removed | `- (void)modifyMessage:(NSString *_Nonnull)messageId body:(EMMessageBody *_Nonnull)body completion:(void (^_Nonnull)(EMError * _Nullable error,EMChatMessage *_Nullable message))completionBlock;` | 其他删除的 API-有替代方式 | 替代 `modifyMessage:body:ext:completion:`(4.24.1 已存在,非 5.0.0 新增) | +| chat_send_message_read_ack | removed | `- (void)sendMessageReadAck:(NSString * _Nonnull)aMessageId toUser:(NSString * _Nonnull)aUsername completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 已读回执体系重构 | 由 `sendMessageReadReceipts:completion:` 取代 | +| chat_send_group_message_read_ack | removed | `- (void)sendGroupMessageReadAck:(NSString * _Nonnull)aMessageId toGroup:(NSString * _Nonnull)aGroupId content:(NSString * _Nullable)aContent completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 群回执不再支持 `content` 自定义内容 | +| chat_send_message_read_receipts | new_api | `- (void)sendMessageReadReceipts:(NSArray * _Nonnull)aMessages completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 批量(≤50 条、同会话);单聊群聊统一;注释与签名不一致,见疑点 | +| chat_ack_conversation_read | removed | `- (void)ackConversationRead:(NSString * _Nonnull)conversationId completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 拆为 clear unread + send receipts 两步 | +| chat_clear_conversation_unread_message_count | new_api | `- (void)clearConversationUnreadMessageCount:(NSString * _Nonnull)aConversationId completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 清本地未读并同步多设备,不发回执 | +| chat_clear_all_conversation_unread_message_count | new_api | `- (void)clearAllConversationUnreadMessageCount:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 同上 | 取代 `markAllConversationsAsRead` | +| chat_get_group_message_read_receipts | new_api | `- (void)getGroupMessageReadReceipts:(NSArray * _Nonnull)aMessages completion:(void (^_Nullable)(NSArray *_Nullable aReceipts, EMError *_Nullable aError))aCompletionBlock;` | 已读回执体系重构-回执详情查询 | ≤20 条、同会话 | +| chat_resend_message | removed | `- (void)resendMessage:(EMChatMessage *_Nonnull)aMessage progress:(void (^_Nullable)(int progress))aProgressBlock completion:(void (^_Nullable)(EMChatMessage *_Nullable message, EMError *_Nullable error))aCompletionBlock;` | 其他删除的 API-有替代方式 | 改用 `sendMessage:progress:completion:` | +| chat_fetch_history_messages_from_server | removed | `- (EMCursorResult *_Nullable)fetchHistoryMessagesFromServer:(NSString *_Nonnull)aConversationId conversationType:(EMConversationType)aConversationType startMessageId:(NSString *_Nullable)aStartMessageId fetchDirection:(EMMessageFetchHistoryDirection)direction pageSize:(int)aPageSize error:(EMError **_Nullable)pError __deprecated_msg(...)` 及无 `fetchDirection` 的同步重载(共 2 个同步方法) | 同上 | 替代 `fetchMessagesFromServerBy:conversationType:cursor:pageSize:option:completion:`(4.24.1 已存在) | +| chat_async_fetch_history_messages_from_server | removed | `- (void)asyncFetchHistoryMessagesFromServer:(NSString *_Nonnull)aConversationId conversationType:(EMConversationType)aConversationType startMessageId:(NSString *_Nullable)aStartMessageId pageSize:(int)aPageSize completion:...` 及带 `fetchDirection:` 的重载(共 2 个异步方法,均 `__deprecated_msg`) | 同上 | | +| chat_fetch_group_message_acks | renamed | 旧:`- (void)asyncFetchGroupMessageAcksFromServer:(NSString *_Nonnull)aMessageId groupId:(NSString *_Nonnull)aGroupId startGroupAckId:(NSString *_Nonnull)aGroupAckId pageSize:(int)aPageSize completion:(void (^_Nullable)(EMCursorResult *_Nullable aResult, EMError *_Nullable error, int totalCount))aCompletionBlock;` → 新:`- (void)asyncFetchGroupMessageReadUsersFromServer:(NSString *_Nonnull)aMessageId groupId:(NSString *_Nonnull)aGroupId readReceiptId:(NSString *_Nonnull)aReadReceiptId pageSize:(int)aPageSize completion:(void (^_Nullable)(EMCursorResult *_Nullable aResult, EMError *_Nullable error, int totalCount))aCompletionBlock;` | 已读回执体系重构-回执详情查询 | 方法名 + 参数名 + 泛型类型全变 | +| chat_report_message | removed | `- (void)reportMessageWithId:(NSString *_Nonnull )aMessageId tag:(NSString *_Nonnull)aTag reason:(NSString *_Nonnull)aReason completion:(void(^_Nullable)(EMError* _Nullable error))aCompletion;` | 其他删除的 API-无客户端替代 | 举报功能下线,走 App Server | +| chat_load_messages_with_type | removed | `- (NSArray * _Nullable)loadMessagesWithType:(EMMessageBodyType)aType timestamp:(long long)aTimestamp count:(int)aCount fromUser:(NSString* _Nullable)aUsername searchDirection:(EMMessageSearchDirection)aDirection;` 及 completion 异步重载(IEMChatManager 上共 2 个) | 其他删除的 API-有替代方式 | `EMConversation` 上的同名同步+异步方法两个 tag 均存在,未动 | +| chat_load_messages_with_keyword_legacy | removed | `- (NSArray *)loadMessagesWithKeyword:(NSString*)aKeywords timestamp:(long long)aTimestamp count:(int)aCount fromUser:(NSString*)aSender searchDirection:(EMMessageSearchDirection)aDirection;` 及对应 completion 重载(无 `scope` 参数的 2 个旧重载) | 同上 | 保留带 `scope:` 的版本 | +| chat_load_messages_with_keyword_scope | param_changed | `- (void)loadMessagesWithKeyword:(NSString* _Nonnull)aKeywords timestamp:(long long)aTimestamp count:(int)aCount fromUser:(NSString *_Nullable)aSender searchDirection:(EMMessageSearchDirection)aDirection scope:(EMMessageSearchScope)aScope completion:(void (^_Nullable)(NSArray *_Nullable aMessages, EMError *_Nullable aError))aCompletionBlock;` | | 仅 nullability 标注收紧(选择器不变) | +| chat_mark_all_conversations_as_read | removed | `- (EMError*)markAllConversationsAsRead;` | 已读回执体系重构 | 由 `clearAllConversationUnreadMessageCount:` 取代 | +| chat_delete_messages_before | param_changed | `- (void)deleteMessagesBefore:(NSUInteger)aTimestamp completion:(void(^_Nullable)(EMError*_Nullable error))aCompletion;`(原 completion 无 nullability 标注) | | 仅标注变化 | +| chat_delegate_conversation_list_did_update | removed | `- (void)conversationListDidUpdate:(NSArray * _Nonnull)aConversationList;`(自 EMChatManagerDelegate 删除) | 监听器回调变化汇总 | 迁移到新协议 `EMConversationDelegate` | +| chat_delegate_messages_did_read | removed | `- (void)messagesDidRead:(NSArray * _Nonnull)aMessages;` | 同上 | 由 `onMessageReadReceipts:` 取代 | +| chat_delegate_group_message_did_read | removed | `- (void)groupMessageDidRead:(NSArray * _Nonnull)aGroupAcks;` 与 `- (void)groupMessageDidRead:(EMChatMessage * _Nonnull)aMessage groupAcks:(NSArray * _Nonnull)aGroupAcks __deprecated_msg("Use -groupMessageDidRead: instead");` | 同上 | 两个重载均删 | +| chat_delegate_group_message_ack_has_changed | removed | `- (void)groupMessageAckHasChanged;` | 同上 | | +| chat_delegate_on_conversation_read | removed | `- (void)onConversationRead:(NSString * _Nonnull)from to:(NSString * _Nonnull)to;` | 同上 | 无直接替代 | +| chat_delegate_on_message_read_receipts | new_listener | `- (void)onMessageReadReceipts:(NSArray * _Nonnull)aReceipts;`(EMChatManagerDelegate) | 同上 | 单聊群聊统一回执回调 | +| conversation_delegate_protocol | new_type | `@protocol EMConversationDelegate @optional - (void)conversationListDidUpdate:(NSArray * _Nonnull)aConversationList; @end`(新文件 `EMConversationDelegate.h`) | 监听器回调变化汇总 | 通过 `addConversationDelegate:delegateQueue:` 注册 | + +### message / conversation 模型(EMChatMessage.h / EMConversation.h / Body / EMStreamChunk.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| message_is_read_acked | renamed | 旧:`@property (nonatomic) BOOL isReadAcked;` → 新:`@property (nonatomic,readonly) BOOL isPeerRead;` | 已读回执体系重构-EMMessage 已读相关方法重命名 | 同时变为只读 | +| message_is_need_group_ack | renamed | 旧:`@property (nonatomic) BOOL isNeedGroupAck;` → 新:`@property (nonatomic) BOOL isNeedReadReceipt;` | 同上 | 语义从群聊扩展为单群聊通用,默认 NO | +| message_group_ack_count | renamed | 旧:`@property (nonatomic, readonly) int groupAckCount;` → 新:`@property (nonatomic, readonly) int groupReadReceiptCount;` | 同上 | | +| message_is_read | param_changed | `@property (nonatomic,readonly) BOOL isRead;`(原 `@property (nonatomic) BOOL isRead;`) | 同上 / 行为变化 4 | setter 删除,应用不可再直接改已读态 | +| message_get_reaction | removed | `- (EMMessageReaction *_Nullable)getReaction:(NSString * _Nonnull)reaction;` | 其他删除的 API-有替代方式 | 改遍历 `reactionList` | +| conversation_name | new_api | `- (NSString* _Nullable)conversationName;`(EMConversation 方法而非属性) | 主要新增 API | 单聊返回对方昵称,群聊返回群名称 | +| conversation_avatar | new_api | `- (NSString* _Nullable)conversationAvatar;`(EMConversation 方法) | 同上 | | +| conversation_mark_message_as_read | removed | `- (void)markMessageAsReadWithId:(NSString *_Nonnull)aMessageId error:(EMError ** _Nullable)pError;` | 已读回执体系重构 | | +| conversation_mark_all_messages_as_read | removed | `- (void)markAllMessagesAsRead:(EMError ** _Nullable)pError;` | 同上 | | +| file_message_body_init_with_data | removed | `- (instancetype _Nonnull)initWithData:(NSData *_Nullable)aData displayName:(NSString *_Nullable)aDisplayName;`(EMFileMessageBody) | 同上 / 行为变化 5 | 附件不再接受 NSData 初始化 | +| file_message_body_init_with_local_path | param_changed | `- (instancetype _Nonnull)initWithLocalPath:(NSString * _Nonnull)aLocalPath displayName:(NSString * _Nonnull)aDisplayName;`(原两参数均 `_Nullable`) | | nullability 收紧为 nonnull | +| image_message_body_init_with_data | removed | `- (instancetype)initWithData:(NSData *)aData thumbnailData:(NSData *)aThumbnailData __deprecated_msg("Use -initWithLocalPath:displayName: instead");`(EMImageMessageBody) | 其他删除的 API-有替代方式 | | +| stream_chunk_sequence_number | renamed | 旧:`@property (nonatomic) long sequenceNumber;` → 新:`@property (nonatomic, readonly) BOOL isComplete;`(EMStreamChunk) | 同上 | 语义改变:不再暴露分片序号,改为是否完成 | +| group_message_ack_type | renamed | 旧:`@interface EMGroupMessageAck : NSObject`,属性 `messageId`/`readAckId`/`from`(NSString*)/`content`/`readCount`/`timestamp` → 新:`@interface EMGroupReadReceipt : NSObject`,属性 `messageId`/`readReceiptId`/`from`(**EMGroupMemberInfo***)/`readCount`/`timestamp`(`content` 删除) | 已读回执体系重构-回执详情查询 | 文件改名 `EMGroupMessageAck.h`→`EMGroupReadReceipt.h`;`from` 由字符串改为对象 | +| message_read_receipt_type | new_type | `@interface EMMessageReadReceipt : NSObject`,只读属性 `messageId`/`conversationId`/`isPeerReceipt`(BOOL)/`readCount`(NSInteger)(新文件 `EMMessageReadReceipt.h`) | 同上 | 统一单群聊回执模型 | + +### group(EMGroup.h / EMGroupConfigs.h / EMGroupOptions.h / IEMGroupManager.h / EMGroupManagerDelegate.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| group_options_type | removed | `@interface EMGroupOptions : NSObject`(属性 `style`/`maxUsers`/`IsInviteNeedConfirm`/`ext`)及 `typedef NS_ENUM(NSInteger, EMGroupStyle) { EMGroupStylePrivateOnlyOwnerInvite = 0, EMGroupStylePrivateMemberCanInvite, EMGroupStylePublicJoinNeedApproval, EMGroupStylePublicOpenJoin, };`(`EMGroupOptions.h` 整文件删除) | 群组配置模型重构 | 由 `EMGroupConfigs` 取代,无兼容层 | +| group_configs_type | new_type | `@interface EMGroupConfigs : NSObject`,属性 `maxUsers`/`IsInviteNeedConfirm`/`ext`/`allowInvites`/`joinApprovalRequired`/`isPublic`(新文件 `EMGroupConfigs.h`) | 同上 | style 枚举拆为 3 个 BOOL;默认 isPublic=NO/joinApprovalRequired=NO/allowInvites=NO/maxUsers=200/IsInviteNeedConfirm=YES/ext=@"" | +| group_configs_type_enum | new_enum | `typedef NS_OPTIONS(NSUInteger, EMGroupConfigsType) { EMGroupConfigsTypeAllowInvites = 1 << 0, EMGroupConfigsTypeMaxUsers = 1 << 1, EMGroupConfigsTypeInviteNeedConfirm = 1 << 2, EMGroupConfigsTypeJoinApprovalRequired = 1 << 3, EMGroupConfigsTypeIsPublic = 1 << 4, EMGroupConfigsTypeExt = 1 << 5, };` | 同上 | 供 `updateGroupWithId:types:configs:completion:` 按位指定更新字段 | +| group_settings_property | param_changed | `@property (nonatomic, strong, readonly) EMGroupConfigs *settings;`(原 `EMGroupOptions *settings`,EMGroup) | 同上 | 属性名不变,类型改变 | +| group_is_push_notification_enabled | removed | `@property (nonatomic, readonly) BOOL isPushNotificationEnabled;`(EMGroup) | 其他删除的 API-有替代方式 | 改查 `EMConversation.disturbType` 或 `getSilentModeForConversation:...` | +| group_create_group_sync | removed | `- (EMGroup * _Nullable)createGroupWithSubject:(NSString *_Nullable)aSubject description:(NSString *_Nullable)aDescription invitees:(NSArray * _Nullable)aInvitees message:(NSString *_Nullable)aMessage setting:(EMGroupOptions *_Nullable)aSetting error:(EMError **_Nullable)pError;` | 群组配置模型重构-相关 API 变化 | | +| group_create_group_completion_no_avatar | removed | `- (void)createGroupWithSubject:(NSString *_Nullable)aSubject description:(NSString *_Nullable)aDescription invitees:(NSArray * _Nullable)aInvitees message:(NSString *_Nullable)aMessage setting:(EMGroupOptions *_Nullable)aSetting completion:(void (^_Nullable)(EMGroup *_Nullable aGroup, EMError *_Nullable aError))aCompletionBlock;` | 同上 | 无 avatar 参数的异步版删除 | +| group_create_group_with_avatar | param_changed | `- (void)createGroupWithSubject:... avatar:... setting:(EMGroupConfigs *)setting completion:...`(选择器不变,`setting` 类型 EMGroupOptions* → EMGroupConfigs*) | 同上 | | +| group_update_group_with_id | new_api | `- (void)updateGroupWithId:(NSString *_Nonnull)groupId types:(EMGroupConfigsType)type configs:(EMGroupConfigs *_Nonnull)configs completion:(void (^_Nullable)(EMGroup *_Nullable group, EMError *_Nullable error))aCompletionBlock;` | 同上 / 主要新增 API | 建群后按字段更新群配置 | +| group_get_public_groups_from_server | removed | `- (EMCursorResult *_Nullable)getPublicGroupsFromServerWithCursor:(NSString *_Nullable)aCursor pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` 及 completion 版本 | 其他删除的 API-无客户端替代 | | +| group_search_public_group | removed | `- (EMGroup * _Nullable)searchPublicGroupWithId:(NSString *_Nonnull)aGroundId error:(EMError **_Nullable)pError;` 及 completion 版本 | 同上 | | +| group_get_joined_groups_from_server_with_page | removed | `- (void)getJoinedGroupsFromServerWithPage:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize needMemberCount:(BOOL)aNeedMemberCount needRole:(BOOL)aNeedRole completion:(void (^_Nullable)(NSArray *_Nullable aList, EMError *_Nullable aError))aCompletionBlock;` | 服务端拉取 API 迁移 | 改读本地 `getJoinedGroups` | +| group_get_groups_without_push_notification | removed | `- (NSArray *)getGroupsWithoutPushNotification:(EMError **)pError EM_DEPRECATED_IOS(3_3_2, 3_8_3, "Use -IEMPushManager::noPushGroups");` | 其他删除的 API-有替代方式 | | +| group_get_group_specification_sync | removed | `- (EMGroup * _Nullable)getGroupSpecificationFromServerWithId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` 与 `- (EMGroup * _Nullable)getGroupSpecificationFromServerWithId:(NSString *_Nonnull)aGroupId fetchMembers:(BOOL)fetchMembers error:(EMError **_Nullable)pError;` | 同上 | completion 版本已存在 | +| group_get_group_member_list_sync | removed | `- (EMCursorResult *)getGroupMemberListFromServerWithId:(NSString *_Nonnull)aGroupId cursor:(NSString *_Nullable)aCursor pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 同上 | | +| group_get_group_blacklist_sync | removed | `- (NSArray * _Nullable)getGroupBlacklistFromServerWithId:(NSString *_Nonnull)aGroupId pageNumber:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 同上 | | +| group_get_group_mute_list_sync | removed | `- (NSArray * _Nullable)getGroupMuteListFromServerWithId:(NSString *_Nonnull)aGroupId pageNumber:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 同上 | | +| group_get_group_file_list_sync | removed | `- (NSArray *_Nullable)getGroupFileListWithId:(NSString *_Nonnull)aGroupId pageNumber:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 同上 | | +| group_get_group_white_list_sync | removed | `- (NSArray *)getGroupWhiteListFromServerWithId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_is_member_in_white_list_sync | removed | `- (BOOL)isMemberInWhiteListFromServerWithGroupId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | completion 版本保留(仅位置/格式微调) | +| group_get_group_announcement_sync | removed | `- (NSString *_Nullable)getGroupAnnouncementWithId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | completion 版本保留 | +| group_add_occupants_sync | removed | `- (EMGroup * _Nullable)addOccupants:(NSArray * _Nonnull)aOccupants toGroup:(NSString *_Nonnull)aGroupId welcomeMessage:(NSString *_Nullable)aWelcomeMessage error:(EMError **_Nullable)pError;` | 同上 | 替代 `addMembers:toGroup:message:completion:`(4.24.1 已有) | +| group_remove_occupants_sync | removed | `- (EMGroup * _Nullable)removeOccupants:(NSArray * _Nonnull)aOccupants fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_block_occupants_sync | removed | `- (EMGroup * _Nullable)blockOccupants:(NSArray * _Nonnull)aOccupants fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_unblock_occupants_sync | removed | `- (EMGroup * _Nullable)unblockOccupants:(NSArray * _Nonnull)aOccupants forGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_change_group_subject_sync | removed | `- (EMGroup * _Nullable)changeGroupSubject:(NSString *_Nullable)aSubject forGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | 替代 `updateGroupSubject:forGroup:completion:`(已有) | +| group_change_description_sync | removed | `- (EMGroup * _Nullable)changeDescription:(NSString *_Nullable)aDescription forGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | 替代 `updateDescription:forGroup:completion:`(已有) | +| group_leave_group_sync | removed | `- (void)leaveGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_destroy_group_sync | removed | `- (EMError *)destroyGroup:(NSString *_Nonnull)aGroupId;` | 同上 | 替代 `destroyGroup:finishCompletion:`(已有) | +| group_block_group_sync | removed | `- (EMGroup * _Nullable)blockGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_unblock_group_sync | removed | `- (EMGroup * _Nullable)unblockGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_update_group_owner_sync | removed | `- (EMGroup * _Nullable)updateGroupOwner:(NSString *_Nonnull)aGroupId newOwner:(NSString *_Nonnull)aNewOwner error:(EMError **_Nullable)pError;` | 同上 | | +| group_add_admin_sync | removed | `- (EMGroup * _Nullable)addAdmin:(NSString *_Nonnull)aAdmin toGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_remove_admin_sync | removed | `- (EMGroup * _Nullable)removeAdmin:(NSString *_Nonnull)aAdmin fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_mute_members_sync | removed | `- (EMGroup * _Nullable)muteMembers:(NSArray * _Nonnull)aMuteMembers muteMilliseconds:(NSInteger)aMuteMilliseconds fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_unmute_members_sync | removed | `- (EMGroup * _Nullable)unmuteMembers:(NSArray * _Nonnull)aMembers fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | completion 版本保留(仅标注微调) | +| group_mute_all_members_sync | removed | `- (EMGroup * _Nullable)muteAllMembersFromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_unmute_all_members_sync | removed | `- (EMGroup * _Nullable)unmuteAllMembersFromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_add_white_list_members_sync | removed | `- (EMGroup * _Nullable)addWhiteListMembers:(NSArray * _Nonnull)aMembers fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_remove_white_list_members_sync | removed | `- (EMGroup * _Nullable)removeWhiteListMembers:(NSArray * _Nonnull)aMembers fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | | +| group_remove_group_shared_file_sync | removed | `- (EMGroup * _Nullable)removeGroupSharedFileWithId:(NSString *_Nonnull)aGroupId sharedFileId:(NSString *_Nonnull)aSharedFileId error:(EMError **_Nullable)pError;` | 同上 | | +| group_update_group_announcement_sync | removed | `- (EMGroup * _Nullable)updateGroupAnnouncementWithId:(NSString *_Nonnull)aGroupId announcement:(NSString *_Nullable)aAnnouncement error:(EMError **_Nullable)pError;` | 同上 | | +| group_update_group_ext_sync | removed | `- (EMGroup * _Nullable)updateGroupExtWithId:(NSString *_Nonnull)aGroupId ext:(NSString *_Nullable)aExt error:(EMError **_Nullable)pError;` | 同上 | | +| group_join_public_group_sync | removed | `- (EMGroup * _Nullable)joinPublicGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 同上 | completion 版本保留(仅位置移动) | +| group_apply_join_public_group | renamed | 旧:`- (EMGroup * _Nullable)applyJoinPublicGroup:(NSString *_Nonnull)aGroupId message:(NSString *_Nullable)aMessage error:(EMError **_Nullable)pError;`(同步)→ 新:`- (void)requestToJoinPublicGroup:(NSString *_Nonnull)aGroupId message:(NSString *_Nullable)aMessage completion:(void (^_Nullable)(EMGroup *_Nullable aGroup, EMError *_Nullable aError))aCompletionBlock;`(4.24.1 已有,5.0.0 仅位置移动) | 同上 | 同步旧版删除,异步新版名字不同且早已存在 | +| group_accept_join_application | renamed | 旧:`- (EMError *)acceptJoinApplication:(NSString *_Nonnull)aGroupId applicant:(NSString *_Nonnull)aUsername;`(同步,删除)→ 新:`approveJoinGroupRequest:sender:completion:`(4.24.1 已有) | 同上 | | +| group_decline_join_application | renamed | 旧:`- (EMError *)declineJoinApplication:(NSString *_Nonnull)aGroupId applicant:(NSString *_Nonnull)aUsername reason:(NSString *_Nullable)aReason;`(同步,删除)→ 新:`declineJoinGroupRequest:sender:reason:completion:`(4.24.1 已有) | 同上 | | +| group_accept_invitation_from_group_sync | removed | `- (EMGroup * _Nullable)acceptInvitationFromGroup:(NSString *_Nonnull)aGroupId inviter:(NSString *_Nonnull)aUsername error:(EMError **_Nullable)pError;` | 同上 | completion 版本已有 | +| group_decline_invitation_from_group | renamed | 旧:`- (EMError *)declineInvitationFromGroup:(NSString *_Nonnull)aGroupId inviter:(NSString *_Nonnull)aUsername reason:(NSString *_Nullable)aReason;`(同步,删除)→ 新:`declineGroupInvitation:inviter:reason:completion:`(4.24.1 已有) | 同上 | | +| group_delegate_join_group_request_did_decline_2param | removed | `- (void)joinGroupRequestDidDecline:(NSString *_Nonnull)aGroupId reason:(NSString *_Nullable)aReason EM_DEPRECATED_IOS(3_1_0, 4_2_0, "Use -joinGroupRequestDidDecline:reason:applicant: instead");` | 监听器回调变化汇总 | 保留 `joinGroupRequestDidDecline:reason:decliner:applicant:`(4.24.1 已有) | +| group_delegate_join_group_request_did_decline_3param | removed | `- (void)joinGroupRequestDidDecline:(NSString *_Nonnull)aGroupId reason:(NSString *_Nullable)aReason applicant:(NSString* _Nonnull )aApplicant;` | 同上 | | +| group_delegate_user_did_join_group_single | removed | `- (void)userDidJoinGroup:(EMGroup *_Nonnull)aGroup user:(NSString *_Nonnull)aUsername EM_DEPRECATED_IOS(3_1_0, 4_15_0, "Use -userDidJoinGroup:users: instead");` | 同上 | 保留 `userDidJoinGroup:users:`(已有) | +| group_delegate_user_did_leave_group_single | removed | `- (void)userDidLeaveGroup:(EMGroup *_Nonnull)aGroup user:(NSString *_Nonnull)aUsername EM_DEPRECATED_IOS(3_1_0, 4_15_0, "Use -userDidLeaveGroup:users: instead");` | 同上 | 保留 `userDidLeaveGroup:users:`(已有) | + +### contact(IEMContactManager.h / EMContactManagerDelegate.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| contact_get_all_contacts_from_server | removed | `- (void)getAllContactsFromServerWithCompletion:(void (^_Nullable)(NSArray* _Nullable aList, EMError* _Nullable aError))aCompletionBlock;` | 服务端拉取 API 迁移 | 改读本地 `getAllContacts`/`getContacts`/`getContact:` | +| contact_get_contacts_from_server_with_cursor | removed | `- (void)getContactsFromServerWithCursor:(NSString* _Nullable)cursor pageSize:(NSUInteger)pageSize completion:(void (^_Nonnull)(EMCursorResult * _Nullable aResult, EMError * _Nullable aError))aCompletionBlock;` | 同上 | | +| contact_get_contacts_from_server_completion | removed | `- (void)getContactsFromServerWithCompletion:(void (^)(NSArray *_Nullable aList, EMError *aError_Nullable ))aCompletionBlock;` | 同上 | | +| contact_get_contacts_from_server_sync | removed | `- (NSArray *_Nullable )getContactsFromServerWithError:(EMError **_Nullable )pError;` | 同上 | | +| contact_add_contact_sync | removed | `- (EMError *_Nullable )addContact:(NSString *_Nonnull)aUsername message:(NSString *_Nullable )aMessage;` | 其他删除的 API-有替代方式 | completion 版本已有 | +| contact_get_blacklist_from_server_sync | removed | `- (NSArray *_Nullable )getBlackListFromServerWithError:(EMError **_Nullable )pError;` | 同上 | 替代 `getBlackListFromServerWithCompletion:`(已有) | +| contact_add_user_to_blacklist_sync | removed | `- (EMError *_Nullable )addUserToBlackList:(NSString *_Nonnull)aUsername;` | 同上 | | +| contact_remove_user_from_blacklist_sync | removed | `- (EMError *_Nullable )removeUserFromBlackList:(NSString *_Nonnull)aUsername;` | 同上 | | +| contact_accept_invitation_sync | removed | `- (EMError *_Nullable )acceptInvitationForUsername:(NSString *_Nonnull)aUsername;` | 同上 | 替代 `approveFriendRequestFromUser:completion:`(已有) | +| contact_decline_invitation_sync | removed | `- (EMError *_Nullable )declineInvitationForUsername:(NSString *_Nonnull)aUsername;` | 同上 | 替代 `declineFriendRequestFromUser:completion:`(已有) | +| contact_get_self_ids_on_other_platform_sync | removed | `- (NSArray *_Nullable )getSelfIdsOnOtherPlatformWithError:(EMError **_Nullable )pError;` | 同上 | 替代 `getSelfIdsOnOtherPlatformWithCompletion:`(已有) | +| contact_save_black_list | new_api | `- (void)saveBlackList:(NSArray *_Nonnull)aBlackList completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 主要新增 API | 批量加黑名单 | +| contact_delegate_on_friend_start_sync | removed | `- (void)onFriendStartSync;`(EMContactManagerDelegate) | 监听器回调变化汇总 | 由 `EMClientDelegate#syncDataStartWithType:` 取代 | +| contact_delegate_on_friend_sync_finished | removed | `- (void)onFriendSyncFinished:(EMError * _Nullable)error;`(EMContactManagerDelegate) | 同上 | 由 `syncDataFinished:type:` 取代 | + +### chatroom(IEMChatroomManager.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| chatroom_create_chatroom | removed | `- (EMChatroom *_Nullable)createChatroomWithSubject:(NSString *_Nullable)aSubject description:(NSString *_Nullable)aDescription invitees:(NSArray *_Nullable)aInvitees message:(NSString *_Nullable)aMessage maxMembersCount:(NSInteger)aMaxMembersCount error:(EMError **_Nullable)pError;` 及 completion 版本 | 其他删除的 API-无客户端替代 | 改用服务端 REST 创建聊天室 | +| chatroom_destroy_chatroom | removed | `- (EMError *_Nullable)destroyChatroom:(NSString *_Nonnull)aChatroomId;` 与 `- (void)destroyChatroom:(NSString *_Nonnull)aChatroomId completion:(void (^_Nullable)(EMError * _Nullable aError))aCompletionBlock;` | 同上 | | + +### push(IEMPushManager.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| push_update_push_display_style_sync | removed | `- (EMError *)updatePushDisplayStyle:(EMPushDisplayStyle)pushDisplayStyle;` | 其他删除的 API-有替代方式 | completion 版本已有 | +| push_update_push_display_name_sync | removed | `- (EMError *_Nullable )updatePushDisplayName:(NSString * _Nonnull)aDisplayName;` | 同上 | | +| push_get_push_options_from_server_sync | removed | `- (EMPushOptions *_Nullable )getPushOptionsFromServerWithError:(EMError *_Nullable *_Nullable)pError;` | 同上 | 替代 `getPushNotificationOptionsFromServerWithCompletion:`(已有) | + +### statistics(Statistics/ 整模块删除) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| statistics_manager_module | removed | `@protocol IEMStatisticsManager`(`getMessageStatisticsById:`、`getMessageCountWithStart:end:direction:type:`、`getMessageStatisticsSizeWithStart:end:direction:type:`)、`@interface EMStatisticsManager : EMManager`、`@interface EMChatMessageStatistics : NSObject`(10 个只读属性)、`EMMessageStatisticsDirection`、`EMMessageStatisticsType` 两个枚举;`Statistics/` 目录 3 个公开头文件全部删除 | 其他删除的 API-无客户端替代 | 消息流量统计功能整体下线 | + +### error code(EMErrorCode.h) + +| id | 类型 | iOS 源签名 | 迁移文档依据 | 备注 | +| --- | --- | --- | --- | --- | +| error_contact_add_faild_typo | renamed | 旧:`EMErrorContactAddFaild = 1000,` → 新:`EMErrorContactAddFailed = 1000,` | 无 | 拼写修正,值不变;RN 侧若按名字映射需同步改名 | + +## 3. 未匹配 / 疑点清单 + +1. **`EMGroup.users` 并非 5.0.0 新增**:迁移文档「主要新增 API」列出 `EMGroup#users`,但 `git grep` 确认 4.24.1 的 `EMGroup.h:212` 已存在 `@property (nonatomic, strong, readonly) NSArray *users;`,且 diff 未触及该属性。可能是语义/文档口径变化(按角色合并不去重),RN 侧若已封装 `users` 则无需改动。 +2. **`sendMessageReadReceipts:completion:` 注释与签名不符**:IEMChatManager.h 新增方法的中文注释描述 `@param aConversationId 会话 ID`、回调含 `aResult` 字典(key 为消息 ID),但实际签名只有 `aMessages` + 单 `aError` 回调,无 `aConversationId` 参数、无 `aResult`。以签名为准;怀疑注释拷贝自内部设计稿。 +3. **`addConversationDelegate:delegateQueue:` 的 delegate 参数标为 `_Nullable`**(`- (void)addConversationDelegate:(id _Nullable)aDelegate ...`),而 `removeConversationDelegate:` 标 `_Nonnull`,疑为标注疏漏。 +4. **大量「5.0.0 替代 API」实际在 4.24.1 已存在**:迁移文档表格中的多数替代接口(`modifyMessage:body:ext:completion:`、`fetchMessagesFromServerBy:...`、`approveJoinGroupRequest:sender:completion:`、`declineJoinGroupRequest:sender:reason:completion:`、`acceptInvitationFromGroup:inviter:completion:`、`declineGroupInvitation:inviter:reason:completion:`、`addMembers:/removeMembers:/blockMembers:/unblockMembers:`、`updateGroupSubject:/updateDescription:`、`addContact:message:completion:`、`approveFriendRequestFromUser:`、`declineFriendRequestFromUser:`、`getSelfIdsOnOtherPlatformWithCompletion:`、`getBlackListFromServerWithCompletion:`、`getLoggedInDevicesFromServerWithUserId:token:completion:`、`kickDeviceWithUserId:token:resource:completion:`、`kickAllDevicesWithUserId:token:completion:`、`updatePushDisplayStyle:completion:`、`updatePushDisplayName:completion:`、`getPushNotificationOptionsFromServerWithCompletion:`、`userAccountDidLoginFromOtherDeviceWithInfo:`、`userDidJoinGroup:users:`、`userDidLeaveGroup:users:`、EMConversation 版 `loadMessagesWithType:...completion:`、`requestToJoinPublicGroup:message:completion:`)经 `git grep` 逐一确认在 4.24.1 与 5.0.0 均存在。**含义:RN 侧工作主要是「删除旧封装」,少量「新增封装」(见类型为 new_api/new_type/new_listener/new_enum/new_property 的行),不存在「改名后需要新增的新接口」——新名字大多早已存在。** +5. **`joinGroupRequestDidReceive:user:reason:`(EMGroupManagerDelegate)在 diff 中同时出现 +/−,签名完全一致**:仅为文件内位置移动(从 deprecated 区附近移到文件顶部),无 API 变化。 +6. **`getBlackList`(IEMContactManager)在 diff 中同时 −/+**:仅空格/标注格式调整(`*_Nullable )getBlackList;` → `*_Nullable)getBlackList;`),无变化。 +7. **`isMemberInWhiteListFromServerWithGroupId:completion:`、`isMemberInMuteListFromServerWithGroupId:completion:`、`getGroupAnnouncementWithId:completion:`、`joinPublicGroup:completion:`、`unmuteMembers:...completion:`、`muteAllMembersFromGroup:completion:`、`unmuteAllMembersFromGroup:completion:`**:diff 中 −/+ 成对出现,仅位置移动或 nullability 标注微调,选择器不变。 +8. **`EMErrorContactAddFaild → EMErrorContactAddFailed` 拼写修正未在迁移文档中提及**(值同为 1000)。RN 侧若用字符串名映射错误码需同步。 +9. **迁移文档称「同步 `renewToken:` 仍在 5.0.0 公共头文件中保留」**:diff 未触及 renewToken 任何版本,与文档一致,无需动作。 +10. **迁移文档提到的本地读取替代接口均未变**:`getAllConversations`/`getAllConversations:`、`getJoinedGroups`、`getBlackList`、`EMConversation.isPinned`/`pinnedTime`、`filterConversationsFromDB:filter:`、`EMConversation.disturbType` 在 4.24.1 均已有且 diff 未触及。 +11. **`EMClientDelegate.h` 的 import 由 `EMCommonDefs.h` 改为 `EMOptions.h`**:因新回调参数使用 `EMDataSyncType`;对 RN 封装无直接影响。 +12. **`EMCommonDefs.h` 中 `EM_DEPRECATED_IOS` 宏定义改为忽略参数**(`__attribute__((deprecated("")))`),属内部工程调整。 +13. **聊天室 `IEMChatroomManager.h` 仅删除创建/解散共 4 个方法**(123 行变更全为删除及 pragma 注释),其余聊天室 API(加入/退出/成员管理等)未动;注意迁移文档只提了 create/destroy。 +14. **`EMGroupManagerListener.h`(C++ listener 桥接头文件)中删除了对旧 delegate 回调的转发调用**(`joinGroupRequestDidDecline:reason:` 两参/三参、`userDidJoinGroup:user:`、`userDidLeaveGroup:user:`),与公开 delegate 删除一一对应,无额外公开 API。 +15. **`EMConversation` 的 `conversationName`/`conversationAvatar` 是实例方法而非属性**(`- (NSString* _Nullable)conversationName;`),RN 封装时注意调用形式。 +16. **4.24.2 侧分支hotfix(`EMUserInfoManager.mm` +2 行)不在 5.0.0 主线**:若 RN 当前基于 4.24.2 平版,需注意该 hotfix 内容在 5.0.0 中不存在(实现层,非公开 API)。 +17. **`EMChatMessage+Private.h` +2 行**:内部头文件,未纳入清单;如 RN iOS 原生层直接引用了 Private 头需另行核对(正常不应引用)。 + +## 4. 统计 + +| 类型 | 条数 | +| --- | --- | +| removed | 66 | +| new_api | 9 | +| new_type | 3 | +| new_enum | 4 | +| new_property | 1 | +| new_listener | 4 | +| renamed | 12 | +| param_changed | 6 | +| **合计** | **105** | + +备注:removed 条数按「逻辑 API 条目」统计(同一方法的同步+异步两个重载合并在一行时按 1 条计,如 `client_register_with_username` 与 `client_register_with_username_completion` 分行计 2 条;group 同步删除系列每个方法 1 条)。若按 OC 方法签名逐个计数,实际删除的方法签名约 90+ 个。 diff --git a/docs/porting/5.0.0/01-api-diff.md b/docs/porting/5.0.0/01-api-diff.md new file mode 100644 index 00000000..5161ff28 --- /dev/null +++ b/docs/porting/5.0.0/01-api-diff.md @@ -0,0 +1,389 @@ +# 5.0.0 平版 · 阶段一:公开 API Diff 双端对照总清单(iOS + Android) + +## 1. 头部信息 + +- **diff 锚点**: + - iOS(仓库 `/Users/asterisk/Codes/zuoyu_native/emclient-ios`):`4.24.1` = `ba235111d262f52d0d4aaeb8045a1b25b1ef1929` → `5.0.0` = `f8f506ccb009db38d47280ab713a65e468287f4b` + - Android(仓库 `/Users/asterisk/Codes/zuoyu_native/emclient-android`):`SDK_4.24.1` = `300865ac9cb1813c113199f0b19a65be79c4731a` → `SDK_5.0.0` = `8025ecb767e72a5c4623fab457ed269011e773d6` +- **中间 tag 核对结论**: + - iOS:`4.24.1` 与 `5.0.0` 之间仅有 `4.24.2`(`90bb2d911f6ca2b02355960b581f5703927fbf4a`),且 `4.24.2` 是侧分支 hotfix tag(不在 5.0.0 祖先链上),仅改动 `EMUserInfoManager.mm` 2 行实现代码,**无任何公开头文件变化**。结论:无跳版遗漏,`4.24.1 → 5.0.0` 直接 diff 覆盖全部公开 API 变化。 + - Android:`SDK_4.24.1` 与 `SDK_5.0.0` 之间**无任何中间 tag**,不存在跳版,直接两端 diff。 +- **diff 范围**: + - iOS:`newSDK/HyphenateSDK/` 下公开头文件(`EM*.h`、`IEM*.h`),排除 `*+Private.h` 等内部头文件与 `.mm` 实现;`pushExtension/`、`translate/` 两目录两 tag 间零改动。 + - Android:`hyphenatechatsdk/src/com/hyphenate/` 根包公开文件 + `chat/` 子包公开类;`chat/adapter/`、`chat/core/` 内部实现未 diff;`cloud/`、`notification/`、`push/`、`util/` 子包仅 AndroidX import 替换,无公开 API 变化。 +- **参考文档**: + - iOS 迁移指南:`/Users/asterisk/Codes/zuoyu/easemob-doc/docs/document/ios/migration_guide.md` + - Android 迁移指南:`/Users/asterisk/Codes/zuoyu/easemob-doc/docs/document/android/migration_guide.md` + - 单端输入清单:`01-api-diff-ios.md`(150 行条目)、`01-api-diff-android.md`(107 行条目,含 3 行「无变化核对」行) +- **合并口径**: + - 按语义配对,双端配对成功的条目共用同一 id;仅单端有的条目保留原输入文件 id。 + - 同名重载合并为一条,签名栏注明「含 N 个重载」;跨端配对时一端签名块天然含多个重载的,在签名栏内注明。 + - 签名逐字保留自两份输入文件,未改写。 + - 决策规则:双端都有 → `include`;仅单端有 → `defer`;`useAgoraChatDomain` 相关 → `skip`(**两份输入清单中均未出现该项,本清单 skip 条数为 0**,KI-107 规则备而不用);Android `EMClient#getDeviceInfo()` 按已知疑点 → `defer`。 + - 状态列全部为 `pending`(阶段二平版执行后回填)。 + - 注意:两份输入文件「统计」节的自称条数(105 / 82)与其表格实际行数(150 / 107)不一致,本文件统计一律按合并后表格实际条目计算,详见未匹配清单 N5。 + +## 2. 总变更清单 + +类型取值:new_api / removed / renamed / param_changed / new_type / new_property / new_enum / new_listener。renamed 条目的签名栏写「旧 → 新」。 + +### client + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| client_register_account | removed | client | `- (EMError *_Nullable)registerWithUsername:(NSString * _Nonnull)aUsername password:(NSString * _Nonnull)aPassword;` 与 `- (void)registerWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSString * _Nonnull aUsername, EMError * _Nullable aError))aCompletionBlock;`(含 2 个重载) | `public void createAccount(String username, String password) throws HyphenateException` | 初始化与登录-密码登录下线 / 密码登录下线 | include | pending | +| client_fetch_token_with_password | removed | client | `- (void)fetchTokenWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSString * _Nullable aToken, EMError * _Nullable aError))aCompletionBlock;` | `public void getUserTokenFromServer(final String username, final String password, final EMValueCallBack callBack)` | 同上 | include | pending | +| client_login_with_password | removed | client | `- (EMError *_Nullable)loginWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword EM_DEPRECATED_IOS(3_0_0, 4_5_0,"Use -EMClient loginWithUsername:token:completion instead");` 与 `- (void)loginWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSString * _Nonnull aUsername, EMError *_Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(3_0_0, 4_5_0,"Use -EMClient loginWithUsername:token:completion instead");`(含 2 个重载) | `public void login(String id, String password, @NonNull final EMCallBack callback)` | 同上 | include | pending | +| client_login_with_agora_token | removed | client | `- (EMError *_Nullable)loginWithUsername:(NSString *_Nonnull)aUsername agoraToken:(NSString *_Nonnull)aAgoraToken EM_DEPRECATED_IOS(3_8_9, 4_2_0,"Use -EMClient loginWithUsername:token instead");` 与 `- (void)loginWithUsername:(NSString *_Nonnull)aUsername agoraToken:(NSString *_Nonnull)aAgoraToken completion:(void (^_Nullable)(NSString * _Nonnull aUsername, EMError *_Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(3_8_9, 4_2_0,"Use -EMClient loginWithUsername:token:completion instead");`(含 2 个重载) | `public void loginWithAgoraToken(String username, String agoraToken, @NonNull final EMCallBack callback)` | 同上 | include | pending | +| client_service_check | removed | client | `- (void)serviceCheckWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(EMServerCheckType aType, EMError *_Nullable aError))aCompletionBlock __deprecated_msg("This method is deprecated");` | `public void check(String username, String password, final CheckResultListener listener)` + `public interface CheckResultListener { void onResult(@EMCheckType.CheckType int type, int result, String desc); }` | 同上 | include | pending | +| client_check_type | removed | client | `typedef NS_ENUM(NSInteger, EMServerCheckType) { EMServerCheckAccountValidation = 0, EMServerCheckGetDNSListFromServer, EMServerCheckGetTokenFromServer, EMServerCheckDoLogin, EMServerCheckDoLogout, };` | `public class EMCheckType`(常量 `ACCOUNT_VALIDATION=0`、`GET_DNS_LIST_FROM_SERVER=1`、`GET_TOKEN_FROM_SERVER=2`、`DO_LOGIN=3`、`DO_MSG_SEND=4`、`DO_LOGOUT=5` + `@interface CheckType`)随 `EMClient#check` 删除 | 同上 | include | pending | +| client_auto_login_state_query | removed | client | `@property(nonatomic, readonly) BOOL isAutoLogin;`(EMClient) | `public boolean isLoggedInBefore()` | 初始化与登录-自动登录移除 / 自动登录移除 | include | pending | +| client_auto_login_behavior | removed | client | 无(行为变化,无签名级条目;效果同 Android) | `EMClient#init` 内基于 `getAutoLogin()`+`isLoggedInBefore()` 的自动登录逻辑 | 同上 | include | pending | +| client_statistics_manager | removed | client | `@property (nonatomic, strong, readonly) id _Nullable statisticsManager;` 及 `#import "IEMStatisticsManager.h"` | `public EMStatisticsManager statisticsManager()` | 其他删除的 API-无客户端替代 | include | pending | +| statistics_module | removed | client | `@protocol IEMStatisticsManager`(`getMessageStatisticsById:`、`getMessageCountWithStart:end:direction:type:`、`getMessageStatisticsSizeWithStart:end:direction:type:`)、`@interface EMStatisticsManager : EMManager`、`@interface EMChatMessageStatistics : NSObject`(10 个只读属性)、`EMMessageStatisticsDirection`、`EMMessageStatisticsType` 两个枚举;`Statistics/` 目录 3 个公开头文件全部删除 | `public class EMStatisticsManager extends EMBase`(含 `getMessageStatistics(String)`、`getMessageCount(long, long, EMSearchMessageDirect, EMSearchMessageType)`、`getMessageSize(long, long, EMSearchMessageDirect, EMSearchMessageType)` 及内部枚举 `EMSearchMessageDirect`、`EMSearchMessageType`);`public class EMMessageStatistics extends EMBase`(含 `getMsgId()`、`getTo()`、`getFrom()`、`getType()`、`getChatType()`、`getMsgTime()`、`direct()`、`getMsgSize()`、`getAttachmentSize()`、`getThumbnailSize()`) | 同上 | include | pending | +| client_get_logged_in_devices_with_password | removed | client | `- (NSArray *_Nullable)getLoggedInDevicesFromServerWithUsername:(NSString * _Nonnull)aUsername password:(NSString * _Nonnull)aPassword error:(EMError ** _Nullable)pError;` 与 `- (void)getLoggedInDevicesFromServerWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(NSArray * _Nullable aList, EMError *_Nullable aError))aCompletionBlock;`(含 2 个重载) | `public List getLoggedInDevicesFromServer(String username, String password) throws HyphenateException` | 设备管理与鉴权 | include | pending | +| client_kick_device_with_password | removed | client | `- (EMError *_Nullable)kickDeviceWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword resource:(NSString *_Nonnull)aResource;` 与 `- (void)kickDeviceWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword resource:(NSString *_Nonnull)aResource completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;`(含 2 个重载) | `public void kickDevice(String username, String password, String resource) throws HyphenateException` | 同上 | include | pending | +| client_kick_all_devices_with_password | removed | client | `- (EMError *_Nullable)kickAllDevicesWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword;` 与 `- (void)kickAllDevicesWithUsername:(NSString *_Nonnull)aUsername password:(NSString *_Nonnull)aPassword completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;`(含 2 个重载) | `public void kickAllDevices(String username, String password) throws HyphenateException` | 同上 | include | pending | +| client_delegate_sync_data_start | new_listener | client | `- (void)syncDataStartWithType:(EMDataSyncType)type;`(EMClientDelegate) | `default void onDataSyncStart(EMOptions.EMDataSyncType type){}`(EMConnectionListener) | 数据同步与服务端拉取 API 迁移 / 数据同步 API | include | pending | +| client_delegate_sync_data_finished | new_listener | client | `- (void)syncDataFinished:(EMError * _Nullable)error type:(EMDataSyncType)type;`(EMClientDelegate) | `default void onDataSyncFinish(EMOptions.EMDataSyncType type, int errorCode){}`(EMConnectionListener) | 同上 | include | pending | +| client_delegate_on_database_opened | new_listener | client | `- (void)onDatabaseOpened:(EMError * _Nullable)error username:(NSString *_Nonnull)username;`(EMClientDelegate) | `default void onDatabaseOpened(String username) {}`(EMConnectionListener) | 初始化与登录-登录与数据库打开解耦 | include | pending | +| client_login_with_username_token_sync | removed | client | `- (EMError *_Nullable)loginWithUsername:(NSString *_Nonnull)aUsername token:(NSString *_Nonnull)aToken;` | 无(Android 本无同步 token 登录) | 初始化与登录-密码登录下线 | defer | pending | +| client_get_device_config | param_changed | client | `- (EMDeviceConfig * _Nullable)getDeviceConfig:(EMError **)pError;`(原 `- (EMDeviceConfig *)getDeviceConfig:(EMError **)pError;`) | 无 | 无(仅 nullability 标注) | defer | pending | +| client_delegate_auto_login_did_complete | removed | client | `- (void)autoLoginDidCompleteWithError:(EMError * _Nullable)aError;`(EMClientDelegate) | 无(Android 本无该回调) | 监听器回调变化汇总 | defer | pending | +| client_delegate_user_account_did_login_from_other_device | removed | client | `- (void)userAccountDidLoginFromOtherDevice:(NSString* _Nullable)aDeviceName EM_DEPRECATED_IOS(4_1_0, 4_7_0, "Use userAccountDidLoginFromOtherDeviceWithInfo: instead");` 与 `- (void)userAccountDidLoginFromOtherDevice EM_DEPRECATED_IOS(3_1_0, 4_1_0, "Use userAccountDidLoginFromOtherDevice: instead");`(含 2 个重载) | 无(Android 侧旧回调为 `onLogout` 系列,见下) | 同上 | defer | pending | +| client_renew_token_no_callback_removed | removed | client | 无(iOS 同步 `renewToken:` 在 5.0.0 保留,diff 未触及) | `public void renewToken(String newAgoraToken)` | 密码登录下线 | defer | pending | +| client_get_logged_in_devices_token_sync_removed | removed | client | 无(iOS 仅有异步 token 版,4.24.1 已存在且未动) | `public List getLoggedInDevicesFromServerWithToken(@NonNull String username, @NonNull String token) throws HyphenateException` | 设备管理与鉴权 | defer | pending | +| client_fetch_logged_in_devices_with_token_new | new_api | client | 无 | `public void fetchLoggedInDevicesFromServerWithToken(@NonNull String username, @NonNull String token, EMValueCallBack> callBack)` | 设备管理与鉴权 | defer | pending | +| client_is_database_opened_new | new_api | client | 无 | `public boolean isDatabaseOpened()` | 登录与数据库打开解耦 | defer | pending | +| client_get_device_info_new | new_api | client | 无 | `public JSONObject getDeviceInfo()` | (无)疑似内部辅助方法暴露为 public | defer | pending | +| client_version_bump | new_property | client | 无 | `public final static String VERSION = "5.0.0"`(原值 `"4.24.1"`) | — | defer | pending | + +### options + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| options_data_sync_type_enum | new_enum | options | `typedef NS_OPTIONS(NSInteger, EMDataSyncType) { EMDataSyncTypeNone = 0, EMDataSyncTypeConversations = 1 << 0, EMDataSyncTypeContacts = 1 << 1, EMDataSyncTypeJoinedGroups = 1 << 2, };` | `public enum EMDataSyncType { NONE(0), CONVERSATIONS(1 << 0), CONTACTS(1 << 1), JOINED_GROUPS(1 << 2); public int getValue(); public static int toNativeMask(EnumSet types); public static EnumSet fromNativeMask(int mask); }` | 数据同步与服务端拉取 API 迁移 / 数据同步 API | include | pending | +| options_data_sync_type | new_property | options | `@property (nonatomic, assign) EMDataSyncType dataSyncType;` | `public void setDataSyncType(EnumSet types)` / `public EnumSet getDataSyncType()` | 同上 | include | pending | +| options_is_auto_login | removed | options | `@property(nonatomic, assign) BOOL isAutoLogin;` | `public void setAutoLogin(boolean autoLogin)` / `public boolean getAutoLogin()` | 初始化与登录-自动登录移除 / 自动登录移除 | include | pending | +| options_enable_require_read_ack | removed | options | `@property(nonatomic, assign) BOOL enableRequireReadAck;` | `public void setRequireAck(boolean requireAck)` / `public boolean getRequireAck()` | 已读回执体系重构 | include | pending | +| options_enable_auto_sync_contacts | removed | options | `@property (nonatomic) BOOL enableAutoSyncContacts;` | `public void setEnableAutoSyncContacts(boolean enable)` / `public boolean isEnableAutoSyncContacts()` | 数据同步与服务端拉取 API 迁移 / 服务端拉取 API 迁移 | include | pending | +| options_report_server_removed | removed | options | 无 | `public String getReportServer()` / `public void setReportServer(String reportServer)` | 其他删除的 API | defer | pending | +| options_area_code_int_removed | removed | options | 无 | `public static class AreaCode`(int 常量容器)+ `public static final int AREA_CODE_CN = 1;` 等 7 个常量 + `public void setAreaCode(int code)` | 有替代方式的 API | defer | pending | +| options_area_code_enum_new | new_enum | options | 无 | `public enum AreaCode { CN(1), NA(2), EU(4), AS(8), JP(16), IN(32), GLOB(-1); public int getValue(); }` | 有替代方式的 API | defer | pending | +| options_set_area_code_param_changed | param_changed | options | 无 | 旧:`public void setAreaCode(int code)` → 新:`public void setAreaCode(AreaCode code)` | 有替代方式的 API | defer | pending | + +### multidevice + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| multidevice_event_conversation_unread_message_count_cleared | new_enum | client | `EMMultiDevicesEventConversationUnreadMessageCountCleared = 65,` | `int CONVERSATION_UNREAD_MESSAGECOUNT_CLEARED = 65;` | 已读回执体系重构-多设备事件 / 多设备事件 | include | pending | +| multidevice_event_all_conversation_unread_message_count_cleared | new_enum | client | `EMMultiDevicesEventAllConversationUnreadMessageCountCleared = 66,` | `int ALL_CONVERSATION_UNREAD_MESSAGECOUNT_CLEARED = 66;` | 同上 | include | pending | + +### chat(含已读回执体系) + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| chat_fetch_conversations_from_server | removed | conversation | `- (void)getConversationsFromServer:(void (^_Nullable)(NSArray * _Nullable aConversations, EMError * _Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(4_0_0, ...);`、`- (void)getConversationsFromServerByPage:(NSUInteger)pageNumber pageSize:(NSUInteger)pageSize completion:(void (^_Nullable)(NSArray * _Nullable aConversations, EMError * _Nullable aError))aCompletionBlock EM_DEPRECATED_IOS(4_0_0, ...);`、`- (void)getConversationsFromServerWithCursor:(nullable NSString *)cursor pageSize:(UInt8)pageSize completion:(nonnull void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock;`(3 个方法) | `public Map fetchConversationsFromServer() throws HyphenateException` + 3 个 async 重载:`asyncFetchConversationsFromServer(EMValueCallBack>)`、`asyncFetchConversationsFromServer(int pageNum, int pageSize, EMValueCallBack>)`、`asyncFetchConversationsFromServer(int limit, String cursor, EMValueCallBack>)` | 服务端拉取 API 迁移 | include | pending | +| chat_get_pinned_conversations_from_server | removed | conversation | `- (void)getPinnedConversationsFromServerWithCursor:(nullable NSString *)cursor pageSize:(UInt8)limit completion:(nonnull void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock;` | `public void asyncFetchPinnedConversationsFromServer(final int limit, final String cursor, final EMValueCallBack> callback)` | 同上 | include | pending | +| chat_get_conversations_with_cursor_filter | removed | conversation | `- (void)getConversationsFromServerWithCursor:(NSString * _Nullable)cursor filter:(EMConversationFilter* _Nonnull)filter completion:(nonnull void (^)(EMCursorResult * _Nullable result, EMError * _Nullable error))completionBlock;` | `public void asyncGetConversationsFromServerWithCursor(@NonNull String cursor, @NonNull EMConversationFilter filter, EMValueCallBack> callback)` | 同上 | include | pending | +| chat_modify_message | param_changed | chat | `- (void)modifyMessage:(NSString *_Nonnull)messageId body:(EMMessageBody *_Nonnull)body completion:(void (^_Nonnull)(EMError * _Nullable error,EMChatMessage *_Nullable message))completionBlock;`(旧三参数版删除;iOS 四参数版 `modifyMessage:body:ext:completion:` 4.24.1 已存在) | 旧:`public void asyncModifyMessage(String messageId, EMMessageBody messageBodyModified, final EMValueCallBack callBack)` → 新:`public void asyncModifyMessage(String messageId, @Nullable EMMessageBody messageBodyModified, @Nullable Map ext, final EMValueCallBack callBack)` | 其他删除的 API-有替代方式 / 有替代方式的 API | include | pending | +| chat_send_message_read_ack | removed | message | `- (void)sendMessageReadAck:(NSString * _Nonnull)aMessageId toUser:(NSString * _Nonnull)aUsername completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | `public void ackMessageRead(String to, String messageId) throws HyphenateException` | 已读回执体系重构 / 发送消息已读回执与清除未读数 | include | pending | +| chat_send_group_message_read_ack | removed | message | `- (void)sendGroupMessageReadAck:(NSString * _Nonnull)aMessageId toGroup:(NSString * _Nonnull)aGroupId content:(NSString * _Nullable)aContent completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | `public void ackGroupMessageRead(String to, String messageId, String ext) throws HyphenateException` | 同上 | include | pending | +| chat_send_message_read_receipts | new_api | message | `- (void)sendMessageReadReceipts:(NSArray * _Nonnull)aMessages completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | `public void asyncSendMessageReadReceipts(final List messages, final EMCallBack callBack)` | 同上 | include | pending | +| chat_ack_conversation_read | removed | conversation | `- (void)ackConversationRead:(NSString * _Nonnull)conversationId completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | `public void ackConversationRead(String conversationId) throws HyphenateException` | 同上 | include | pending | +| chat_clear_conversation_unread_message_count | new_api | conversation | `- (void)clearConversationUnreadMessageCount:(NSString * _Nonnull)aConversationId completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | `public void asyncClearConversationUnreadMessageCount(final String conversationId, final EMCallBack callBack)` | 同上 | include | pending | +| chat_clear_all_conversation_unread_message_count | new_api | conversation | `- (void)clearAllConversationUnreadMessageCount:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | `public void asyncClearAllConversationUnreadMessageCount(final EMCallBack callBack)` | 同上 | include | pending | +| chat_get_group_message_read_receipts | new_api | message | `- (void)getGroupMessageReadReceipts:(NSArray * _Nonnull)aMessages completion:(void (^_Nullable)(NSArray *_Nullable aReceipts, EMError *_Nullable aError))aCompletionBlock;` | `public void asyncGetGroupMessageReadReceipts(final List messages, final EMValueCallBack> callBack)` | 已读回执体系重构-回执详情查询 | include | pending | +| chat_fetch_history_messages | removed | message | `- (EMCursorResult *_Nullable)fetchHistoryMessagesFromServer:(NSString *_Nonnull)aConversationId conversationType:(EMConversationType)aConversationType startMessageId:(NSString *_Nullable)aStartMessageId fetchDirection:(EMMessageFetchHistoryDirection)direction pageSize:(int)aPageSize error:(EMError **_Nullable)pError __deprecated_msg(...)` 及无 `fetchDirection` 的同步重载(共 2 个同步方法);`- (void)asyncFetchHistoryMessagesFromServer:(NSString *_Nonnull)aConversationId conversationType:(EMConversationType)aConversationType startMessageId:(NSString *_Nullable)aStartMessageId pageSize:(int)aPageSize completion:...` 及带 `fetchDirection:` 的重载(共 2 个异步方法,均 `__deprecated_msg`) | `public EMCursorResult fetchHistoryMessages(String conversationId, EMConversationType type, int pageSize, String startMsgId)` 及带 `EMSearchDirection` 重载(同步);`public void asyncFetchHistoryMessage(final String conversationId, final EMConversationType type, final int pageSize, final String startMsgId, ...)` 两个异步重载 | 其他删除的 API-有替代方式 / 有替代方式的 API | include | pending | +| chat_fetch_group_message_read_receipts | renamed | message | 旧:`- (void)asyncFetchGroupMessageAcksFromServer:(NSString *_Nonnull)aMessageId groupId:(NSString *_Nonnull)aGroupId startGroupAckId:(NSString *_Nonnull)aGroupAckId pageSize:(int)aPageSize completion:(void (^_Nullable)(EMCursorResult *_Nullable aResult, EMError *_Nullable error, int totalCount))aCompletionBlock;` → 新:`- (void)asyncFetchGroupMessageReadUsersFromServer:(NSString *_Nonnull)aMessageId groupId:(NSString *_Nonnull)aGroupId readReceiptId:(NSString *_Nonnull)aReadReceiptId pageSize:(int)aPageSize completion:(void (^_Nullable)(EMCursorResult *_Nullable aResult, EMError *_Nullable error, int totalCount))aCompletionBlock;` | 旧:`public EMCursorResult fetchGroupReadAcks(String msgId, int pageSize, String startAckId) throws HyphenateException` / `public void asyncFetchGroupReadAcks(final String msgId, final int pageSize, final String startAckId, final EMValueCallBack> callBack)` → 新:`public void asyncFetchGroupMessageReadReceipts(final String msgId, final int pageSize, final String startAckId, final EMValueCallBack> callBack)` | 已读回执体系重构-回执详情查询 | include | pending | +| chat_report_message | removed | message | `- (void)reportMessageWithId:(NSString *_Nonnull )aMessageId tag:(NSString *_Nonnull)aTag reason:(NSString *_Nonnull)aReason completion:(void(^_Nullable)(EMError* _Nullable error))aCompletion;` | `public void asyncReportMessage(String msgId, String tag, String reportReason, EMCallBack callback)` / `public void reportMessage(String msgId, String tag, String reportReason) throws HyphenateException` | 其他删除的 API-无客户端替代 | include | pending | +| chat_mark_all_conversations_as_read | removed | conversation | `- (EMError*)markAllConversationsAsRead;` | `public boolean markAllConversationsAsRead()` | 已读回执体系重构 | include | pending | +| chat_delegate_messages_did_read | removed | message | `- (void)messagesDidRead:(NSArray * _Nonnull)aMessages;`(EMChatManagerDelegate) | `default void onMessageRead(List messages) {}`(EMMessageListener) | 监听器回调变化汇总 / 接收消息已读回执 | include | pending | +| chat_delegate_group_message_did_read | removed | message | `- (void)groupMessageDidRead:(NSArray * _Nonnull)aGroupAcks;` 与 `- (void)groupMessageDidRead:(EMChatMessage * _Nonnull)aMessage groupAcks:(NSArray * _Nonnull)aGroupAcks __deprecated_msg("Use -groupMessageDidRead: instead");`(含 2 个重载) | `default void onGroupMessageRead(List groupReadAcks) {}`(EMMessageListener) | 同上 | include | pending | +| chat_delegate_group_message_ack_has_changed | removed | message | `- (void)groupMessageAckHasChanged;`(EMChatManagerDelegate) | `default void onReadAckForGroupMessageUpdated() {}`(EMMessageListener) | 同上(Android 文档称改名为 `onReadReceiptForGroupMessageUpdated`,5.0.0 源码不存在,见交叉验证已知矛盾 1) | include | pending | +| chat_delegate_on_conversation_read | removed | conversation | `- (void)onConversationRead:(NSString * _Nonnull)from to:(NSString * _Nonnull)to;`(EMChatManagerDelegate) | `void onConversationRead(String from, String to)`(EMConversationListener) | 同上 | include | pending | +| chat_delegate_on_message_read_receipts | new_listener | message | `- (void)onMessageReadReceipts:(NSArray * _Nonnull)aReceipts;`(EMChatManagerDelegate) | `default void onMessageReadReceipts(List receipts) {}`(EMMessageListener) | 同上 | include | pending | +| chat_get_unread_message_count | new_api | chat | `- (NSInteger)getUnreadMessageCount;` | 无(Android `getUnreadMessageCount` 4.24.1 已存在,5.0.0 仅统计范围行为变化,双端一致) | 主要新增 API / 行为变化 1 | defer | pending | +| chat_import_conversations | removed | conversation | `- (void)importConversations:(NSArray * _Nullable)aConversations completion:(void (^ _Nullable)(EMError * _Nullable aError))aCompletionBlock;` | 无(Android diff 未提及) | 其他删除的 API-无客户端替代 | defer | pending | +| chat_resend_message | removed | message | `- (void)resendMessage:(EMChatMessage *_Nonnull)aMessage progress:(void (^_Nullable)(int progress))aProgressBlock completion:(void (^_Nullable)(EMChatMessage *_Nullable message, EMError *_Nullable error))aCompletionBlock;` | 无 | 其他删除的 API-有替代方式 | defer | pending | +| chat_load_messages_with_type | removed | message | `- (NSArray * _Nullable)loadMessagesWithType:(EMMessageBodyType)aType timestamp:(long long)aTimestamp count:(int)aCount fromUser:(NSString* _Nullable)aUsername searchDirection:(EMMessageSearchDirection)aDirection;` 及 completion 异步重载(IEMChatManager 上共 2 个) | 无 | 同上 | defer | pending | +| chat_load_messages_with_keyword_legacy | removed | message | `- (NSArray *)loadMessagesWithKeyword:(NSString*)aKeywords timestamp:(long long)aTimestamp count:(int)aCount fromUser:(NSString*)aSender searchDirection:(EMMessageSearchDirection)aDirection;` 及对应 completion 重载(无 `scope` 参数的 2 个旧重载) | 无 | 同上 | defer | pending | +| chat_load_messages_with_keyword_scope | param_changed | message | `- (void)loadMessagesWithKeyword:(NSString* _Nonnull)aKeywords timestamp:(long long)aTimestamp count:(int)aCount fromUser:(NSString *_Nullable)aSender searchDirection:(EMMessageSearchDirection)aDirection scope:(EMMessageSearchScope)aScope completion:(void (^_Nullable)(NSArray *_Nullable aMessages, EMError *_Nullable aError))aCompletionBlock;`(仅 nullability 标注收紧,选择器不变) | 无 | 无 | defer | pending | +| chat_delete_messages_before | param_changed | chat | `- (void)deleteMessagesBefore:(NSUInteger)aTimestamp completion:(void(^_Nullable)(EMError*_Nullable error))aCompletion;`(原 completion 无 nullability 标注) | 无 | 无 | defer | pending | +| chat_delegate_conversation_list_did_update | removed | conversation | `- (void)conversationListDidUpdate:(NSArray * _Nonnull)aConversationList;`(自 EMChatManagerDelegate 删除) | 无(Android 会话更新回调本就在 EMConversationListener) | 监听器回调变化汇总 | defer | pending | +| chat_add_conversation_delegate | new_api | conversation | `- (void)addConversationDelegate:(id _Nullable)aDelegate delegateQueue:(dispatch_queue_t _Nullable)aQueue NS_SWIFT_NAME(addConversation(delegate:queue:));` 与 `- (void)removeConversationDelegate:(id _Nonnull)aDelegate NS_SWIFT_NAME(removeConversation(delegate:));` | 无 | 主要新增 API | defer | pending | +| conversation_delegate_protocol | new_type | conversation | `@protocol EMConversationDelegate @optional - (void)conversationListDidUpdate:(NSArray * _Nonnull)aConversationList; @end`(新文件 `EMConversationDelegate.h`) | 无 | 监听器回调变化汇总 | defer | pending | +| chat_load_all_conversations_removed | removed | conversation | 无 | `public boolean loadAllConversations()` | 服务端拉取 API 迁移 | defer | pending | +| chat_update_participant_removed | removed | chat | 无 | `public boolean updateParticipant(String from, String changeTo)` | 其他删除的 API | defer | pending | +| chat_async_fetch_history_messages_new | new_api | message | 无(iOS 对应 `fetchMessagesFromServerBy:conversationType:cursor:pageSize:option:completion:` 4.24.1 已存在) | `public void asyncFetchHistoryMessages(String conversationId, EMConversationType type, int pageSize, String cursor, EMFetchMessageOption option, final EMValueCallBack> callBack)` | 有替代方式的 API | defer | pending | +| chat_async_delete_conversations_new | new_api | conversation | 无 | `public void asyncDeleteConversations(List conversationIds, boolean deleteMessages, EMCallBack callBack)` | 主要新增 API | defer | pending | +| msg_listener_on_message_recalled_removed | removed | message | 无(iOS diff 未触及撤回回调) | `default void onMessageRecalled(List messages){}`(EMMessageListener) | 监听器回调变化汇总 | defer | pending | + +### message / conversation 模型 + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| message_is_peer_read | renamed | message | 旧:`@property (nonatomic) BOOL isReadAcked;` → 新:`@property (nonatomic,readonly) BOOL isPeerRead;` | 旧:`public boolean isAcked()` / `public void setAcked(boolean isAcked)` → 新:`public boolean isPeerRead()`(+ 包私有 `void setPeerRead(boolean)`) | 已读回执体系重构-EMMessage 已读相关方法重命名 | include | pending | +| message_is_read | renamed | message | 旧:`@property (nonatomic) BOOL isRead;` → 新:`@property (nonatomic,readonly) BOOL isRead;`(同名,setter 删除) | 旧:`public boolean isUnread()` / `public void setUnread(boolean unread)` → 新:`public boolean isRead()`(+ 包私有 `void setRead(boolean)`)(语义反转,取值取反) | 同上 / 行为变化 4 | include | pending | +| message_is_need_read_receipt | renamed | message | 旧:`@property (nonatomic) BOOL isNeedGroupAck;` → 新:`@property (nonatomic) BOOL isNeedReadReceipt;` | 旧:`public boolean isNeedGroupAck()` / `public void setIsNeedGroupAck(boolean need)` → 新:`public boolean isNeedReadReceipt()` / `public void setIsNeedReadReceipt(boolean need)` | 同上 | include | pending | +| message_group_read_receipt_count | renamed | message | 旧:`@property (nonatomic, readonly) int groupAckCount;` → 新:`@property (nonatomic, readonly) int groupReadReceiptCount;` | 旧:`public int groupAckCount()` / `public void setGroupAckCount(int count)` → 新:`public int readReceiptCount()`(+ 包私有 `void setReadReceiptCount(int)`) | 同上 | include | pending | +| conversation_name_avatar | new_api | conversation | `- (NSString* _Nullable)conversationName;` 与 `- (NSString* _Nullable)conversationAvatar;`(EMConversation 实例方法而非属性,2 个方法) | `public String getConversationName()` / `public String getConversationAvatar()`(EMConversation) | 主要新增 API | include | pending | +| conversation_mark_message_as_read | removed | conversation | `- (void)markMessageAsReadWithId:(NSString *_Nonnull)aMessageId error:(EMError ** _Nullable)pError;` 与 `- (void)markAllMessagesAsRead:(EMError ** _Nullable)pError;`(EMConversation,2 个方法) | `public void markMessageAsRead(String messageId)` / `public void markAllMessagesAsRead()`(EMConversation) | 已读回执体系重构 | include | pending | +| group_read_receipt_type | renamed | message | 旧:`@interface EMGroupMessageAck : NSObject`,属性 `messageId`/`readAckId`/`from`(NSString*)/`content`/`readCount`/`timestamp` → 新:`@interface EMGroupReadReceipt : NSObject`,属性 `messageId`/`readReceiptId`/`from`(**EMGroupMemberInfo***)/`readCount`/`timestamp`(`content` 删除;文件改名 `EMGroupMessageAck.h`→`EMGroupReadReceipt.h`) | 旧:`public class EMGroupReadAck { getAckId(); getMsgId(); String getFrom(); String getContent(); getCount(); getTimestamp(); }` → 新:`public class EMGroupReadReceipt extends EMBase { public String getAckId(); public String getMsgId(); public EMGroupMemberInfo getFrom(); public int getCount(); public long getTimestamp(); }` | 已读回执体系重构-回执详情查询 | include | pending | +| message_read_receipt_type | new_type | message | `@interface EMMessageReadReceipt : NSObject`,只读属性 `messageId`/`conversationId`/`isPeerReceipt`(BOOL)/`readCount`(NSInteger)(新文件 `EMMessageReadReceipt.h`) | `public class EMMessageReadReceipt extends EMBase { public String getMessageId(); public String getConversationId(); public boolean isPeerReceipt(); public int getReadCount(); }` | 同上 / 接收消息已读回执 | include | pending | +| message_get_reaction | removed | message | `- (EMMessageReaction *_Nullable)getReaction:(NSString * _Nonnull)reaction;` | 无 | 其他删除的 API-有替代方式 | defer | pending | +| file_message_body_init_with_data | removed | message | `- (instancetype _Nonnull)initWithData:(NSData *_Nullable)aData displayName:(NSString *_Nullable)aDisplayName;`(EMFileMessageBody) | 无 | 同上 / 行为变化 5 | defer | pending | +| file_message_body_init_with_local_path | param_changed | message | `- (instancetype _Nonnull)initWithLocalPath:(NSString * _Nonnull)aLocalPath displayName:(NSString * _Nonnull)aDisplayName;`(原两参数均 `_Nullable`) | 无 | 无(nullability 收紧) | defer | pending | +| image_message_body_init_with_data | removed | message | `- (instancetype)initWithData:(NSData *)aData thumbnailData:(NSData *)aThumbnailData __deprecated_msg("Use -initWithLocalPath:displayName: instead");`(EMImageMessageBody) | 无 | 其他删除的 API-有替代方式 | defer | pending | +| stream_chunk_sequence_number | renamed | message | 旧:`@property (nonatomic) long sequenceNumber;` → 新:`@property (nonatomic, readonly) BOOL isComplete;`(EMStreamChunk) | 无 | 同上 | defer | pending | +| conversation_get_message_param_changed | param_changed | conversation | 无 | 旧:`public EMMessage getMessage(String messageId, boolean markAsRead)` → 新:`public EMMessage getMessage(String messageId)`(EMConversation) | 行为变化 4 | defer | pending | +| conversation_search_msg_scope_sync_removed | removed | conversation | 无 | `public List searchMsgFromDB(String keywords, long timeStamp, int maxCount, String from, EMSearchDirection direction, EMMessageSearchScope searchScope)`(EMConversation,同步) | 有替代方式的 API | defer | pending | +| message_create_txt_send_message_removed | removed | message | 无 | `public static EMMessage createTxtSendMessage(String content, String username)` | 有替代方式的 API | defer | pending | +| message_get_user_name_removed | removed | message | 无 | `public String getUserName()` | 有替代方式的 API | defer | pending | +| message_get_recaller_removed | removed | message | 无 | `public String getRecaller()` | 有替代方式的 API | defer | pending | + +### group + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| group_options_type | removed | group | `@interface EMGroupOptions : NSObject`(属性 `style`/`maxUsers`/`IsInviteNeedConfirm`/`ext`)(`EMGroupOptions.h` 整文件删除) | `public class EMGroupOptions { public int maxUsers = 200; public EMGroupManager.EMGroupStyle style; public boolean inviteNeedConfirm = false; public String extField; }` | 群组配置模型重构 | include | pending | +| group_style_enum | removed | group | `typedef NS_ENUM(NSInteger, EMGroupStyle) { EMGroupStylePrivateOnlyOwnerInvite = 0, EMGroupStylePrivateMemberCanInvite, EMGroupStylePublicJoinNeedApproval, EMGroupStylePublicOpenJoin, };` | `public enum EMGroupStyle { EMGroupStylePrivateOnlyOwnerInvite, EMGroupStylePrivateMemberCanInvite, EMGroupStylePublicJoinNeedApproval, EMGroupStylePublicOpenJoin }`(EMGroupManager 内部枚举) | 同上 | include | pending | +| group_configs_type | new_type | group | `@interface EMGroupConfigs : NSObject`,属性 `maxUsers`/`IsInviteNeedConfirm`/`ext`/`allowInvites`/`joinApprovalRequired`/`isPublic`(新文件 `EMGroupConfigs.h`) | `public class EMGroupConfigs { public int maxUsers = 200; public boolean isPublic = false; public boolean joinApprovalRequired = false; public boolean allowInvites = false; public boolean inviteNeedConfirm = false; public String extField; }` | 同上 | include | pending | +| group_configs_type_enum | new_enum | group | `typedef NS_OPTIONS(NSUInteger, EMGroupConfigsType) { EMGroupConfigsTypeAllowInvites = 1 << 0, EMGroupConfigsTypeMaxUsers = 1 << 1, EMGroupConfigsTypeInviteNeedConfirm = 1 << 2, EMGroupConfigsTypeJoinApprovalRequired = 1 << 3, EMGroupConfigsTypeIsPublic = 1 << 4, EMGroupConfigsTypeExt = 1 << 5, };` | `public enum EMGroupConfigsType { IS_PUBLIC(1 << 0), JOIN_APPROVAL_REQUIRED(1 << 1), ALLOW_INVITES(1 << 2), MAX_USERS(1 << 3), INVITE_NEED_CONFIRM(1 << 4), EXT(1 << 5); }`(EMGroupManager 内部枚举,含 `getValue()` / `static int toNativeMask(EnumSet)`) | 同上 / 相关 API 变化 | include | pending | +| group_create_group | param_changed | group | 删除:`- (EMGroup * _Nullable)createGroupWithSubject:(NSString *_Nullable)aSubject description:(NSString *_Nullable)aDescription invitees:(NSArray * _Nullable)aInvitees message:(NSString *_Nullable)aMessage setting:(EMGroupOptions *_Nullable)aSetting error:(EMError **_Nullable)pError;`(同步)与 `- (void)createGroupWithSubject:(NSString *_Nullable)aSubject description:(NSString *_Nullable)aDescription invitees:(NSArray * _Nullable)aInvitees message:(NSString *_Nullable)aMessage setting:(EMGroupOptions *_Nullable)aSetting completion:(void (^_Nullable)(EMGroup *_Nullable aGroup, EMError *_Nullable aError))aCompletionBlock;`(无 avatar 异步版) | 旧:`createGroup(String groupName, String avatar, String desc, String[] allMembers, String reason, EMGroupOptions option)`(及无 avatar 重载 + 两个 `asyncCreateGroup(...)`)→ 新:`public EMGroup createGroup(String groupName, String avatar, String desc, String[] allMembers, String reason, EMGroupConfigs configs) throws HyphenateException` / `public void asyncCreateGroup(final String groupName, final String avatar, final String desc, final String[] allMembers, final String reason, final EMGroupConfigs configs, final EMValueCallBack callback)` | 群组配置模型重构-相关 API 变化 | include | pending | +| group_create_group_with_avatar | param_changed | group | `- (void)createGroupWithSubject:... avatar:... setting:(EMGroupConfigs *)setting completion:...`(选择器不变,`setting` 类型 EMGroupOptions* → EMGroupConfigs*) | 同上一条 Android 侧(同一变更的两端表述;5.0.0 双端均仅保留带 avatar 形式) | 同上 | include | pending | +| group_update_group_configs | new_api | group | `- (void)updateGroupWithId:(NSString *_Nonnull)groupId types:(EMGroupConfigsType)type configs:(EMGroupConfigs *_Nonnull)configs completion:(void (^_Nullable)(EMGroup *_Nullable group, EMError *_Nullable error))aCompletionBlock;` | `public EMGroup updateGroupConfigs(String groupId, EnumSet types, EMGroupConfigs configs) throws HyphenateException` / `public void asyncUpdateGroupConfigs(final String groupId, final EnumSet types, final EMGroupConfigs configs, final EMValueCallBack callback)` | 同上 / 主要新增 API | include | pending | +| group_get_public_groups_from_server | removed | group | `- (EMCursorResult *_Nullable)getPublicGroupsFromServerWithCursor:(NSString *_Nullable)aCursor pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` 及 completion 版本 | `public EMCursorResult getPublicGroupsFromServer(int pageSize, String cursor) throws HyphenateException` / `public void asyncGetPublicGroupsFromServer(final int pageSize, final String cursor, final EMValueCallBack> callback)` | 其他删除的 API-无客户端替代 | include | pending | +| group_get_joined_groups_from_server | removed | group | `- (void)getJoinedGroupsFromServerWithPage:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize needMemberCount:(BOOL)aNeedMemberCount needRole:(BOOL)aNeedRole completion:(void (^_Nullable)(NSArray *_Nullable aList, EMError *_Nullable aError))aCompletionBlock;` | `public synchronized List getJoinedGroupsFromServer()` / `public synchronized List getJoinedGroupsFromServer(int pageIndex, int pageSize, boolean needMemberCount, boolean needRole)` / `public void asyncGetJoinedGroupsFromServer(final EMValueCallBack> callback)` / `public void asyncGetJoinedGroupsFromServer(final int pageIndex, final int pageSize, boolean needMemberCount, boolean needRole, final EMValueCallBack> callback)`(含 4 个重载) | 服务端拉取 API 迁移 | include | pending | +| group_delegate_join_request_declined | removed | group | `- (void)joinGroupRequestDidDecline:(NSString *_Nonnull)aGroupId reason:(NSString *_Nullable)aReason EM_DEPRECATED_IOS(3_1_0, 4_2_0, "Use -joinGroupRequestDidDecline:reason:applicant: instead");` 与 `- (void)joinGroupRequestDidDecline:(NSString *_Nonnull)aGroupId reason:(NSString *_Nullable)aReason applicant:(NSString* _Nonnull )aApplicant;`(含 2 个重载) | `void onRequestToJoinDeclined(String groupId, String groupName, String decliner, String reason)`(EMGroupChangeListener,4.x 已废弃) | 监听器回调变化汇总 | include | pending | +| group_delegate_user_did_join_group | removed | group | `- (void)userDidJoinGroup:(EMGroup *_Nonnull)aGroup user:(NSString *_Nonnull)aUsername EM_DEPRECATED_IOS(3_1_0, 4_15_0, "Use -userDidJoinGroup:users: instead");` | `void onMemberJoined(final String groupId, final String member)`(EMGroupChangeListener,4.x 已废弃) | 同上 | include | pending | +| group_delegate_user_did_leave_group | removed | group | `- (void)userDidLeaveGroup:(EMGroup *_Nonnull)aGroup user:(NSString *_Nonnull)aUsername EM_DEPRECATED_IOS(3_1_0, 4_15_0, "Use -userDidLeaveGroup:users: instead");` | `void onMemberExited(final String groupId, final String member)`(EMGroupChangeListener,4.x 已废弃) | 同上 | include | pending | +| group_settings_property | param_changed | group | `@property (nonatomic, strong, readonly) EMGroupConfigs *settings;`(原 `EMGroupOptions *settings`,EMGroup;属性名不变,类型改变) | 无(Android diff 未触及 EMGroup 对应访问器) | 群组配置模型重构 | defer | pending | +| group_is_push_notification_enabled | removed | group | `@property (nonatomic, readonly) BOOL isPushNotificationEnabled;`(EMGroup) | 无 | 其他删除的 API-有替代方式 | defer | pending | +| group_search_public_group | removed | group | `- (EMGroup * _Nullable)searchPublicGroupWithId:(NSString *_Nonnull)aGroundId error:(EMError **_Nullable)pError;` 及 completion 版本 | 无 | 其他删除的 API-无客户端替代 | defer | pending | +| group_get_groups_without_push_notification | removed | group | `- (NSArray *)getGroupsWithoutPushNotification:(EMError **)pError EM_DEPRECATED_IOS(3_3_2, 3_8_3, "Use -IEMPushManager::noPushGroups");` | 无 | 其他删除的 API-有替代方式 | defer | pending | +| group_get_group_specification_sync | removed | group | `- (EMGroup * _Nullable)getGroupSpecificationFromServerWithId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` 与 `- (EMGroup * _Nullable)getGroupSpecificationFromServerWithId:(NSString *_Nonnull)aGroupId fetchMembers:(BOOL)fetchMembers error:(EMError **_Nullable)pError;`(含 2 个重载) | 无(Android 对应变更见 group_get_from_server_fetch_members_removed) | 同上 | defer | pending | +| group_get_group_member_list_sync | removed | group | `- (EMCursorResult *)getGroupMemberListFromServerWithId:(NSString *_Nonnull)aGroupId cursor:(NSString *_Nullable)aCursor pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_get_group_blacklist_sync | removed | group | `- (NSArray * _Nullable)getGroupBlacklistFromServerWithId:(NSString *_Nonnull)aGroupId pageNumber:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_get_group_mute_list_sync | removed | group | `- (NSArray * _Nullable)getGroupMuteListFromServerWithId:(NSString *_Nonnull)aGroupId pageNumber:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_get_group_file_list_sync | removed | group | `- (NSArray *_Nullable)getGroupFileListWithId:(NSString *_Nonnull)aGroupId pageNumber:(NSInteger)aPageNum pageSize:(NSInteger)aPageSize error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_get_group_white_list_sync | removed | group | `- (NSArray *)getGroupWhiteListFromServerWithId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_is_member_in_white_list_sync | removed | group | `- (BOOL)isMemberInWhiteListFromServerWithGroupId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_get_group_announcement_sync | removed | group | `- (NSString *_Nullable)getGroupAnnouncementWithId:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_add_occupants_sync | removed | group | `- (EMGroup * _Nullable)addOccupants:(NSArray * _Nonnull)aOccupants toGroup:(NSString *_Nonnull)aGroupId welcomeMessage:(NSString *_Nullable)aWelcomeMessage error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_remove_occupants_sync | removed | group | `- (EMGroup * _Nullable)removeOccupants:(NSArray * _Nonnull)aOccupants fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_block_occupants_sync | removed | group | `- (EMGroup * _Nullable)blockOccupants:(NSArray * _Nonnull)aOccupants fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_unblock_occupants_sync | removed | group | `- (EMGroup * _Nullable)unblockOccupants:(NSArray * _Nonnull)aOccupants forGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_change_group_subject_sync | removed | group | `- (EMGroup * _Nullable)changeGroupSubject:(NSString *_Nullable)aSubject forGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_change_description_sync | removed | group | `- (EMGroup * _Nullable)changeDescription:(NSString *_Nullable)aDescription forGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_leave_group_sync | removed | group | `- (void)leaveGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_destroy_group_sync | removed | group | `- (EMError *)destroyGroup:(NSString *_Nonnull)aGroupId;` | 无 | 同上 | defer | pending | +| group_block_group_sync | removed | group | `- (EMGroup * _Nullable)blockGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_unblock_group_sync | removed | group | `- (EMGroup * _Nullable)unblockGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_update_group_owner_sync | removed | group | `- (EMGroup * _Nullable)updateGroupOwner:(NSString *_Nonnull)aGroupId newOwner:(NSString *_Nonnull)aNewOwner error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_add_admin_sync | removed | group | `- (EMGroup * _Nullable)addAdmin:(NSString *_Nonnull)aAdmin toGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_remove_admin_sync | removed | group | `- (EMGroup * _Nullable)removeAdmin:(NSString *_Nonnull)aAdmin fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_mute_members_sync | removed | group | `- (EMGroup * _Nullable)muteMembers:(NSArray * _Nonnull)aMuteMembers muteMilliseconds:(NSInteger)aMuteMilliseconds fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_unmute_members_sync | removed | group | `- (EMGroup * _Nullable)unmuteMembers:(NSArray * _Nonnull)aMembers fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_mute_all_members_sync | removed | group | `- (EMGroup * _Nullable)muteAllMembersFromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_unmute_all_members_sync | removed | group | `- (EMGroup * _Nullable)unmuteAllMembersFromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_add_white_list_members_sync | removed | group | `- (EMGroup * _Nullable)addWhiteListMembers:(NSArray * _Nonnull)aMembers fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_remove_white_list_members_sync | removed | group | `- (EMGroup * _Nullable)removeWhiteListMembers:(NSArray * _Nonnull)aMembers fromGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_remove_group_shared_file_sync | removed | group | `- (EMGroup * _Nullable)removeGroupSharedFileWithId:(NSString *_Nonnull)aGroupId sharedFileId:(NSString *_Nonnull)aSharedFileId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_update_group_announcement_sync | removed | group | `- (EMGroup * _Nullable)updateGroupAnnouncementWithId:(NSString *_Nonnull)aGroupId announcement:(NSString *_Nullable)aAnnouncement error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_update_group_ext_sync | removed | group | `- (EMGroup * _Nullable)updateGroupExtWithId:(NSString *_Nonnull)aGroupId ext:(NSString *_Nullable)aExt error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_join_public_group_sync | removed | group | `- (EMGroup * _Nullable)joinPublicGroup:(NSString *_Nonnull)aGroupId error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_apply_join_public_group | renamed | group | 旧:`- (EMGroup * _Nullable)applyJoinPublicGroup:(NSString *_Nonnull)aGroupId message:(NSString *_Nullable)aMessage error:(EMError **_Nullable)pError;`(同步)→ 新:`- (void)requestToJoinPublicGroup:(NSString *_Nonnull)aGroupId message:(NSString *_Nullable)aMessage completion:(void (^_Nullable)(EMGroup *_Nullable aGroup, EMError *_Nullable aError))aCompletionBlock;`(4.24.1 已有,5.0.0 仅位置移动) | 无 | 同上 | defer | pending | +| group_accept_join_application | renamed | group | 旧:`- (EMError *)acceptJoinApplication:(NSString *_Nonnull)aGroupId applicant:(NSString *_Nonnull)aUsername;`(同步,删除)→ 新:`approveJoinGroupRequest:sender:completion:`(4.24.1 已有) | 无 | 同上 | defer | pending | +| group_decline_join_application | renamed | group | 旧:`- (EMError *)declineJoinApplication:(NSString *_Nonnull)aGroupId applicant:(NSString *_Nonnull)aUsername reason:(NSString *_Nullable)aReason;`(同步,删除)→ 新:`declineJoinGroupRequest:sender:reason:completion:`(4.24.1 已有) | 无 | 同上 | defer | pending | +| group_accept_invitation_from_group_sync | removed | group | `- (EMGroup * _Nullable)acceptInvitationFromGroup:(NSString *_Nonnull)aGroupId inviter:(NSString *_Nonnull)aUsername error:(EMError **_Nullable)pError;` | 无 | 同上 | defer | pending | +| group_decline_invitation_from_group | renamed | group | 旧:`- (EMError *)declineInvitationFromGroup:(NSString *_Nonnull)aGroupId inviter:(NSString *_Nonnull)aUsername reason:(NSString *_Nullable)aReason;`(同步,删除)→ 新:`declineGroupInvitation:inviter:reason:completion:`(4.24.1 已有) | 无 | 同上 | defer | pending | +| group_is_member_only_renamed | renamed | group | 无 | 旧:`public boolean isMemberOnly()` → 新:`public boolean isJoinApprovalRequired()`(EMGroup) | 相关 API 变化 | defer | pending | +| group_get_users_new | new_api | group | 无(iOS `EMGroup#users` 属性 4.24.1 已存在,diff 未触及;迁移文档误列为新增,见交叉验证已知矛盾 2) | `public List getUsers()`(EMGroup) | 主要新增 API | defer | pending | +| group_get_from_server_fetch_members_removed | param_changed | group | 无 | 旧:`public EMGroup getGroupFromServer(String groupId, boolean fetchMembers) throws HyphenateException` 删除,保留 `public EMGroup getGroupFromServer(String groupId)` | 有替代方式的 API | defer | pending | +| group_load_all_groups_removed | removed | group | 无 | `public synchronized boolean loadAllGroups()` | 服务端拉取 API 迁移 | defer | pending | +| group_async_upload_shared_file_overload_removed | removed | group | 无 | `public void asyncUploadGroupSharedFile(final String groupId, final String filePath, final EMCallBack callBack)` | 其他删除的 API | defer | pending | + +### contact + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| contact_get_all_contacts_from_server | removed | contact | `- (void)getAllContactsFromServerWithCompletion:(void (^_Nullable)(NSArray* _Nullable aList, EMError* _Nullable aError))aCompletionBlock;`(EMContact 模型列表) | `public void asyncFetchAllContactsFromServer(EMValueCallBack> callback)`(EMContact 模型列表) | 服务端拉取 API 迁移 | include | pending | +| contact_get_contacts_from_server_with_cursor | removed | contact | `- (void)getContactsFromServerWithCursor:(NSString* _Nullable)cursor pageSize:(NSUInteger)pageSize completion:(void (^_Nonnull)(EMCursorResult * _Nullable aResult, EMError * _Nullable aError))aCompletionBlock;` | `public void asyncFetchAllContactsFromServer(int limit, String cursor, EMValueCallBack> callback)` | 同上 | include | pending | +| contact_get_contacts_string_list_from_server | removed | contact | `- (void)getContactsFromServerWithCompletion:(void (^)(NSArray *_Nullable aList, EMError *aError_Nullable ))aCompletionBlock;` 与 `- (NSArray *_Nullable )getContactsFromServerWithError:(EMError **_Nullable )pError;`(NSString 列表,含 2 个重载) | `public List getAllContactsFromServer() throws HyphenateException` / `public void asyncGetAllContactsFromServer(final EMValueCallBack> callback)`(String 列表,含 2 个重载) | 同上 | include | pending | +| contact_delegate_on_friend_sync_start | removed | contact | `- (void)onFriendStartSync;`(EMContactManagerDelegate) | `default void onContactSyncStart() {}`(EMContactListener) | 监听器回调变化汇总 / 服务端拉取 API 迁移 | include | pending | +| contact_delegate_on_friend_sync_finished | removed | contact | `- (void)onFriendSyncFinished:(EMError * _Nullable)error;`(EMContactManagerDelegate) | `default void onContactSyncFinishWithError(int errorCode, String error) {}`(EMContactListener) | 同上 | include | pending | +| contact_add_contact_sync | removed | contact | `- (EMError *_Nullable )addContact:(NSString *_Nonnull)aUsername message:(NSString *_Nullable )aMessage;` | 无 | 其他删除的 API-有替代方式 | defer | pending | +| contact_get_blacklist_from_server_sync | removed | contact | `- (NSArray *_Nullable )getBlackListFromServerWithError:(EMError **_Nullable )pError;` | 无 | 同上 | defer | pending | +| contact_add_user_to_blacklist_sync | removed | contact | `- (EMError *_Nullable )addUserToBlackList:(NSString *_Nonnull)aUsername;` | 无 | 同上 | defer | pending | +| contact_remove_user_from_blacklist_sync | removed | contact | `- (EMError *_Nullable )removeUserFromBlackList:(NSString *_Nonnull)aUsername;` | 无 | 同上 | defer | pending | +| contact_accept_invitation_sync | removed | contact | `- (EMError *_Nullable )acceptInvitationForUsername:(NSString *_Nonnull)aUsername;` | 无 | 同上 | defer | pending | +| contact_decline_invitation_sync | removed | contact | `- (EMError *_Nullable )declineInvitationForUsername:(NSString *_Nonnull)aUsername;` | 无 | 同上 | defer | pending | +| contact_get_self_ids_on_other_platform_sync | removed | contact | `- (NSArray *_Nullable )getSelfIdsOnOtherPlatformWithError:(EMError **_Nullable )pError;` | 无 | 同上 | defer | pending | +| contact_save_black_list | new_api | contact | `- (void)saveBlackList:(NSArray *_Nonnull)aBlackList completion:(void (^_Nullable)(EMError *_Nullable aError))aCompletionBlock;` | 无 | 主要新增 API | defer | pending | + +### room + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| room_create_chatroom | removed | room | `- (EMChatroom *_Nullable)createChatroomWithSubject:(NSString *_Nullable)aSubject description:(NSString *_Nullable)aDescription invitees:(NSArray *_Nullable)aInvitees message:(NSString *_Nullable)aMessage maxMembersCount:(NSInteger)aMaxMembersCount error:(EMError **_Nullable)pError;` 及 completion 版本(含 2 个重载) | `public EMChatRoom createChatRoom(String subject, String description, String welcomeMessage, int maxUserCount, List members) throws HyphenateException` / `public void asyncCreateChatRoom(final String subject, final String description, final String welcomeMessage, final int maxUserCount, final List members, final EMValueCallBack callBack)`(含 2 个重载) | 其他删除的 API-无客户端替代 | include | pending | +| room_destroy_chatroom | removed | room | `- (EMError *_Nullable)destroyChatroom:(NSString *_Nonnull)aChatroomId;` 与 `- (void)destroyChatroom:(NSString *_Nonnull)aChatroomId completion:(void (^_Nullable)(EMError * _Nullable aError))aCompletionBlock;`(含 2 个重载) | `public void destroyChatRoom(String chatRoomId) throws HyphenateException` / `public void asyncDestroyChatRoom(final String chatRoomId, final EMCallBack callBack)`(含 2 个重载) | 同上 | include | pending | +| room_get_all_chat_rooms_removed | removed | room | 无 | `public List getAllChatRooms()` | 其他删除的 API | defer | pending | +| room_fetch_from_server_fetch_members_removed | param_changed | room | 无 | 旧:`public EMChatRoom fetchChatRoomFromServer(String roomId, boolean fetchMembers) throws HyphenateException` 删除,保留 `public EMChatRoom fetchChatRoomFromServer(String roomId)` | 有替代方式的 API | defer | pending | +| room_remove_chat_room_listener_removed | renamed | room | 无 | 旧:`public void removeChatRoomListener(EMChatRoomChangeListener listener)` → 新:`public void removeChatRoomChangeListener(EMChatRoomChangeListener listener)` | 有替代方式的 API | defer | pending | +| room_listener_on_member_joined_2arg_removed | removed | room | 无 | `default void onMemberJoined(final String roomId, final String participant){}`(EMChatRoomChangeListener,4.x 已废弃) | 监听器回调变化汇总 | defer | pending | +| room_listener_on_mute_list_added_removed | removed | room | 无 | `void onMuteListAdded(final String chatRoomId, final List mutes, final long expireTime)`(EMChatRoomChangeListener,4.x 已废弃) | 监听器回调变化汇总 | defer | pending | + +### push + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| push_update_push_display_style_sync | removed | push | `- (EMError *)updatePushDisplayStyle:(EMPushDisplayStyle)pushDisplayStyle;` | 无(Android push 模块无公开 API 变化) | 其他删除的 API-有替代方式 | defer | pending | +| push_update_push_display_name_sync | removed | push | `- (EMError *_Nullable )updatePushDisplayName:(NSString * _Nonnull)aDisplayName;` | 无 | 同上 | defer | pending | +| push_get_push_options_from_server_sync | removed | push | `- (EMPushOptions *_Nullable )getPushOptionsFromServerWithError:(EMError *_Nullable *_Nullable)pError;` | 无 | 同上 | defer | pending | + +### error code + +| id | 类型 | 领域 | iOS 源签名 | Android 源签名 | 迁移文档依据 | 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| error_contact_add_faild_typo | renamed | client | 旧:`EMErrorContactAddFaild = 1000,` → 新:`EMErrorContactAddFailed = 1000,`(拼写修正,值不变) | 无(`EMError.java` 在 diff 中零变化) | 无 | defer | pending | + +## 3. 未匹配 / 疑点清单(合并版) + +合并自 `01-api-diff-ios.md` 第 3 节(17 条,编号 i1-i17)与 `01-api-diff-android.md` 第 3 节(10 条,编号 a1-a10),逐条保留;「处理」列标注合并后的去向。末尾 N1-N7 为本次配对新发现的跨端语义不对齐项。 + +### 3.1 来自 iOS 清单 + +| 编号 | 现象 | 双端证据 | 处理 / 建议 | +| --- | --- | --- | --- | +| i1 | `EMGroup.users` 并非 5.0.0 新增,迁移文档「主要新增 API」误列 | iOS:`git grep` 确认 4.24.1 `EMGroup.h:212` 已有 `@property (nonatomic, strong, readonly) NSArray *users;`,diff 未触及;Android:`getUsers()` 为 5.0.0 真新增(defer 项 group_get_users_new) | → 已核实,收入交叉验证「已知矛盾 2」;RN 侧若已封装 `users` 则无需改动 | +| i2 | `sendMessageReadReceipts:completion:` 注释与签名不符(注释描述 `aConversationId` 参数和 `aResult` 字典回调,签名均无) | iOS 5.0.0 `IEMChatManager.h:544` 实测签名仅 `aMessages` + 单 `aError` 回调;Android 对应 `asyncSendMessageReadReceipts` 文档无此问题 | → 已核实,收入交叉验证「已知矛盾 3」;以签名为准 | +| i3 | `addConversationDelegate:delegateQueue:` 的 delegate 参数标 `_Nullable`,而 `removeConversationDelegate:` 标 `_Nonnull`,疑标注疏漏 | 仅 iOS(该能力 Android 由既有 EMConversationListener 承载) | 保留疑点;对应条目 chat_add_conversation_delegate 已 defer,RN 平版时按 `_Nonnull` 语义处理并向上游确认 | +| i4 | 大量「5.0.0 替代 API」实际在 4.24.1 已存在(`modifyMessage:body:ext:`、`fetchMessagesFromServerBy:`、`approveJoinGroupRequest:` 等 20+ 个) | iOS `git grep` 逐一确认;Android 侧同类情况(如 `kickDeviceWithToken`、`onMembersJoined` 等 4.24.1 已存在) | 结论性说明,无需 defer:RN 侧工作以「删除旧封装」为主,「新名字」大多早已存在 | +| i5 | `joinGroupRequestDidReceive:user:reason:` 在 diff 中 +/− 成对、签名一致,仅位置移动 | 仅 iOS | 已确认无 API 变化,不进清单 | +| i6 | `getBlackList`(IEMContactManager)diff 中 −/+ 成对,仅空格/标注格式调整 | 仅 iOS | 已确认无 API 变化,不进清单 | +| i7 | `isMemberInWhiteListFromServerWithGroupId:completion:` 等 7 个 completion 方法 −/+ 成对,仅位置移动或标注微调 | 仅 iOS | 已确认无 API 变化,不进清单 | +| i8 | `EMErrorContactAddFaild → EMErrorContactAddFailed` 拼写修正(值同为 1000)未在迁移文档提及 | 仅 iOS;Android `EMError.java` 零变化 | → 已转为 defer 决策项 error_contact_add_faild_typo;RN 侧若按字符串名映射错误码需同步 | +| i9 | 迁移文档称同步 `renewToken:` 在 5.0.0 保留,diff 未触及,与文档一致 | iOS 无变化;Android 反向:无回调版 `renewToken(String)` 被删(defer 项 client_renew_token_no_callback_removed) | 无需动作(iOS);Android 侧走 defer 决策 | +| i10 | 迁移文档提到的本地读取替代接口(`getAllConversations`、`getJoinedGroups`、`getBlackList`、`isPinned`/`pinnedTime`、`filterConversationsFromDB:`、`disturbType`)均未变 | iOS 4.24.1 已有且 diff 未触及;Android 对应 `getAllConversations()`/`getAllGroups()`/`getContactsFromLocal()` 同样未变 | 无需动作,作为 RN 迁移目标接口确认 | +| i11 | `EMClientDelegate.h` 的 import 由 `EMCommonDefs.h` 改为 `EMOptions.h`(因新回调参数用 `EMDataSyncType`) | 仅 iOS | 对 RN 封装无直接影响 | +| i12 | `EMCommonDefs.h` 中 `EM_DEPRECATED_IOS` 宏定义改为忽略参数 | 仅 iOS | 内部工程调整,无公开 API 影响 | +| i13 | 聊天室 `IEMChatroomManager.h` 仅删除创建/解散共 4 个方法,其余聊天室 API 未动;迁移文档也只提 create/destroy | 双端一致(Android 同删 create/destroy,另多删 `getAllChatRooms()`) | → 已并入 room_create_chatroom / room_destroy_chatroom 两条 include;Android 独有的 getAllChatRooms 删除已 defer | +| i14 | `EMGroupManagerListener.h`(C++ listener 桥接头文件)删除旧 delegate 转发调用 | 仅 iOS,与公开 delegate 删除一一对应 | 无额外公开 API,无需动作 | +| i15 | `EMConversation` 的 `conversationName`/`conversationAvatar` 是实例方法而非属性 | 仅 iOS(Android 为 `getConversationName()` 方法,形态天然一致) | 注意点已写入 conversation_name_avatar 条目;RN 封装按方法调用 | +| i16 | 4.24.2 侧分支 hotfix(`EMUserInfoManager.mm` +2 行)不在 5.0.0 主线 | 仅 iOS | 头部中间 tag 核对已覆盖;实现层非公开 API,若 RN 当前基于 4.24.2 平版需注意该 hotfix 在 5.0.0 不存在 | +| i17 | `EMChatMessage+Private.h` +2 行(内部头文件,未纳入清单) | 仅 iOS | 保留提示:RN iOS 原生层若直接引用 Private 头需另行核对(正常不应引用) | + +### 3.2 来自 Android 清单 + +| 编号 | 现象 | 双端证据 | 处理 / 建议 | +| --- | --- | --- | --- | +| a1 | `onReadReceiptForGroupMessageUpdated` 不存在:迁移文档两处称 `onReadAckForGroupMessageUpdated()` 改名为它,但 5.0.0 源码全树(含内部包)无该方法 | Android:`git grep onReadReceiptForGroupMessageUpdated SDK_5.0.0` 无命中;SDK 仓库自带 `doc/api_changes_5.0.md` 明确「已删除」;iOS 对应回调 `groupMessageAckHasChanged` 也是直接删除 | → 已核实,收入交叉验证「已知矛盾 1」;以源码为准,RN 侧不应平版这个"新回调",群已读更新由 `onMessageReadReceipts` 承载 | +| a2 | 迁移文档称删除「全部 4 个 `asyncFetchConversationsFromServer(...)` 重载」,SDK_4.24.1 实际只有 3 个 | Android:`git grep` SDK_4.24.1 实测 3 个重载(无参 / pageNum+pageSize / limit+cursor);算上 pinned 与 filter 版共 6 个服务端会话拉取相关方法被删 | → 已核实,收入交叉验证「文档有 diff 无 - 数量不符」;以源码为准 | +| a3 | `EMClient#getDeviceInfo()` 疑似内部 API 暴露(无 javadoc、文档未提及,供内部 `setPresence` 用) | 仅 Android | → 已转为 defer 决策项 client_get_device_info_new;建议 RN 侧不平版,待确认 | +| a4 | `EMGroupConfigsType.toNativeMask(EnumSet)` / `EMDataSyncType.toNativeMask` / `fromNativeMask` 为 public static,语义属内部 JNI 掩码转换 | 仅 Android | RN 侧无需平版;include 条目 options_data_sync_type_enum / group_configs_type_enum 平版时只取枚举值,忽略掩码工具方法 | +| a5 | 迁移文档未精确指明的删除:`EMConversation#searchMsgFromDB(..., EMMessageSearchScope)` 同步重载(异步版保留) | 仅 Android | → 已转为 defer 决策项 conversation_search_msg_scope_sync_removed;交叉验证「diff 有文档无」收录 | +| a6 | 迁移文档未明确:`createGroup`/`asyncCreateGroup` 无 avatar 重载删除,5.0.0 仅保留带 avatar 形式 | 双端一致(iOS 同步版与无 avatar 异步版均删) | → 已并入 include 项 group_create_group / group_create_group_with_avatar;交叉验证收录 | +| a7 | 行为变化(无签名变化):`getUnreadMessageCount` 统计范围收窄;`getMessage(String)` 不再自动标已读;`EMChatService` 旧保活移除;推送 Token 上传判断变化;前后台检测改用 AndroidX `ProcessLifecycleOwner` | Android 文档「行为变化」节;iOS 侧前两项同样适用(行为变化 1/4) | 前两项已分别落入 defer 项 chat_get_unread_message_count / conversation_get_message_param_changed 备注;保活与前后台检测对 RN 封装层无 API 影响,仅影响 example 工程依赖 | +| a8 | `EMOptions` 中 `AreaCode` 旧 int 常量(`AREA_CODE_CN` 等 7 个)全删,文档只提 `setAreaCode(int)` → `setAreaCode(AreaCode)` | 仅 Android | → 已转为 defer 决策项 options_area_code_int_removed 等 3 条;若 RN 侧把 areaCode 作 number 透传需改枚举映射 | +| a9 | `EMLoginExtensionInfo` 未变化:`onLogout(int, EMLoginExtensionInfo)` 4.24.1 已是 default 方法,5.0.0 仅去掉代理旧回调 | 仅 Android | 无变化确认;旧回调删除见 defer 项 connection_on_logout_* | +| a10 | 根包其余文件(`EMCallBack`、`EMValueCallBack`、`EMError` 等)diff 为空;`EMSmartHeartBeat`、`EMConversationFilter` 等仅内部调整;`EMPresenceManager`、`EMChatThreadManager`、`EMUserInfoManager`、`EMPushManager` 等无公开 API 签名变化 | Android 核对结论;含输入文件「其他核对的类型」表中 `error_codes_unchanged`、`presence_thread_userinfo_push_unchanged` 两行无变化确认 | 无变化确认,不进变更清单;presence/thread/userinfo/push 四领域双端均无签名变化(iOS 仅 push 同步方法删除 3 条,已 defer) | + +### 3.3 配对中新发现的跨端语义不对齐 + +| 编号 | 现象 | 双端证据 | 建议 | +| --- | --- | --- | --- | +| N1 | 群成员合并列表接口双端引入版本不同:iOS `EMGroup#users` 4.24.1 已有(非新增),Android `EMGroup#getUsers()` 5.0.0 才新增 | i1 + group_get_users_new | RN 侧若已封装 `users` 则两端都不用动;否则按 defer 项决策,注意 iOS 无需等 5.0.0 | +| N2 | `isRead` 双端变化不对等:iOS 仅删 setter(同名变只读);Android 是 `isUnread()` → `isRead()` 改名且**取值取反** | message_is_read 条目双端签名 | RN 平版时 Android 侧语义反转最易出错,TS 模型层若曾映射 `isUnread` 需整体翻转 | +| N3 | iOS 大批量删除群组同步方法(36 条),Android 对应同步方法全部保留 | group 领域 defer 区 | 全部 defer;RN 只用异步封装,预计多数无需处理,待用户确认 | +| N4 | iOS 删除 push 同步方法 3 条,Android push 模块零变化 | push 领域 defer 区 | 全部 defer;RN 若只用 completion 版则无影响 | +| N5 | 输入文件统计口径不一致:iOS 文件自称 105 条但表格实际 150 行;Android 文件自称 82 条但实际 107 行(含 3 行无变化核对行) | 本文件头部说明 | 本文件统计按合并后实际条目计算;如需回修输入文件统计,另行处理(本次约束不改输入文件) | +| N6 | `getUnreadMessageCount`:iOS 为 5.0.0 新增 API,Android 4.24.1 已有仅行为变化 | chat_get_unread_message_count 条目 | 已 defer;行为变化双端一致(均不统计聊天室/免打扰会话) | +| N7 | contact 服务端拉取接口双端命名不对称:iOS `getAllContacts...`=EMContact 列表 / `getContacts...`=NSString 列表;Android 相反(`getAllContactsFromServer`=String 列表 / `asyncFetchAllContactsFromServer`=EMContact 列表) | contact 领域 3 条 include | 已按返回类型语义配对(模型列表/分页/字符串列表各一条),RN 侧按语义而非名字对齐 | + +## 4. 统计 + +合并后总条数:**171**(双端配对共用一 id;同名重载已合并;全部状态 pending)。 + +按类型分布: + +| 类型 | 条数 | +| --- | --- | +| removed | 116 | +| new_api | 15 | +| renamed | 14 | +| param_changed | 12 | +| new_enum | 5 | +| new_listener | 4 | +| new_type | 3 | +| new_property | 2 | +| **合计** | **171** | + +按决策分布: + +| 决策 | 条数 | 说明 | +| --- | --- | --- | +| include | 70 | 双端都有,RN 侧执行对应新增/删除/改名/参数调整 | +| defer | 101 | 仅单端有(iOS-only 71 条、Android-only 30 条),待用户决策 | +| skip | 0 | 两份输入清单中均未出现 `useAgoraChatDomain` 相关项(KI-107 规则备而不用) | + +按领域分布(include / defer): + +| 领域 | include | defer | +| --- | --- | --- | +| client | 18 | 11 | +| chat | 1 | 3 | +| conversation | 10 | 8 | +| message | 17 | 14 | +| group | 12 | 45 | +| contact | 5 | 8 | +| options | 5 | 4 | +| room | 2 | 5 | +| push | 0 | 3 | +| **合计** | **70** | **101** | + +覆盖核对:iOS 输入 150 行条目 = 71 行 defer + 79 行并入 include;Android 输入 107 行 = 3 行无变化核对(转入未匹配清单 a10)+ 30 行 defer + 74 行并入 include。无条目丢失。 + +## 5. 迁移文档交叉验证 + +以两份官方迁移指南为「预期」、git diff 清单为「实际」双向核对。核实命令均为只读:`git show :` / `git grep `,仓库与 tag 见头部。 + +### 5.1 文档有、diff 无(3 条) + +| # | 文档说法 | 核实结果 | 结论 | +| --- | --- | --- | --- | +| 1 | iOS 文档「主要新增 API」列 `EMGroup#users` 为 5.0.0 新增 | `git grep` 证实 4.24.1 `EMGroup.h:212` 已有该属性,diff 未触及 | 文档误列新增;非 diff 遗漏。对应 Android 真新增 `getUsers()` 已 defer(group_get_users_new) | +| 2 | Android 文档两处称 `onReadAckForGroupMessageUpdated()` 改名为 `onReadReceiptForGroupMessageUpdated()` | `git grep onReadReceiptForGroupMessageUpdated SDK_5.0.0` 全树(含内部包)零命中;SDK 仓库自带 `doc/api_changes_5.0.md` 与 `doc/sdk-4.x-to-5.0-migration.md` 均写「已删除」 | 文档错误;旧回调是直接删除,以源码为准(详见 5.3 矛盾 1) | +| 3 | Android 文档称删除「全部 4 个 `asyncFetchConversationsFromServer(...)` 重载」 | `git grep` SDK_4.24.1 实测仅 3 个重载(无参 / pageNum+pageSize / limit+cursor) | 文档数量误差;diff 清单已按实际 3 个收录(chat_fetch_conversations_from_server) | + +除上述外,两份迁移文档列出的所有删除/改名/新增在 diff 清单中均有对应条目。 + +### 5.2 diff 有、文档无(10 条,文档未覆盖的隐藏 breaking change) + +iOS 侧(5 条): + +| # | 条目 | 说明 | +| --- | --- | --- | +| 1 | error_contact_add_faild_typo | `EMErrorContactAddFaild → EMErrorContactAddFailed` 拼写修正(值 1000 不变);RN 若按字符串名映射错误码必须同步 | +| 2 | client_get_device_config | `getDeviceConfig:` 返回值 nullability 标注变化 | +| 3 | chat_load_messages_with_keyword_scope | `loadMessagesWithKeyword:...scope:completion:` nullability 收紧(选择器不变) | +| 4 | chat_delete_messages_before | `deleteMessagesBefore:completion:` completion 增加 nullability 标注 | +| 5 | file_message_body_init_with_local_path | `initWithLocalPath:displayName:` 两参数由 `_Nullable` 收紧为 `_Nonnull` | + +Android 侧(5 条): + +| # | 条目 | 说明 | +| --- | --- | --- | +| 6 | client_get_device_info_new | `EMClient#getDeviceInfo()` 新增 public 方法,无 javadoc、文档未提及,疑似内部方法暴露(已 defer) | +| 7 | client_version_bump | `EMClient.VERSION` 由 `"4.24.1"` 变为 `"5.0.0"` | +| 8 | options_area_code_int_removed | `AreaCode` 旧 int 常量(`AREA_CODE_CN` 等 7 个)全删;文档只提 `setAreaCode(int)` → `setAreaCode(AreaCode)`,未提常量删除 | +| 9 | group_create_group 无 avatar 重载删除 | 文档只说旧 `EMGroupOptions` 重载删除,未明确 5.0.0 仅保留带 avatar 的 createGroup/asyncCreateGroup 形式(iOS 同样删除无 avatar 版) | +| 10 | conversation_search_msg_scope_sync_removed | `EMConversation#searchMsgFromDB(..., EMMessageSearchScope)` 同步重载删除;文档仅笼统提「部分旧版 searchMsgFromDB 调用方式」调整,未指明具体重载 | + +### 5.3 已知矛盾(均已用 git 核实,结论以源码为准) + +1. **Android `onReadReceiptForGroupMessageUpdated` 不存在**:迁移文档「接收消息已读回执」与「监听器回调变化汇总」两处称 `onReadAckForGroupMessageUpdated()` 仅改名。核实:`git grep onReadReceiptForGroupMessageUpdated SDK_5.0.0` 在整个 tag(含内部包)零命中;`onReadAckForGroupMessageUpdated` 仅残留在 proguard.map 与 SDK 自带 doc 中,且自带文档明确写「已删除;群消息已读状态更新统一经 `onMessageReadReceipts(List)` 下发」。**以源码为准:旧回调直接删除,无新名;RN 侧不应平版该"新回调"**。 +2. **iOS `EMGroup.users` 文档误列为新增**:`git grep` 证实 4.24.1 `newSDK/HyphenateSDK/GroupManager/EMGroup.h:212` 已有 `@property (nonatomic, strong, readonly) NSArray *users;`,4.24.1→5.0.0 diff 未触及。**以源码为准:非新增;RN 若已封装 `users` 无需改动**。 +3. **iOS `sendMessageReadReceipts:completion:` 注释与签名不符**:`git show 5.0.0:.../IEMChatManager.h`(544 行附近)实测:中文注释描述 `@param aConversationId 会话 ID`、回调含 `aResult` 字典(key 为消息 ID),但实际签名仅 `(NSArray *)aMessages` + 单 `EMError` 回调。**以签名(源码)为准:无 `aConversationId` 参数、无逐消息 `aResult`;怀疑注释拷贝自内部设计稿,RN 封装回调只暴露整体 error**。 + +另记录一处文档小误差(不影响 diff 对应关系):iOS 文档「已读回执体系重构」表写作 `EMConversation#markMessageAsReadWithId:completion:`,实际签名尾词为 `error:`(见 conversation_mark_message_as_read 条目)。 diff --git a/docs/porting/5.0.0/02-contract.md b/docs/porting/5.0.0/02-contract.md new file mode 100644 index 00000000..d26ee276 --- /dev/null +++ b/docs/porting/5.0.0/02-contract.md @@ -0,0 +1,247 @@ +# 5.0.0 平版 · 阶段二:基线调查与跨端契约(React Native) + +- 目标仓库:`/Users/asterisk/Codes/zuoyu_rn/react-native-chat-sdk`,worktree `.worktree/5.0.0`,分支 `5.0.0`(从 dev/1.20.0 切出) +- RN 目标版本号:**5.0.0**(本次起与 native 版本号同步) +- native 依赖:iOS `HyphenateChat` 4.24.1 → **5.0.0**;Android `io.hyphenate:hyphenate-chat` 4.24.1 → **5.0.0** +- 输入:`01-api-diff.md`(171 条,include 70 / defer 101 / skip 0)、双端迁移指南、基线调查报告 + +## 1. 基线调查结论(要点,证据见调查记录) + +- 桥接架构:TS `Native._callMethod(MTxxx, {[MTxxx]: {...}})` → 单一 TurboModule `callMethod` → Java/ObjC `ExtSdkDispatch` → 各 `ExtSdk{Domain}Wrapper`;返回统一 `{<方法名 key>: object}`;事件为聚合 key + `params.type` 判别(ChatManager 消息事件除外:每个 MTon* 单独 addListener)。 +- 常量四处同步:`src/__internal__/Consts.ts`、`modules/java/.../ExtSdkMethodType.java`、`modules/objc/common/ExtSdkMethodTypeObjc.{h,m}`、`modules/cpp/common/ExtSdkMethodType.{h,cpp}`(cpp 按仓库规范补齐,不作契约依据)。 +- 契约测试强制(`src/__tests__/contract/`):TS MT* ⊆ Java ⊆ ObjC;ObjC key 必须在 methodMap 有条目;`MTon*` ⊆ iOS `supportedEvents`;**`stripDeprecated` 剔除含 `// deprecated` 的行**——删除接口时三层同步删常量,或标 deprecated 豁免。 +- 已存在可复用资产:`ExtSdkGroupMemberInfoHelper`(Java,key:`memberId/joinedTimestamp/role/namecard/nickname/avatarUrl`)与对应 ObjC category;`fetchJoinedGroupCount` 底层 `asyncGetJoinedGroupsCountFromServer` / `getJoinedGroupsCountFromServerWithCompletion:` 在双端 5.0.0 均存活(已 git grep 核实)→ 该 API 不动;`getUnreadMessageCount` RN 已有 → defer 项 N6 无动作。 +- 版本现状:`package.json` 1.20.0;example 三处 1.20.0(`example/package.json`、`example/android/app/build.gradle:93` versionName、`project.pbxproj` MARKETING_VERSION ×2);CHANGELOG 中英双份,首条固定句式记 native 依赖升级。 + +## 2. 已知问题清单浏览结论 + +| 条目 | 命中? | 处理 | +| --- | --- | --- | +| KI-101 podspec/SPM 版本不一致 | 命中风险项 | 本次 bump 两处同步改为 5.0.0,已互核一致 | +| KI-105 cpp 常量文件滞后 | 命中 | 按仓库规范补齐 cpp 三处同步,不作契约对齐依据 | +| KI-107 `useAgoraChatDomain` 对内开关 | 未命中 | diff 双端均未出现该项,skip 条数 0;如实现期发现则不平版 | +| KI-001/002/003/106 | 验证期事项 | 阶段四实机验证时遵守(505 环境限制、脚本 timeout、消息 round-trip、注册名核对) | + +## 3. native 依赖 bump 记录与 hooks + +### 3.1 bump(本阶段第一步,已完成) + +| 位置 | 旧 | 新 | +| --- | --- | --- | +| `ChatSdk.podspec:53` SPM `minimumVersion` | `4.24.1` | `5.0.0` | +| `ChatSdk.podspec:57` CocoaPods `s.dependency 'HyphenateChat'` | `~> 4.24.1` | `~> 5.0.0` | +| `android/build.gradle:91` | `io.hyphenate:hyphenate-chat:4.24.1` | `io.hyphenate:hyphenate-chat:5.0.0` | + +互核命令输出(2026-09-15): + +```text +$ grep -n 'minimumVersion\|s.dependency' ChatSdk.podspec && grep -n 'hyphenate-chat' android/build.gradle +53: requirement: { kind: 'upToNextMinorVersion', minimumVersion: '5.0.0' }, +57: s.dependency 'HyphenateChat', '~> 5.0.0' +91: implementation 'io.hyphenate:hyphenate-chat:5.0.0' +``` + +发布状态(2026-09-16 已全部确认):Android Maven `io.hyphenate:hyphenate-chat:5.0.0` 已由阶段四真实构建实证;iOS CocoaPods trunk 与 SPM 仓库均已发布 HyphenateChat 5.0.0(用户确认远端)。无需走本地依赖编译验证流程。 + +### 3.2 hooks + +当前 harness(Kimi Code)hooks 事件契约与 skill 自带 Codex 契约(PreToolUse/Stop + stdin JSON 字段)不一致,盲接有误拦风险 → 按 skill「无 hooks 能力的 agent」档执行**流程强制**: + +- `gate` 列为每阶段门禁必跑命令,输出原样附进阶段产物(本节与 03/04 均附)。 +- `pre` 靠纪律自约束:不主动执行 git 提交类命令;验收报告标注代码「未提交」状态。 + +阶段二 gate 留痕: + +```text +$ echo '{}' | bash /Users/asterisk/Codes/zuoyu_rn/.agents/skills/platform-sdk-porting-v2/hooks/porting_guard.sh gate react-native /Users/asterisk/Codes/zuoyu_rn/react-native-chat-sdk/.worktree/5.0.0 +(无输出)gate_exit=0 +``` + +## 4. 跨端契约(冻结) + +通则:方法名 key(MT 值)、参数 JSON key、返回 JSON key、事件名与负载 key 在 TS / Java / ObjC 三处**逐字一致**;新 MT 常量四处(含 cpp)同步;新事件聚合 key 进 iOS `supportedEvents`。以下只列 include 70 条中需要契约要素的项;纯删除项列在 §5 删除清单(无新契约)。 + +### 4.1 client(登录与设备管理) + +| 项 | 契约 | +| --- | --- | +| `MTlogin` 保留 | 仅 token 登录。TS 删除 `login()` 与 `loginWithAgoraToken()`;保留 `loginWithToken(userId, token)`。wrapper 删除 isPassword 分支,固定调 token 登录;payload 其余 key 不变 | +| 设备管理三方法 | TS 签名改为 `getLoggedInDevicesFromServer(userId, token)`、`kickDevice(userId, token, resource)`、`kickAllDevices(userId, token)`(删 `isPassword` 参数)。MT key 不变(`getLoggedInDevicesFromServer`/`kickDevice`/`kickAllDevices`)。payload key:`password` → **`token`**,其余 key(`username`/`resource`)不变。wrapper 改调 token 版 native API(iOS `getLoggedInDevicesFromServerWithUserId:token:completion:` 等;Android `fetchLoggedInDevicesFromServerWithToken`/`kickDeviceWithToken`/`kickAllDevicesWithToken`) | +| `MTonUserDidLoginFromOtherDevice` | 已是 deprecated 豁免状态,5.0 双端旧回调均删 → 三层同步删除该常量与接线;保留 `MTonUserDidLoginFromOtherDeviceWithInfo` | +| `renewAgoraToken` | native 异步 `renewToken` 双端存活 → wrapper 不动。**用户裁决(2026-09-16):TS 公开方法改名 `renewToken`**(参数 `agoraToken`→`token`),MT 常量三层本已是 `renewToken` 不动 | + +新增事件(挂连接监听组 `ChatConnectEventListener`,聚合方式同现有 `MTonConnected` 等:每事件独立 MT key、独立 addListener): + +| MT 常量(值逐字) | 事件负载 JSON | TS listener 方法 | native 来源 | +| --- | --- | --- | --- | +| `MTonDataSyncStart = 'onDataSyncStart'` | `{type: number}`(掩码值,见 4.2 枚举) | `onDataSyncStart?(type: ChatDataSyncType)` | iOS `syncDataStartWithType:`;Android `onDataSyncStart(EMDataSyncType)`(wrapper 用 `getValue()` 转 int) | +| `MTonDataSyncFinish = 'onDataSyncFinish'` | `{type: number, errorCode: number}`(0=成功;iOS 取 `error.code`,无错为 0) | `onDataSyncFinish?(type: ChatDataSyncType, errorCode: number)` | iOS `syncDataFinished:type:`;Android `onDataSyncFinish(EMDataSyncType, int)` | +| `MTonDatabaseOpened = 'onDatabaseOpened'` | `{username: string, errorCode: number}`(iOS `error?.code ?? 0`;Android 恒 0) | `onDatabaseOpened?(username: string, errorCode: number)` | iOS `onDatabaseOpened:username:`;Android `onDatabaseOpened(String)` | + +三个 MTon* 均须进 `ExtSdkApiObjcRN.mm` `supportedEvents`。 + +连接事件收敛(用户裁决 2026-09-20,取代本节"每事件独立 MT key"通则中涉及强制下线回调的部分): + +- `ChatConnectEventListener` 的 8 个服务器强制下线回调(`onAppActiveNumberReachLimit`、`onUserDidLoginFromOtherDeviceWithInfo`、`onUserDidRemoveFromServer`、`onUserDidForbidByServer`、`onUserDidChangePassword`、`onUserDidLoginTooManyDevice`、`onUserKickedByOtherDevice`、`onUserAuthenticationFailed`)全部删除,统一为 `onDisconnected?(errorCode?: number, info?: { deviceName?: string; ext?: string })`;新增 `ChatDisconnectErrorCode` 枚举(值与 native `EMError` 一致,覆盖断开路径全量码:退出原因 8/104/110/202/204/206/207/213/214/216/217/220/304/305,连接原因 2/4/300/303/306;2026-09-21 修订补全,见 `docs/spec/2026-09-20-connect-event-listener-spec.md` §7)。 +- 事件负载统一为 `{errorCode?: number, deviceName?: string, ext?: string}`,`deviceName`/`ext` 仅 206 携带。MT key 仅保留 `MTonDisconnected`,8 个被删事件的 MT 常量在 TS/Java/ObjC 三层同步删除(含 dispatch 的 "no implement" stub 与 `supportedEvents`)。 +- Android wrapper:`onDisconnected(int)` 直接透传 code(206 跳过,由 `onLogout(int, EMLoginExtensionInfo)` 合并 deviceName/ext 后发同一 `onDisconnected` 事件);`onLogout` 仅处理 206,修掉"每次强制下线都多发一次 WithInfo 空参数事件"的双发问题。5.0.0 源码确认 `onDisconnected(int)` 对所有码无条件触发、`onLogout` 对 8/206/207/213/214/216/217/220/305 九个码额外触发。 +- iOS wrapper:`userAccountDidForcedToLogout:` 透传 `aError.code`;`userAccountDidRemoveFromServer`→207、`userDidForbidByServer`→305、`userAccountDidLoginFromOtherDeviceWithInfo:`→206+info(补 nil 保护);`connectionStateDidChange(NO)` 发无码断开,用 `forcedLogoutPending` 标志吞掉强制下线后重复的断开回调。 +- 语义规则(2026-09-21 修订):`errorCode` 原样透传、SDK 不过滤,枚举未列出的码(含 native 未来新增)也会送达,默认按连接原因处理;退出原因码见枚举的 logout 分组,收到时回登录页;连接原因码(2/4/300/303/306)保持登录态、自动重连;无 `errorCode` 表示平台未提供原因(如 iOS 网络断开、App 切后台),按连接事件处理;token 过期登出不走 `onDisconnected`,仅 `onTokenDidExpire`——退出共两个事件通道,接入方需同时处理。 + +### 4.2 options(ChatOptions) + +- 删除属性:`autoLogin`、`requireAck`、`enableAutoSyncContacts`(含构造函数参数与 wrapper fromJson/toJson 对应 key `autoLogin`/`requireAck`/`enableAutoSyncContacts`)。 +- 新增属性 `dataSyncType`: + +```ts +export enum ChatDataSyncType { + None = 0, + Conversations = 1, // 1 << 0 + Contacts = 2, // 1 << 1 + JoinedGroups = 4, // 1 << 2 +} +// ChatOptions.dataSyncType?: ChatDataSyncType(数值掩码,用法 ChatDataSyncType.Conversations | ChatDataSyncType.Contacts) +``` + +JSON key:`dataSyncType`(int 掩码)。iOS wrapper 直接赋 `EMOptions.dataSyncType`;Android wrapper 用 `EMDataSyncType.fromNativeMask(int)` 转 `EnumSet`。**掩码位值双端一致(1/2/4),无映射问题**。默认值差异已在迁移文档注明(iOS 默认 Conversations,Android 默认 NONE),RN 不主动设默认值、遵循 native 默认 ⚠️。 +- `areaCode`:TS 接口不变(`ChatAreaCode` 数值)。Android wrapper `setAreaCode(int)` 5.0.0 已删 → 改为 int→`EMOptions.AreaCode` 枚举映射(CN=1/NA=2/EU=4/AS=8/JP=16/IN=32/GLOB=-1,switch 实现);iOS 无变化。 + +### 4.3 已读回执体系(message/chat,本次最大改动块) + +**删除**(三层同步删,含 MT 常量、TS 方法、wrapper 方法、dispatch case):`MTackMessageRead`、`MTackGroupMessageRead`、`MTackConversationRead`、`MTmarkAllChatMsgAsRead`、`MTmarkMessageAsRead`、`MTmarkAllMessagesAsRead`、`MTasyncFetchGroupAcks`、`MTgroupAckCount`;对应 TS 方法 `sendMessageReadAck`/`sendGroupMessageReadAck`/`sendConversationReadAck`/`markAllConversationsAsRead`/`markMessageAsRead`/`markAllMessagesAsRead`/`fetchGroupAcks`/`groupAckCount`。 + +**删除事件**:`MTonMessagesRead`、`MTonGroupMessageRead`、`MTonConversationHasRead`、`MTonReadAckForGroupMessageUpdated` 及 `ChatMessageEventListener` 的 `onMessagesRead`/`onGroupMessageRead`/`onConversationRead`;wrapper 删除对应 native 回调的注册与发射。(`onReadReceiptForGroupMessageUpdated` 在 native 5.0.0 **不存在**,迁移文档说法错误,不平版——交叉验证矛盾 1。) + +**新增 API**: + +| TS 方法 | MT key(值逐字) | 请求 payload(内层) | 返回 payload | 备注 | +| --- | --- | --- | --- | --- | +| `sendMessageReadReceipts(msgIds: string[]): Promise` | `sendMessageReadReceipts` | `{msg_ids: string[]}` | 无 | wrapper 按 msgId 逐个查库取消息对象后调 native 批量接口;约束写进注释:≤50 条、同一会话、仅 `isNeedReadReceipt=true` 的消息生效 | +| `clearConversationUnreadMessageCount(convId: string): Promise` | `clearConversationUnreadMessageCount` | `{convId: string}` | 无 | 只清本地未读数+同步多设备,不发已读回执 | +| `clearAllConversationUnreadMessageCount(): Promise` | `clearAllConversationUnreadMessageCount` | `{}` | 无 | 同上 | +| `getGroupMessageReadReceipts(msgIds: string[]): Promise` | `getGroupMessageReadReceipts` | `{msg_ids: string[]}` | `{getGroupMessageReadReceipts: [{...receipt}]}` | ≤20 条、同一会话 | +| `fetchGroupMessageReadReceipts(msgId, groupId, cursor?, pageSize?): Promise>` | `fetchGroupMessageReadReceipts` | `{msg_id, group_id, receipt_id, pageSize}` | `{fetchGroupMessageReadReceipts: {cursor, list, totalCount?}}` | 替代 `fetchGroupAcks`;Android 5.0 无 groupId 参数(wrapper 收下不用)⚠️;`totalCount` 仅 iOS 有,TS 侧 optional ⚠️ | + +**新增事件**:`MTonMessageReadReceipts = 'onMessageReadReceipts'`,负载 `{receipts: [{...}]}`;ChatManager 按消息事件惯例单独 addListener;`ChatMessageEventListener.onMessageReadReceipts?(receipts: ChatMessageReadReceipt[])`。进 iOS `supportedEvents`。 + +**新增/改名模型**: + +```ts +// 新增 src/common/ChatMessageReadReceipt.ts(native JSON → 构造) +class ChatMessageReadReceipt { msgId: string; convId: string; isPeerReceipt: boolean; readCount: number; } +// JSON keys:msg_id / conv_id / isPeerReceipt / readCount + +// 改名 src/common/ChatGroup.ts 内 ChatGroupMessageAck → ChatGroupReadReceipt +class ChatGroupReadReceipt { msgId: string; ackId: string; from: ChatGroupMemberInfo; count: number; timestamp: number; } +// JSON keys:msg_id / ack_id / from / count / timestamp(content 删除;from 由 string 变为对象) + +// 新增 ChatGroupMemberInfo(复用既有 wrapper 序列化 key):memberId / joinedTimestamp / role / namecard? / nickname? / avatarUrl? +// TS 字段同名;role 用既有 ChatGroupPermissionType 枚举转换路径 +``` + +**ChatMessage 属性改名(JSON key 同步改)**: + +| 旧(TS 属性 / JSON key) | 新(TS 属性 / JSON key) | 双端 wrapper 映射 | +| --- | --- | --- | +| `hasReadAck` / `hasReadAck` | `isPeerRead` / `isPeerRead` | iOS `isPeerRead`;Android `isPeerRead()`。只读:fromJson 不再 set(setter 已包私有/只读) | +| `hasRead` / `hasRead` | `isRead` / `isRead` | **直接映射,双端均不取反** ⚠️ Android 旧代码 `!message.isUnread()` 必须删(语义反转点,交叉验证 N2);iOS `isRead` 本就正向 | +| `needGroupAck` / `needGroupAck` | `isNeedReadReceipt` / `isNeedReadReceipt` | iOS `isNeedReadReceipt`;Android `isNeedReadReceipt()` / `setIsNeedReadReceipt()`(**唯一保留的公开 setter**,fromJson 保留该 set) | +| `groupAckCount` / `groupAckCount` | `groupReadReceiptCount` / `groupReadReceiptCount` | iOS `groupReadReceiptCount`;Android `readReceiptCount()`。只读 | + +### 4.4 群组配置模型重构(group) + +- **删除** TS `ChatGroupStyle` 枚举与 `ChatGroupOptions` 类;删除已 deprecated 的 `createGroup()`(旧签名)。 +- **新增**: + +```ts +class ChatGroupConfigs { + maxCount: number; // 默认 200 + inviteNeedConfirm: boolean; + ext?: string; + isPublic: boolean; // 默认 false + joinApprovalRequired: boolean; + allowInvites: boolean; +} +// JSON keys(双端 wrapper 逐字一致):maxCount / inviteNeedConfirm / ext / isPublic / joinApprovalRequired / allowInvites + +enum ChatGroupConfigsType { // 掩码位序以 iOS 为准绳 + AllowInvites = 1, // 1<<0 + MaxUsers = 2, // 1<<1 + InviteNeedConfirm = 4, // 1<<2 + JoinApprovalRequired = 8, // 1<<3 + IsPublic = 16, // 1<<4 + Ext = 32, // 1<<5 +} +``` + +⚠️ Android 枚举位序与 iOS **不同**(IS_PUBLIC=1<<0…):iOS wrapper 直接透传掩码;**Android wrapper 必须逐位映射**到 `EnumSet`,不得直接 `toNativeMask`。 + +- `ChatGroup.options?: ChatGroupOptions` → **`ChatGroup.configs?: ChatGroupConfigs`**;JSON key `options` → **`configs`**。wrapper 群序列化改写:iOS 读 `EMGroup.settings`(EMGroupConfigs);Android 用 `EMGroup` 各 getter(`isPublic()`/`isJoinApprovalRequired()`/`isMemberAllowToInvite()`/`getMaxUserCount()` 等,实现期按 5.0.0 可用 getter 核对)。`isDisabled` 保留在 `ChatGroup` 顶层(来自 `EMGroup.isDisabled`,不属于 configs)。 +- `createGroupEx(params)`:参数 `options?: ChatGroupOptions` → `configs?: ChatGroupConfigs`;payload 内层 key `options` → `configs`(其余 key `groupName/groupAvatar/desc/inviteMembers/inviteReason` 不变,avatar 双端已通)。MT key `createGroup` 不变。 +- **新增** `updateGroupConfigs`: + +| TS 方法 | MT key | payload | 返回 | +| --- | --- | --- | --- | +| `updateGroupConfigs(groupId: string, types: ChatGroupConfigsType, configs: ChatGroupConfigs): Promise` | `updateGroupConfigs` | `{group_id: string, types: number(掩码), configs: {...}}` | `{updateGroupConfigs: {...group json}}` | + +- **删除**:`fetchPublicGroupsFromServer`(`MTgetPublicGroupsFromServer`)、`fetchJoinedGroupsFromServer`(`MTgetJoinedGroupsFromServer`)及对应 wrapper 方法。 +- **事件**:`onRequestToJoinDeclined` 负载增加 `applicant`(双端 5.0 回调均带申请者 ID;iOS `joinGroupRequestDidDecline:reason:decliner:applicant:` / Android `onRequestToJoinDeclined(..., applicant)`)。旧单成员回调(`userDidJoinGroup:user:`/`onMemberJoined` 单参版等)native 已删,wrapper 若仍有残留注册一并清除(RN 自 4.15 起已用多成员版,预期无 TS 改动)。 + +### 4.5 会话(conversation) + +- **删除服务端拉取**:`fetchAllConversations`(`MTgetConversationsFromServer`)、`fetchConversationsFromServerWithPage`(Consts.ts:109 对应常量)、`fetchConversationsFromServerWithCursor`(`MTgetConversationsFromServerWithCursor`)、`fetchPinnedConversationsFromServerWithCursor`(Consts.ts:118 对应常量)及 wrapper 方法。 +- **删除** `MTfetchHistoryMessages`(旧分页 API 的 deprecated TS 方法 `fetchHistoryMessages`);`fetchHistoryMessagesByOptions`(`MTfetchHistoryMessagesByOptions`)保留(4.24.1 已是新 API)。 +- `conversation_name_avatar`:双端 5.0 新增 `conversationName`/`conversationAvatar`。RN 已有 TS 侧 `ChatConversation.name()` 异步 helper → 决策:**wrapper 会话 toJson 增加 key `name`/`avatar`**(iOS 调 `conversationName`/`conversationAvatar` 方法,Android 调 `getConversationName()`/`getConversationAvatar()`),TS `ChatConversation` 新增只读属性 `displayName?`/`displayAvatar?`(避开与既有 `name()` 方法冲突)⚠️ 命名映射记验收报告;既有 `name()` 方法不动。 +- 会话列表回调迁移(iOS `EMConversationDelegate` 独立协议):RN 事件层无感知(现有 `MTonConversationChanged` 等不变),仅 iOS wrapper 注册位置改为 `addConversationDelegate:delegateQueue:`;Android 仍 `EMConversationListener`。实现期核对。 + +### 4.6 联系人(contact) + +- **删除**:`getAllContactsFromServer`(`MTgetAllContactsFromServer`)、`fetchAllContacts`(`MTfetchAllContacts`)、`fetchContacts`(`MTfetchContacts`)及 wrapper 方法;保留本地 `getAllContactsFromDB`。 +- **删除事件**:`ChatContactEventListener.onContactSyncStart`/`onContactSyncFinish`;wrapper 停止发射这两个 type(`onContactChanged` 聚合 key 保留,其余 type 不变)。替代路径 = 4.1 的连接级 `onDataSyncStart/Finish`(type 含 Contacts 位)。 + +### 4.7 聊天室(room) + +- 删除:`createChatRoom`(`MTcreateChatRoom`)、`destroyChatRoom`(`MTdestroyChatRoom`)三层。**用户裁决(2026-09-16):`getAllChatRooms` 三端删除不保留**(TS 公开方法本不存在,清残余 MT 常量;Java/objc wrapper、dispatch、常量全删)。其余 room API 不动。 + +### 4.8 其他 include 项处理备忘 + +| 项 | 决策 | +| --- | --- | +| `chat_modify_message`(加 ext 参数) | TS `modifyMessage(msgId, body, ext?)` 增加可选 `ext`;payload 增加 key `ext`(可选);wrapper 解析并调四参 native 版。MT key `modifyMessage` 不变 | +| `chat_report_message` | 三层删除 `MTreportMessage` 与 `reportMessage` | +| `client_statistics_module` | RN 三层均无 → 无动作(核对后关闭) | +| `multidevice 事件 65/66` | TS 多设备事件枚举增加 `ConversationUnreadMessageCountCleared = 65`、`AllConversationUnreadMessageCountCleared = 66`(事件 int 透传链路不变,仅枚举与注释) | +| `error_contact_add_faild_typo` | RN 按数值 code 映射错误(`ChatError.code`),iOS 枚举改名值不变 → 无动作(阶段三 grep 核实后关闭) | +| `chat_get_unread_message_count`(N6) | RN 已有 `getUnreadMessageCount` → 无动作;统计范围行为变化写进 CHANGELOG ⚠️ | +| `client_auto_login_behavior` 行为变化 | RN 无自动登录代码路径 → 无动作;行为变化写 CHANGELOG | + +## 5. 删除清单汇总(三层同步删除的 MT 常量) + +client:`MTcreateAccount`、`MTloginWithAgoraToken`、`MTisLoggedInBefore`、`MTonUserDidLoginFromOtherDevice`(本已 deprecated) +chat/message:`MTackMessageRead`、`MTackGroupMessageRead`、`MTackConversationRead`、`MTmarkAllChatMsgAsRead`、`MTmarkMessageAsRead`、`MTmarkAllMessagesAsRead`、`MTasyncFetchGroupAcks`、`MTgroupAckCount`、`MTreportMessage`、`MTfetchHistoryMessages`、`MTonMessagesRead`、`MTonGroupMessageRead`、`MTonConversationHasRead`、`MTonReadAckForGroupMessageUpdated` +conversation:`MTgetConversationsFromServer`、`MTgetConversationsFromServerWithCursor`、Consts.ts:109/118 对应的分页/置顶常量 +group:`MTgetJoinedGroupsFromServer`、`MTgetPublicGroupsFromServer` +contact:`MTgetAllContactsFromServer`、`MTfetchAllContacts`、`MTfetchContacts` +room:`MTcreateChatRoom`、`MTdestroyChatRoom` + +新增 MT 常量:`sendMessageReadReceipts`、`clearConversationUnreadMessageCount`、`clearAllConversationUnreadMessageCount`、`getGroupMessageReadReceipts`、`fetchGroupMessageReadReceipts`、`updateGroupConfigs`、`onDataSyncStart`、`onDataSyncFinish`、`onDatabaseOpened`、`onMessageReadReceipts`。 + +删除纪律:能三层同步删净的直接删;有外部引用风险或需保留兼容的一律按仓库契约测试惯例标 `// deprecated` 豁免(默认选「删净」,5.0.0 是主版本,不留兼容层)。 + +## 6. defer 项处理原则(101 条不实施,进验收报告待用户决策) + +- iOS 群组/联系人/push 同步方法批量删除(约 45 条):RN 只用异步封装,**预期无需处理**,实现期如编译暴露残留引用再个案清除并记录。 +- Android 独有变更(30 条):逐条核对 RN 是否封装(如 `getDeviceInfo` 不平版、`asyncDeleteConversations` RN 未封装等),核对结果写进 03-implementation.md。 +- 实现期因 bump 5.0.0 导致的**编译强制改动**(wrapper 引用了已删 native API)不受 defer 限制,必须改通,逐条记录。 + +## 7. 待决策(2026-09-16 用户已全部裁决,结果记录于验收报告 §5) + +1. ~~`renewAgoraToken` 是否改名 `renewToken`~~ → **裁决:改名**,已实施。 +2. ~~`ChatMessage.hasReadAck→isPeerRead` 等四个属性改名~~ → **裁决:接受**。 +3. ~~`fetchGroupMessageReadReceipts` 跨端差异~~ → **裁决:接受,TS 保持现状**。 +4. ~~defer 101 条逐条确认~~ → **裁决:按默认批量通过「无需处理」**。 +5. ~~`dataSyncType` 双端默认值不一致~~ → **裁决:接受,RN 不设显式默认**。 + +追加裁决(同批): +- **`autoLoginDidCompleteWithError:`(iOS)**:契约层面**标记作废、本条目不删除**(用户裁决:契约暂时保留标记);实现层 iOS wrapper 已删除该 delegate 及其派发的 `activeNumbersReachLimitation`(native 5.0.0 已无触发源,`userDidForbidByServer` 有独立 delegate 保留)。副作用:TS 事件 `onAppActiveNumberReachLimit` 自此仅 Android 可触发(Android wrapper 由断连错误码 8 触发,native 错误码仍存活)——保留该事件,跨端差异写入 CHANGELOG。(2026-09-20 更新:此裁决已被"连接事件收敛"取代——`onAppActiveNumberReachLimit` 连同其余 7 个强制下线回调全部删除,统一为 `onDisconnected(errorCode)`,见 §4.1 末尾。) +- **RN 自身 deprecated API 17 处一并删除**;`ChatAreaCode`/`ChatDataSyncType` 枚举并入 `ChatOptions.ts`;`fetchConversationsByOptions` 的 `EMConversationFilter` 残留(iOS Json category)删除。 diff --git a/docs/porting/5.0.0/03-implementation.md b/docs/porting/5.0.0/03-implementation.md new file mode 100644 index 00000000..bc41a8f0 --- /dev/null +++ b/docs/porting/5.0.0/03-implementation.md @@ -0,0 +1,114 @@ +# 5.0.0 平版 · 阶段三:分层实现记录(React Native) + +- 契约依据:`02-contract.md`(已冻结);变更清单:`01-api-diff.md` +- 实施方式:契约冻结后三个子代理并行(TS 层 / Android wrapper+cpp / iOS wrapper),文件边界互斥;主代理负责收尾(版本号、CHANGELOG、example/ci 脚本、AGENTS.md 滞后条目)与全量静态验证。 +- 子代理模型:与主代理同级(kimi-code/k3)。 + +## 1. 各层实施摘要 + +### TypeScript 层(src/**、example/src/**) + +- `Consts.ts`:删 26 个 MT 常量(契约 §5 全清单);新增 10 个(值逐字按契约)。 +- `ChatClient.ts`:删 `login`/`loginWithAgoraToken`/`createAccount`/`isLoginBefore`;设备管理三方法改 token 鉴权(payload key `password`→`token`);新增 `onDataSyncStart`/`onDataSyncFinish`/`onDatabaseOpened` 三事件订阅与分发;删 `MTonUserDidLoginFromOtherDevice` 接线。 +- `ChatManager.ts`:删 14 个旧方法;新增 5 个(`sendMessageReadReceipts`/`clearConversationUnreadMessageCount`/`clearAllConversationUnreadMessageCount`/`getGroupMessageReadReceipts`/`fetchGroupMessageReadReceipts`,约束写进 TypeDoc);`modifyMessageBody` 加 `ext?`;事件订阅删 4 加 `onMessageReadReceipts`。 +- 模型:新增 `ChatMessageReadReceipt`/`ChatGroupMemberInfo`/`ChatDataSyncType`;`ChatGroupMessageAck`→`ChatGroupReadReceipt`(`from` 变 `ChatGroupMemberInfo`,删 `content`);删 `ChatGroupStyle`/`ChatGroupOptions`,新增 `ChatGroupConfigs`+`ChatGroupConfigsType`(位序 1/2/4/8/16/32 以 iOS 为准绳);`ChatGroup.options`→`configs`,`isDisabled` 提升顶层;`ChatMessage` 四属性改名(`isPeerRead`/`isRead`/`isNeedReadReceipt`/`groupReadReceiptCount`);`ChatConversation` 加 `displayName?`/`displayAvatar?`;`ChatCursorResult` 加 `totalCount?`;`ChatOptions` 删 3 属性加 `dataSyncType`。 +- `ChatGroupManager.ts`:`createGroupEx` 参数/payload `options`→`configs`;新增 `updateGroupConfigs`;删 `createGroup`/`fetchJoinedGroupsFromServer`/`fetchPublicGroupsFromServer`。 +- `ChatContactManager.ts`/`ChatRoomManager.ts`:按契约删方法与事件分支。`ChatEvents.ts`:listener 接口同步增删;多设备事件枚举加 65/66。 +- `index.ts` 三行新导出;单测同步更新(models/ChatManager.events/ChatClient 用例)。 + +### Android wrapper(modules/java/** + modules/cpp/common) + +- 常量:删 31 / 新增 10;dispatch case 同步;cpp 常量同步(cpp 为 Java 子集)。 +- Client:login 固定 token;设备管理三方法调 token 版(`getLoggedInDevicesFromServer` 编译强制改异步 `fetchLoggedInDevicesFromServerWithToken`);连接监听器新增三数据同步事件发射;删 `onLogout(int,String)` override。 +- Chat:删 11 个旧方法;新增 5 个(`msg_ids` 逐个查库取消息对象后调批量接口);消息监听器改 `onMessageReadReceipts`;会话监听器删 `onConversationRead`。 +- 序列化(ExtSdkHelper):消息四 key 改名——**`isRead` 直接映射不取反(旧 `!isUnread()` 已删,N2 语义反转点已处理)**;fromJson 仅保留 `setIsNeedReadReceipt`(其余 setter 已包私有);`ExtSdkGroupAckHelper`→`EMGroupReadReceipt`,新增 `ExtSdkMessageReadReceiptHelper`;`ExtSdkGroupOptionsHelper`→`ExtSdkGroupConfigsHelper`(六 key);群 toJson `options`→`configs`;options 删 3 key 加 `dataSyncType`(`fromNativeMask`);`areaCode` 改 int→`AreaCode` 枚举映射(含 GLOB(-1) 兜底)。 +- Group:createGroup 改 configs;`updateGroupConfigs` **逐位掩码映射**到 `EnumSet`(未直接 toNativeMask,Android 位序不同);删服务端拉取两方法;`getJoinedGroups` 删 `loadAllGroups()` 调用(编译强制,5.0 已包私有)。 +- Contact/Room:按契约删方法;`removeChatRoomListener`→`removeChatRoomChangeListener`(编译强制)。 +- **已用真实 `hyphenate-chat:5.0.0` AAR(Maven Central)离线 javac 编译验证通过,0 error**。 + +### iOS wrapper(modules/objc/**) + +- 常量四层(h key/value、m methodMap、dispatch case、supportedEvents)同步删 29 / 增 10。 +- Client:login 固定 `loginWithUsername:token:completion:`;设备管理三方法 token 版;新增三数据同步回调发射;删旧多端登录回调。 +- Chat:删 10+ 旧方法;新增 5 个(`fetchGroupMessageReadReceipts` 返回 `{cursor,list,totalCount}`);delegate 改 `onMessageReadReceipts:`;**`conversationListDidUpdate:` 迁移到 `EMConversationDelegate` 协议**,注册改 `addConversationDelegate:delegateQueue:`。 +- 序列化(ExtSdkToJson):消息四 key 改名,fromJson 仅留 `isNeedReadReceipt`;`EMGroupMessageAck (Json)`→`EMGroupReadReceipt (Json)`,新增 `EMMessageReadReceipt (Json)`;`EMGroupOptions (Json)`→`EMGroupConfigs (Json)`;群 toJson `options`→`configs`(读 `EMGroup.settings`);EMOptions 删 3 加 `dataSyncType`;EMConversation toJson 加 `name`/`avatar`。 +- Group:`updateGroupConfigs` 掩码直传(iOS 位序与契约一致);删单成员版加入/离开回调。 +- 编译强制改动 5 处(mergeMessage 只读属性、resendMessage→sendMessage、updateImPushStyle 改 completion 版、删 getGroupsWithoutPushNotification wrapper、群 toJson 删 noticeEnable/style/isMemberOnly key)。 + +## 2. ⚠️ 推断项汇总(随验收报告上交) + +1. TS `modifyMessageBody` 加 `ext`(契约写的方法名是 `modifyMessage`,现存承载方法是 `modifyMessageBody`);`modifyMsgBody` 本就带 ext 不动。 +2. `loginWithToken` payload 保留 `isPassword: false` key(wrapper 忽略)。 +3. `ChatGroupMemberInfo.role` 直接 int 赋值(镜像 `ChatGroupMember` 既有做法,避免循环引用)。 +4. `fetchGroupMessageReadReceipts` 默认 `pageSize ?? 20`、`cursor ?? ''`(照既有分页惯例)。 +5. `removeMessagesFromServerWithMsgIds/WithTimestamp` 的 `isLoginBefore()` 守卫改为 `isConnected()`(原方法已删)。 +6. `ChatGroup.isDisabled` JSON key 假设为顶层 `isDisabled`。 +7. Android 群 toJson `configs.inviteNeedConfirm` 不下发(`EMGroup` 5.0.0 无该 getter);TS 侧按 optional 容忍。 +8. Android options toJson 补 `dataSyncType`(对称推断);iOS login payload key 仍为 `pwdOrToken`(与 TS 核对一致)。 +9. 聊天室 `onMuteListAdded` 事件仅余 Map 版(`muteKVs`),旧 `mutes`/`expireTime` 负载不再下发(native 行为变化)。 +10. iOS `autoLoginDidCompleteWithError:` wrapper 实现成无触发源死代码(可编译);`onAppActiveNumberReachLimit` 无触发源。 +11. 群成员事件此后只发数组版 `{members:[...]}`(单成员版回调 native 已删)。 +12. `EMConversationFilter (Json)` category(ObjC)无调用方但保留(native 类仍存在,不影响编译)。 + +## 3. 契约空白的裁决记录 + +- **`fetchConversationsByOptions`**:底层 native 接口双端全删且无替代 → 主代理裁决**三层删净**(含 TS 方法、MT 常量、dispatch case、wrapper stub、`ChatConversationFetchOptions` 模型类)。已执行完毕,grep 三层零残留。 +- **`getAllChatRooms`**:仅 Android native 删除、iOS 保留 → 跨端分歧不进契约,Android wrapper 降级为报错 `"getAllChatRooms is not supported by the native SDK."`(MT/dispatch 保留),iOS 正常。**进待决策清单**。 + +## 4. 主代理收尾 + +- `package.json` version 1.20.0→5.0.0;`yarn gen:version_file` 重新生成 `src/version.ts`(VERSION='5.0.0')。 +- example 三处版本:example/package.json 5.0.0、versionName "5.0.0"(versionCode 2→3)、MARKETING_VERSION 5.0.0 ×2(CURRENT_PROJECT_VERSION 2→3)。 +- CHANGELOG.md / CHANGELOG.zh.md 双语 5.0.0 条目(首条 native 依赖升级,含全部 breaking change 说明)。 +- `example/ci/no_login_smoke.json`/`single_account.json`/`port_4_24_1.json`:替换已删 API 引用(→ getAllConversations/getAllContacts/getJoinedGroups/searchMessagesFromServer),移除 init 块中 `autoLogin` key。⚠️ no_login_smoke 中 contact/group 本地读步骤的 `errorCode: 201` 预期为推断值,阶段四跑 `smoke_local.sh` 实证校准。 +- AGENTS.md 滞后条目:HyphenateChat 4.18.1→5.0.0;demo2 引用改为 API tester 现状描述。 + +## 5. 静态检查(主代理亲自执行,2026-09-15) + +| 检查项 | 结果 | +| --- | --- | +| `yarn typecheck` | ✅ 0 error | +| `yarn lint` | ✅ 通过 | +| `yarn check:circular:dpdm` | ✅ no circular dependency | +| `yarn test --no-watchman` | ✅ 19 suites / 110 tests 全过(含 contract:TS⊆Java⊆ObjC、methodMap、supportedEvents 全对齐) | + +阶段三 gate 留痕: + +```text +$ echo '{}' | bash /Users/asterisk/Codes/zuoyu_rn/.agents/skills/platform-sdk-porting-v2/hooks/porting_guard.sh gate react-native /Users/asterisk/Codes/zuoyu_rn/react-native-chat-sdk/.worktree/5.0.0 +(无输出)gate_exit=0 +``` + +## 6. 变更清单状态回填口径 + +- include 70 条:已实现(含编译强制改动),逐条状态在验收报告二维表中呈现。 +- defer 101 条:按契约 §6 原则核对——iOS 群组/联系人/push 同步删除、Android 独有条目等,RN 封装层核对结果逐条列入验收报告(多数为「RN 未封装,无需处理」)。 +- skip 0 条。 + +## 7. 第二轮:审查决策落实(2026-09-16) + +用户审查验收报告后给出裁决(见 acceptance-report.md §3/§5),本轮按裁决落实,三层各一个子代理并行实施: + +**TS 层:** +- `ChatClient.renewAgoraToken` → `renewToken(token)`(TypeDoc 去 Agora 化;native 载荷键 `agora_token` 保留,Java wrapper 仍读该键)。 +- `ChatAreaCode`/`ChatDataSyncType` 独立文件删除,枚举并入 `src/common/ChatOptions.ts`(index.ts 经 `export * from './common/ChatOptions'` 带出)。 +- RN 自身 17 处 `@deprecated` 全部删除(事件 3、方法 12、属性 2、构造入口 1 类),连带删除 Consts.ts 死常量 6 个(`MTmodifyMessage`/`MTgetAllChatRooms`/`MTonMessagesRecalled`/`MTonMessageReadAck`/`MTonMessageDeliveryAck`/`MTonMessageStatusChanged`);`ChatOptions` 公开构造函数私有化(入口只剩 `withAppKey`/`withAppId`);example 两处 `new ChatOptions(...)` 改工厂方法。 +- 行为变化 TypeDoc 注释(决策 a7):`ChatManager.getUnreadCount`(统计收窄口径与替代累加方案)、`ChatManager.getMessage`(不再自动标已读)。 +- `getAllChatRooms`:TS 公开方法本不存在,删除残余死常量 `MTgetAllChatRooms`。 + +**Android + cpp 层:** +- `getAllChatRooms` 删净(ExtSdkMethodType.java / ExtSdkDispatch.java case / ExtSdkChatRoomManagerWrapper.java 方法 / cpp ExtSdkMethodType.h/.cpp)。 +- EMConversationFilter:Android 侧零残留(无 helper 文件)。 +- 聊天室禁言事件:wrapper 已是 Map 版(`muteKVs`),与 native `EMChatRoomChangeListener.onMuteListAdded(String, Map)` 一致,无旧负载残留。 +- **决策 a5 已关闭(用户复审裁决 2026-09-16,未改代码)**:初裁「Android searchMsgFromDB 已作废、迁移 asyncSearchMsgFromDB」前提不成立——native 5.0.0 仅删除 `EMConversation#searchMsgFromDB(..., EMMessageSearchScope)` 一个同步重载(其异步版 wrapper 早已使用);其余 5 个同步调用点对应重载在 5.0.0 均存活且未标 `@Deprecated`,且 `EMChatManager` 不存在任何 `asyncSearchMsgFromDB`、EMConversation 也无对应异步重载——没有可迁移的异步版本。用户复审确认保持现状,不做任何改动。 +- `yarn example build:android` BUILD SUCCESSFUL。 + +**iOS 层:** +- `getAllChatRooms` 删净(key/Value/methodMap/dispatch case/wrapper .h+.m/supportedEvents 清单,共 6 文件)。 +- `EMConversationFilter (Json)` category 删除(ExtSdkToJson.h import+声明、.m 实现,无其他调用方)。 +- `autoLoginDidCompleteWithError:` 删除(native 证据:EMClientDelegate sdk-5.0 已删此方法);`activeNumbersReachLimitation` 无其他触发源一并删除;`userDidForbidByServer` 保留(native EMClientDelegate.h:103 独立 delegate 仍在)。 +- EMConversation toJsonObject 的 name/avatar 加 nil 防护(nil 则不放 key,与 TS optional 语义一致;native EMConversation.h:253/:261 `_Nullable` 佐证)。 +- 全部 11 个 wrapper 的 .h/.m 声明实现程序化比对,零残留。 +- `yarn example build:ios` 成功(SPM 路径)。 + +**主代理裁决(记录待用户确认):** `onAppActiveNumberReachLimit` 事件**保留**——Android 触发路径仍存活(native 错误码 8 `APP_ACTIVE_NUMBER_REACH_LIMITATION` 在 5.0.0 存在,wrapper 由 `onDisconnected(8)` 触发),iOS 无触发源;跨端差异已写 CHANGELOG。 diff --git a/docs/porting/5.0.0/04-verification.md b/docs/porting/5.0.0/04-verification.md new file mode 100644 index 00000000..e69d8042 --- /dev/null +++ b/docs/porting/5.0.0/04-verification.md @@ -0,0 +1,144 @@ +# 5.0.0 平版 · 阶段四验证记录 + +验证日期:2026-09-16。所有命令在 worktree `.worktree/5.0.0` 内执行。结果四档:✅ 通过 / ❌ 不通过(实现缺陷)/ ⚠️ 不通过(环境限制)/ ⏭️ 未执行。 + +## 1. 静态检查(仓库根) + +| 验证项 | 结果 | 失败原因/备注 | +|---|---|---| +| `yarn typecheck` | ✅ | exit 0,无输出 | +| `yarn lint` | ✅ | 初跑 1 个 prettier 格式错误(`example/src/registry/apis/contact.ts` 新增 addContact 条目的 `JSON.stringify` 换行),修复后复跑 exit 0 | +| `yarn check:circular:dpdm` | ✅ | no circular dependency;仅既有 warning(skip react-native/index.js) | +| `yarn test --no-watchman`(unit + contract) | ✅ | 19 suites / 110 tests 全过,含 TS↔Java↔ObjC 三层契约对齐测试 | + +## 2. example 真实应用构建(双端 wrapper 完整编译) + +| 验证项 | 结果 | 失败原因/备注 | +|---|---|---| +| example Android 构建(`yarn example build:android`) | ✅ | BUILD SUCCESSFUL;拉取真实 Maven 产物 `io.hyphenate:hyphenate-chat:5.0.0` AAR 编译通过 | +| example iOS 构建(`yarn example build:ios`) | ✅(走 SPM 路径) | 验证当时 CocoaPods trunk 上 HyphenateChat 最新仅 4.24.1(5.0.0 未发布),改用 SPM 集成路径(`USE_FRAMEWORKS=dynamic bundle exec pod install`,SPM 仓库 HyphenateChat_iOS 有 tag 5.0.0)后 pod install + xcodebuild 均成功。**更新(2026-09-16):用户确认 CocoaPods trunk 与 SPM 远端均已发布 HyphenateChat 5.0.0**,podspec 的 `~> 5.0.0` 声明可直接走默认 CocoaPods 路径 | + +## 3. 无登录冒烟(example 自动化模式,CI 同款驱动) + +脚本:`example/ci/no_login_smoke.json`(init + 7 步,每 manager 一步,断言错误码)。 + +| 验证项 | 结果 | 失败原因/备注 | +|---|---|---| +| Android 冒烟(`bash scripts/ci/smoke_local.sh android`,Pixel_9 模拟器) | ✅ | assert_script: OK(init + 7 steps, all as expected);日志 `build/reports/smoke-android-api_test.log` | +| iOS 冒烟(`bash scripts/ci/smoke_local.sh ios`,模拟器) | ✅ | assert_script: OK(init + 7 steps, all as expected);日志 `build/reports/smoke-ios-api_test.log` | +| contact 步跨端一致性 | ✅ | 初跑发现跨端分歧:`contact.getAllContacts` Android 返回 error 3 / iOS 返回空成功。校准:example registry 新增 `ChatContactManager.addContact` 条目(`example/src/registry/apis/contact.ts`),冒烟脚本 contact 步改为 `addContact`(`{userId:"rn-ci-nonexistent-user",reason:"rn-ci"}`,expect errorCode 201)。复测双端均返回 201("User is not logged in"),其余 6 步双端一致(searchMessagesFromServer/fetchPublicChatRoomsFromServer/publishPresence/fetchSilentModeForAll/fetchSubscribedUsers 均 201,getJoinedGroups 空成功) | + +## 4. 契约要素 grep 抽查(自检清单) + +| 验证项 | 结果 | 失败原因/备注 | +|---|---|---| +| 10 个新 MT 常量四层同步(TS Consts.ts / Java ExtSdkMethodType.java / ObjC .h+.m / cpp ExtSdkMethodType) | ✅ | 逐一 grep 核对,四层齐全 | +| 事件符号名与 iOS `supportedEvents` 注册 | ✅ | 事件符号为 `ExtSdkMethodKeyOn*`(大写 O)形式,均在 `ExtSdkApiObjcRN.mm` supportedEvents 中 | +| 消息四 key(`isPeerRead`/`isRead`/`isNeedReadReceipt`/`groupReadReceiptCount`)、configs 六 key、`msg_ids`/`convId`/`receipt_id`、设备管理 `token` key | ✅ | 三层逐字一致 | +| Android `isUnread()`→`isRead()` 取值取反 | ✅ | wrapper 无遗漏取反逻辑(native 语义从"未读"翻转为"已读",最易错点,已专项核对) | +| `updateGroupConfigs` 逐位掩码映射 | ✅ | Java/ObjC 与契约 §4 一致 | +| 版本号对齐(package.json 5.0.0 / src/version.ts / example 三处 / podspec 两处 / gradle 一处) | ✅ | `yarn gen:version_file` 已跑,version.ts = '5.0.0';example versionCode/CURRENT_PROJECT_VERSION 递增至 3 | +| 双语 CHANGELOG 5.0.0 条目 | ✅ | CHANGELOG.md / CHANGELOG.zh.md 同步,含 native 依赖升级行 | +| 新类型 `src/index.ts` 导出 | ✅ | 由 contract 测试与 typecheck 覆盖 | + +## 5. 登录态功能验证(2026-09-18 更新) + +原 2026-09-16 记录为“缺真实账号、未执行”。2026-09-18 已使用本地 token 环境和 `example/ci/port_5_0_0.json` 完成 Android/iOS 单账号 21 步真实模拟器复验;双账号事件仍未执行。 + +| 验证项 | 结果 | 失败原因/备注 | +|---|---|---| +| Android 登录态 21 步(Android 15,`emulator-5554`) | ❌ 19 步符合预期 / 1 步崩溃 / 1 步未执行 | 正向 17 步及前两个不存在消息错误路径均符合预期;`fetch_group_receipt_missing` 调 native 后 `EMMessage.getChatType()` 空指针崩溃,`kick_all` 未执行。证据:`build/reports/5.0.0/20260918070127-android-emulator-5554/` | +| iOS 登录态 21 步(iOS 18.2,iPhone 16 Pro simulator) | ❌ 20 步符合预期 / 1 步不符合 | 同一 `fetch_group_receipt_missing` 未报错,返回空成功 `{cursor:"", totalCount:0, list:[]}`;无崩溃,其余步骤通过。证据:`build/reports/5.0.0/20260918070518-ios-4BEA133B-4B24-430F-96FC-924632C2CF53/` | +| Android / iOS 结构化对比 | ❌ 1 个行为差异 + 2 个后续状态差异 | 权威对比:`build/reports/5.0.0/comparison-20260918071059.md`;另观察到消息 JSON 的 `body.targetLanguageCodes`、`receiverList` 仅 Android 返回,分页 `totalCount` 仅 iOS 返回(后者为已接受契约) | +| `onDatabaseOpened` / `onDataSyncStart` / `onDataSyncFinish` | ✅ | 两端结构化日志均观察到 | +| `onMessageReadReceipts` / `ChatGroupMemberInfo` | ⏭️ 未执行 | 单账号无法产生另一成员已读回执;需双账号协同场景 | + +## 6. 阶段门禁 + +```text +$ echo '{}' | bash .agents/skills/platform-sdk-porting-v2/hooks/porting_guard.sh gate react-native +gate exit=0(无输出,通过) +``` + +## 7. 结论 + +2026-09-16 阶段四门禁当时通过:静态检查、双端构建、双端冒烟、契约抽查全绿;登录态功能验证当时因缺真实账号标 ⏭️。验证时的唯一环境限制(CocoaPods trunk 未发布 HyphenateChat 5.0.0,iOS 走 SPM 路径完成验证)已于 2026-09-16 解除——用户确认 CocoaPods trunk 与 SPM 远端均已发布 5.0.0,默认 CocoaPods 路径可用。 + +**2026-09-18 更新:阶段四运行时门禁重新打开。** 21 步错误路径复验确认 Android native 崩溃及 iOS 空成功语义差异;在 wrapper 防护与跨端预期落实、双端重验之前,不应把登录态 API 验证标为完成。用户已裁决消息不存在的错误码最终双端统一为 `500 MESSAGE_INVALID`,但实现与崩溃修复本轮暂缓。静态门禁结果不受影响。 + +## 8. 第二轮验证(审查决策落实后,2026-09-16) + +第二轮改动范围:TS 层(renewToken 改名、17 处 deprecated 删除、枚举归位、注释)、Android+cpp(getAllChatRooms 删净)、iOS(getAllChatRooms/EMConversationFilter/autoLoginDidCompleteWithError 删除、会话 name/avatar nil 防护)。 + +| 验证项 | 结果 | 失败原因/备注 | +|---|---|---| +| `yarn typecheck` / `yarn lint` / `check:circular:dpdm` / `yarn test --no-watchman` | ✅ | 全部复跑通过(19 suites / 110 tests,契约测试覆盖删除后三层一致性) | +| example 构建(Android) | ✅ | 改动后 BUILD SUCCESSFUL(子代理留痕) | +| example 构建(iOS) | ✅ | 改动后 BUILD SUCCEEDED(SPM 路径,子代理留痕) | +| 无登录冒烟 Android | ✅ | init + 7 步全过(19 条结构化日志) | +| 无登录冒烟 iOS | ✅ | init + 7 步全过 | +| 门禁复跑 | ✅ | exit=0 无输出 | + +## 9. 第三轮:可追溯自动报告与错误路径复验(2026-09-18) + +- 新增 `yarn report:api --platform [--device ]`,复用 `smoke_local.sh` 与现有设备驱动,不另建构建链路。 +- 每次运行在 Git 忽略的 `build/reports/5.0.0//` 生成 `run.json`、`events.jsonl`、`steps.json`、`summary.md`、`issues.md`、`crash.log`;记录 commit、dirty 状态、脚本/报告器 SHA-256、双端 native 版本、设备与时间,不保存 token/clientSecret。 +- `assert_script.js` 与报告器共用同一结果分类器;`expect.success=false` 和固定 `errorCode` 均可作为通过条件,不再把 `script.done.failed` 的原始失败数误当最终结论。 +- 5.0.0 脚本从 18 步扩为 21 步,增加 `sendMessageReadReceipts`、`getGroupMessageReadReceipts`、`fetchGroupMessageReadReceipts` 三个不存在消息错误路径。 +- example 的 `ChatManager.sendMessage` 注册项改为等待 `onSuccess` 并返回服务端回写后的消息;否则步骤拿到临时 msgId,正向本地查询会误报 `messages is empty`,并可能提前触发 native 崩溃。 +- 报告器单测:`yarn test:ci-scripts`,10/10 通过;`yarn typecheck`、`yarn lint`、`yarn check:circular:dpdm`、全量 Jest 19 suites / 110 tests 通过;porting gate exit 0。 + +## 10. 第四轮:正向/反向用例拆分(2026-09-18) + +- 原 `example/ci/port_5_0_0.json` 拆为 `port_5_0_0_positive.json` 与 `port_5_0_0_negative.json`;`yarn report:api` 默认执行正向脚本,反向脚本通过 `--script example/ci/port_5_0_0_negative.json` 指定。 +- 正向脚本只断言成功返回;反向脚本覆盖不存在资源、非法参数及无效 token。无参数本地查询没有稳定可控的错误输入,不为满足数量机械构造无意义反例。 +- `fetchGroupMessageReadReceipts` 的缺失消息反例已确认会导致 Android native 崩溃,按用户决策在反向脚本中暂时屏蔽;报告器固定生成 `fetch-group-receipt-missing-disabled` 候选项,继续追踪而不阻断其他用例。 +- 双账号场景暂不实施;`onMessageReadReceipts` 和非空 `ChatGroupMemberInfo` 继续保留为后续验证项。 +- 报告目录及文件结构、Android/iOS 对比逻辑保持不变。 +- 脚本与报告器验证:`yarn test:ci-scripts` 12/12、`yarn lint`、`yarn typecheck`、porting gate 均通过。 + +| 验证项 | Android | iOS | 结论 | +| --- | --- | --- | --- | +| 正向 18 步 | 17 passed / 1 failed | 17 passed / 1 failed | 唯一失败均为 `modifyMsgBody` 返回 305(当前集群未开通消息编辑服务);其余正常路径完成,真实 resource 的 `kickDevice` 与后续 `kickAllDevices` 均成功 | +| 反向 10 步 | 8 passed / 2 failed | 7 passed / 3 failed | 无崩溃、全部步骤执行完成;两个批量回执 API 当前均返回 110,与目标 500 不符;iOS 另有 `renewToken("")` 空成功差异 | +| 反向一致项 | 600 / 600 / 500 / 110 / 303 / 303 / 303 | 同 Android | 非法群成员、不存在群、缺失修改消息、空会话 ID、设备管理三方法无效 token 的结果与错误码双端一致 | +| `renewToken("")` | 104 `INVALID_TOKEN` | success | ❌ iOS native 5.0.0 的空 token 分支构造 `EMErrorInvalidToken` 后未立即返回,随后结果被 core 调用覆盖;本轮只记录、不修改 RN wrapper/native 行为 | + +权威本地证据: + +- 正向 Android:`build/reports/5.0.0/20260918085559-android-emulator-5554/` +- 正向 iOS:`build/reports/5.0.0/20260918085608-ios-4BEA133B-4B24-430F-96FC-924632C2CF53/` +- 正向双端对比:`build/reports/5.0.0/comparison-20260918085631.md` +- 反向 Android:`build/reports/5.0.0/20260918085616-android-emulator-5554/` +- 反向 iOS:`build/reports/5.0.0/20260918085623-ios-4BEA133B-4B24-430F-96FC-924632C2CF53/` +- 反向双端对比:`build/reports/5.0.0/comparison-20260918085641.md` + +## 11. 第五轮:项目侧移除多集群支持(2026-09-21) + +### 11.1 用户裁决 + +example 的 `config.local.json` 一度支持多集群(`clusters` / `defaultCluster` / 每条资源的 `cluster` 字段,`env.ts.` 缓存 + `yarn env:use` 激活)。用户裁决:**产品支持多种集群,但项目实现侧不做「多选」**——要换环境就把对应值直接填进 `config.local.json`;便利性由使用者在 Git 外自行维护(推荐自备 `config.ngi.json` / `config.ebs.json` 这类副本,不入库、敏感信息人工管理,用哪套就复制为 `config.local.json`);不为多环境增加项目复杂度。Flutter 侧已先行实施同一裁决(im_flutter_sdk `ece93914`),本轮 RN 对齐。 + +### 11.2 移除清单 + +| 位置 | 移除内容 | +| --- | --- | +| `scripts/env-gettoken.js`(242 → 198 行) | `clusters` 遍历、`defaultCluster` 解析、accounts/groups 按集群过滤、集群级 chatOptions 合并、「跳过并累计失败」逻辑;改为直接写 `example/src/env.ts`,必填字段缺失/占位即 fail fast(退出码 1) | +| `scripts/env-use.js` | 整个删除(激活集群 = 复制 env.ts.,已无存在意义) | +| `package.json` | `env:use` 命令 | +| `.gitignore` | `example/src/env.ts.*`;新增 `example/config.*.json`(用户自管的多环境副本同样不入库) | +| `eslint.config.mjs` | `example/src/env.ts.*` 忽略项 | +| `example/src/auto/auto_mode.ts`、`scripts/ci/fetch_e2e_user_token.js` | 注释中的集群措辞 | + +### 11.3 保留与迁移 + +- **保留私有化部署**:`enablePrivateConfig` + `webSocketServer` / `restServer` / `msyncServer`——它描述「这一个环境是不是私有化」,与选集群无关。 +- **旧格式有一句明确报错**:`config.local.json` 仍带 `clusters` 或 `defaultCluster` 时直接报错退出(退出码 1);`accounts`/`groups` 条目里遗留的 `cluster` 键静默丢弃,避免从旧文件粘贴时被卡住。 +- 本地 `example/config.local.json` 已拍平(ngi 的值提到顶层,原文件备份在仓库外 `/tmp/config.local.json.pre-single-env`),旧的 `example/src/env.ts.ngi` 已删除。 +- CI 无改动:`.github/workflows/` 本就只走 `E2E_*` secrets,与集群机制无关。 + +### 11.4 复验 + +- `node --check scripts/env-gettoken.js` 语法通过;旧格式配置报错路径、缺字段 fail-fast 路径、模板生成路径均已本地实测。 +- 真实链路:`yarn env:gettoken`(ngi 凭据)exit 0,`zuoyu01` / `zuoyu02` 各取到 user token,`example/src/env.ts` 直接生成(无 `env.ts.` 中间文件)。 +- `yarn typecheck` / `yarn lint` / `yarn test --no-watchman` / `yarn test:ci-scripts` 全绿。 diff --git a/docs/porting/5.0.0/05-pushkit.md b/docs/porting/5.0.0/05-pushkit.md new file mode 100644 index 00000000..9b2831c3 --- /dev/null +++ b/docs/porting/5.0.0/05-pushkit.md @@ -0,0 +1,114 @@ +# 5.0.0 补充:iOS PushKit(VoIP 推送)与推送配置收敛 + +范围:`01-api-diff.md` ~ `04-verification.md` 与 `acceptance-report.md` 之外的**遗漏项补齐**。 + +PushKit 是 iOS 原生 SDK 长期存在的公开能力,不在 4.24.1 → 5.0.0 的版本 diff 区间内,历次平版均未覆盖;此外本次一并把 iOS 推送证书名收敛为**初始化配置**,并用 `bindDeviceToken` 取代 `updatePushConfig`。因此单列本文档。 + +## 1. 结论 + +- 5.0.0 分支此前**没有** PushKit 能力:TypeScript、iOS wrapper、Android wrapper 全链路无 `PushKit` / `pushKitCertName`(`git grep -i pushkit` 命中 0)。 +- 本次只在 **5.0.0 分支**补齐 iOS 专用公开 API,并把两个 iOS 证书名改为初始化配置、移除 `pushConfig` / `ChatPushConfig` / `updatePushConfig`。 +- iOS 原生侧能力完整(HyphenateChat 5.0.0 头文件与实现均有),本次不改 native。 +- 参考实现:Flutter 仓库 `im_flutter_sdk` 5.0.0 分支 commit `41d2cf17`(同一批改动)。 + +## 2. 变更清单(native → React Native) + +| id | 类型 | 领域 | iOS native 签名(HyphenateChat 5.0.0) | Android native | RN 决策 | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| `pushkit_cert_name` | existing_option | push | `@property(nonatomic, copy) NSString *pushKitCertName`(`EMOptions.h`) | 无(iOS 专有) | include → `ChatOptions.pushKitCertName`(初始化时下发,运行时不修改) | implemented | +| `apns_cert_name` | existing_option | push | `@property(nonatomic, copy) NSString *apnsCertName`(`EMOptions.h`) | 无(iOS 专有) | include → `ChatOptions.apnsCertName`(初始化时下发);同时移除 iOS wrapper 在绑定时写 `options.apnsCertName` 的旧路径 | implemented | +| `pushkit_register` | existing_api | push | `- (void)registerPushKitToken:(NSData *)aPushToken completion:(void (^)(EMError *))aCompletionBlock` | 无(iOS 专有) | include → `ChatClient.bindPushKitToken({ deviceToken })`;Android 注册同名路由并返回 `EMError.OPERATION_UNSUPPORTED`(TS 非 iOS 不下发) | implemented | +| `pushkit_unregister` | existing_api | push | `- (void)unRegisterPushKitTokenWithCompletion:(void (^)(EMError *))aCompletionBlock` | 无(iOS 专有) | include → `ChatClient.unbindPushKitToken()`;Android 同上 | implemented | +| `pushkit_bind_sync` | existing_api | push | `- (EMError *)bindPushKitToken:(NSData *)aPushToken` | 无(iOS 专有) | skip(同步变体不单独暴露:TS 层本身即 Promise,暴露会产生两个语义完全相同的 API) | skip | +| `pushkit_unbind_sync` | existing_api | push | `- (EMError *)unBindPushKitToken` | 无(iOS 专有) | skip(同上) | skip | +| `device_token_notifier_name` | signature_change | push | `EMPushManager`:绑定 token 只读 `options.apnsCertName` | `EMPushManager.bindDeviceToken(String notifierName, String deviceToken, EMCallBack)`(`notifierName` 非空校验,空则参数非法) | `ChatClient.bindDeviceToken({ deviceToken, notifierName? })`:参数改为可选,iOS 忽略,Android 必填 | implemented | +| `push_config_removed` | removed_option | push | — | — | 移除 `ChatOptions.pushConfig` 与 `ChatPushConfig` 类;iOS `ExtSdkToJson` 的 `EMOptions` 映射、Android `ExtSdkOptionsHelper` 的 `EMPushConfig` 构建分支一并删除 | implemented | +| `update_push_config_removed` | removed_api | push | `registerForRemoteNotificationsWithCertName:deviceToken:completion:`(旧路径) | `EMPushManager.bindDeviceToken` | 移除 `ChatClient.updatePushConfig`,改用 `ChatClient.bindDeviceToken` | implemented | +| `android_vendor_push_init` | behavior_change | push | — | — | 移除 `ChatOptions.pushConfig` 后,Android 厂商推送依赖、manifest 与 `EMPushConfig` 初始化责任转移到原生工程侧 | implemented | +| `ios_renew_token_dispatch` | bug_fix | client | `- (void)renewToken:completion:` | `EMClient.renewToken(token, EMCallBack)` | 补上 iOS `ExtSdkMethodKeyRenewTokenValue` 的 dispatch 分支(此前落到 default,返回 `not implement: renewToken`) | implemented | + +未匹配清单: + +- UM-1:Android 端无 PushKit 能力。用户已确认「仅 iOS 支持,非 iOS 调用静默 no-op」。 +- UM-2:native 4 个 PushKit 方法为语义相同的同步/异步两对。用户已确认「只暴露异步一对」。 +- UM-3:`EMOptions.h` 注释称 `pushKitCertName` / `apnsCertName`「只能在 `initializeSDKWithOptions` 时设置」,而 `registerForRemoteNotificationsWithCertName:`(`EMClient.mm:1780`)会在运行时写入 `apnsCertName`。本次按注释语义实施(仅初始化设置、运行时不修改),并弃用该旧路径,注释修订属 native 侧事项。 +- UM-4:`notifierName` 在两端同名不同义(iOS 曾是证书名、Android 是厂商凭据)。通过「改为可选 + 平台语义注释」收敛,不引入双路径。 +- UM-5:普通推送 token 只有绑定、没有解绑(`bindDeviceToken`),PushKit 绑定/解绑成对。两者是独立能力,不合并入口。 + +## 3. 跨端契约 + +| 契约要素 | 取值 | 落点 | +| --- | --- | --- | +| 初始化选项 key | `apnsCertName`、`pushKitCertName`(顶层,可选) | TS `ChatOptions` / iOS `ExtSdkToJson` 的 `EMOptions (Json)` | +| 方法名 key | `bindDeviceToken` / `bindPushKitToken` / `unbindPushKitToken` | `Consts.ts` / `ExtSdkMethodTypeObjc.h` / `ExtSdkMethodType.java` / `ExtSdkMethodType.h` | +| 路由注册 | iOS wrapper 实现;Android wrapper 注册 PushKit 同名路由并返回 `EMError.OPERATION_UNSUPPORTED`(111) | `ExtSdkDispatch.m` / `ExtSdkDispatch.java` | +| 请求参数 key | `bindDeviceToken`:`deviceToken` + `notifierName`(可选,iOS 忽略,空串照传);`bindPushKitToken`:`deviceToken`(十六进制字符串) | TS 组装 / iOS、Android 读取 | +| 返回结构 | `onResult` 包装为 `{<方法名>: nil}`,TS 只取错误 | 与 `updatePushConfig` 等 object 为 nil 的路由一致 | +| 非 iOS 行为 | TS 侧 `Platform.OS !== 'ios'` 守卫,静默 no-op | `src/ChatClient.ts` | + +## 4. 原生行为依据(决定 API 形态的关键证据) + +证据取自本地 `emclient-ios` 仓库 tag `5.0.0`(`git archive 5.0.0` 解出,与 `HyphenateChat` 5.0.0 发布头文件同源): + +- 绑定时读取证书名:`EMClient.mm:1503`(`pushKitCertName` 为空 → `EMErrorUserIllegalArgument`)、`1515`(绑定);`1560`(`apnsCertName` 为空 → 同样报错)、`1575`(绑定)。 +- 解绑时同样读取该属性:`EMClient.mm:1689`。 +- 登录后自动重绑:`EMClient.mm:1096-1097` 登录成功后自动 `_bindDeviceToken:NO` + `_bindPushKitToken`,失败按退避重试(`1413`/`1435`)。 +- 登出自动解绑:`EMClient.mm:1331` —— `logout(YES)` 已同时调用 `unBindPushKitToken`。 +- token 先入 keychain 再绑定:`EMClient.mm:1649`(写入 `kBindPushKitToken`)、`1508`(绑定时读取)。 +- 旧 APNs 路径会在运行时改写证书名:`EMClient.mm:1780`(`registerForRemoteNotificationsWithCertName:` 内 `self.options.apnsCertName = aCertName`),与 `EMOptions.h:276` 的「仅初始化可设置」注释冲突;`bindFCMToken:`(`1787`)只读取 `options.apnsCertName`,因此证书名必须在初始化时下发。 +- 结论:证书名只要在绑定之前存在于 `options` 上即可生效。原实现的运行期赋值虽然可用,但与 `EMOptions.h` 契约冲突,且留下「证书名可能被后续 bind 调用覆盖」的隐患;本次统一改为初始化下发,绑定链路只读不写。 + +## 5. 实现落点 + +| 层 | 文件 | 改动 | +| --- | --- | --- | +| TS 选项 | `src/common/ChatOptions.ts` | 新增 `apnsCertName` / `pushKitCertName`(iOS 专用注释 + 可选字段):类字段、私有构造函数参数、赋值与两个静态工厂全部打通;删除 `pushConfig` 字段与其类型引用 | +| TS 类型 | `src/common/ChatPushConfig.ts` | 删除 `ChatPushConfig` 类(保留 `ChatPushDisplayStyle`、`ChatPushOption`)及其 `Platform` 依赖 | +| TS API | `src/ChatClient.ts` | 新增 `bindDeviceToken({ deviceToken, notifierName? })`;`bindPushKitToken` 去掉 `notifierName` 参数;删除 `updatePushConfig` | +| TS 常量 | `src/__internal__/Consts.ts` | 新增 `MTbindDeviceToken`;删除 `MTupdatePushConfig`;PushKit 两个常量保留 | +| iOS 选项 | `modules/objc/dispatch/ExtSdkToJson.m` | `EMOptions` 的 `fromJsonObject` 读取 `apnsCertName` / `pushKitCertName` 写入 `EMOptions`,`toJsonObject` 回读补上这两个 key;删除 `pushConfig.deviceId` → `apnsCertName` 的旧映射 | +| iOS 常量 | `modules/objc/common/ExtSdkMethodTypeObjc.h/.m` | 客户端段新增 `bindDeviceToken`(value 118)与 PushKit(value 119/120),删除 `updatePushConfig`(key + value + methodMap 条目) | +| iOS 路由 | `modules/objc/dispatch/ExtSdkDispatch.m` | `updatePushConfig` 分支替换为 `bindDeviceToken`;补上此前缺失的 `renewToken` 分支 | +| iOS 实现 | `modules/objc/dispatch/ExtSdkClientWrapper.h/.m`、`modules/objc/rn/ExtSdkApiObjcRN.mm` | `bindDeviceToken` 改为调用 `bindFCMToken:completion:` 且不再写 `options.apnsCertName`;`bindPushKitToken` 不再写 `options.pushKitCertName`;新增两个 PushKit 路由;`supportedEvents` 列表同步增删 | +| Android | `modules/java/.../ExtSdkMethodType.java`、`ExtSdkDispatch.java`、`ExtSdkClientWrapper.java`、`ExtSdkHelper.java` | 常量与路由由 `updatePushConfig` 换为 `bindDeviceToken`;新增两条 PushKit 路由返回 `EMError.OPERATION_UNSUPPORTED`;删除 `ExtSdkOptionsHelper.fromJson` 中读取 `pushConfig` 构建 `EMPushConfig` 的分支与不再使用的 import | +| C++ | `modules/cpp/common/ExtSdkMethodType.h/.cpp` | 新增 `bindDeviceToken`、PushKit 两个常量(与 TS/Java/ObjC 同步) | +| 契约测试 | `src/__tests__/contract/{parsers.ts,methodNames.test.ts}` | 新增「methodMap 中每个非事件 key 必须有 dispatch 分支」检查,防止 `renewToken` 同类漏接回归 | +| example | `example/src/registry/apis/client.ts`、`example/src/auto/auto_mode.ts` | API 测试页新增 `ChatClient.bindDeviceToken`、修正 PushKit 条目参数;`CHAT_OPTIONS_KEYS` 用 `apnsCertName`/`pushKitCertName` 替换 `pushConfig` | + +实现说明: + +- **token 传参**:TS 传十六进制字符串,iOS 侧沿用既有 APNs 路径(`bindFCMToken:`)的方式把字符串交给原生 `_extractTokenFromRawData:`(`EMClient.mm:1360`,同时兼容 `NSData` 与 `NSString`,后者会去掉 `<>` 与空格)。未在 wrapper 内做 hex → `NSData` 转换,以避免走 `NSData` 分支的固定 8×4 字节读取。 +- **未添加上层判空**:证书名为空由原生返回 `EMErrorUserIllegalArgument`,与既有 `bindDeviceToken` 行为一致,不擅自在上层加过滤。`notifierName` 省略时按空串下发,由原生报参数非法错误。 +- **行为变更**:iOS 上 `bindDeviceToken` 不再影响 `options.apnsCertName`;Android 上厂商推送不再由 SDK 初始化。仅依赖旧写法的应用升级后必须改用 `ChatOptions` 与原生工程配置,否则推送不可用。该变更已写入 CHANGELOG 的 Breaking Changes 与迁移指南。 +- **renewToken 修复**:`ExtSdkDispatch.m` 此前没有 `ExtSdkMethodKeyRenewTokenValue` 分支,iOS 调用 `ChatClient.renewToken` 会返回 `not implement: renewToken`(Android 正常);本次补齐并加入契约测试防护。 + +## 6. 文档更新 + +- `CHANGELOG.md` / `CHANGELOG.zh.md`:Breaking Changes 新增「推送配置」小节(`pushConfig`/`ChatPushConfig`/`updatePushConfig` 移除、证书名改为初始化配置、Android 厂商初始化转移);New Features 新增 `apnsCertName`/`pushKitCertName`、`bindDeviceToken`、PushKit 接口条目。 +- `docs/5.0.0-migration-guide/2026-09-23-rn-4.x-to-5.0.0-migration-guide.md`:新增「推送与设备 Token」整节(4.x 对照表、两处 warning、示例、PushKit 小节);「其他删除的 API」「有替代方式的 API」「主要新增 API」三张表补充条目;「行为变化」新增 13~15 条;迁移检查清单新增推送、iOS 证书名、Android 厂商推送与 VoIP 四项。 +- `docs/push.md`:重写为「初始化配置证书名 + 运行时绑定 token + VoIP 推送」三段式,并说明 Android 厂商推送的工程侧责任。 + +## 7. 验证记录 + +| 验证项 | 命令 | 结果 | +| --- | --- | --- | +| TypeScript 类型检查 | `yarn typecheck` | ✅ 0 错误 | +| example 类型检查 | `npx tsc -p example/tsconfig.json` | ✅ 0 错误 | +| Lint | `yarn lint` | ✅ 通过 | +| 单元 + 契约测试 | `yarn test --no-watchman` | ✅ 19 suites / 117 tests(含新增的 dispatch 分支检查) | +| 循环依赖 | `yarn check:circular:dpdm` | ✅ 无循环依赖 | +| 三端方法名 key 比对 | `grep` TS / ObjC header+methodMap / Java / cpp | ✅ `bindDeviceToken`、`bindPushKitToken`、`unbindPushKitToken` 四处一致;`updatePushConfig` 已无残留 | +| 初始化选项 key 比对 | TS `ChatOptions` ↔ `ExtSdkToJson` | ✅ `apnsCertName` / `pushKitCertName` 两处 key 一致 | +| 运行期赋值残留检查 | `grep -n "options.apnsCertName\s*=\|options.pushKitCertName\s*=" modules/objc` | ✅ 无残留(仅 `ExtSdkToJson` 在初始化时赋值) | +| iOS 语法编译 | `xcrun --sdk iphoneos clang -fsyntax-only`(HyphenateChat 5.0.0 头文件 + RN 头文件 shim) | ✅ `ExtSdkClientWrapper.m`、`ExtSdkDispatch.m`、`ExtSdkMethodTypeObjc.m`、`ExtSdkToJson.m` 0 error / 0 warning;`ExtSdkApiObjcRN.mm` 0 error(1 个既有格式化告警,与本改动无关) | +| Android 编译 | `javac`(`hyphenate-chat-5.0.0.aar` + `android.jar` + androidx annotation)编译 `modules/java/{common,dispatch}` | ✅ 0 error;`javap` 确认 `bindDeviceToken` / `bindPushKitToken` / `unbindPushKitToken` 存在、`updatePushConfig` 已移除 | +| porting gate | `echo '{}' \| bash .agents/skills/platform-sdk-porting-v2/hooks/porting_guard.sh gate react-native ` | ✅ 无输出(无问题) | +| 端到端 VoIP 推送 | 需真机 + PushKit 证书 + 应用侧 `PKPushRegistry` | ⏭️ 当前环境不具备(example 无 `PKPushRegistry`,无法产生真实 PushKit token) | +| 真机推送绑定 | 需真机 + 推送证书 + 厂商推送 token | ⏭️ 未执行(example 无推送 token 来源) | + +## 8. 待用户决策 + +1. **发布版本号**:改动落在 5.0.0 分支,包版本仍为 `5.0.0`。随 5.0.0 发布,还是另开 5.0.1?(影响 CHANGELOG 版本头与代码内 `// 5.0.0` 注释) +2. **`bindPushKitToken` 参数形态**:当前保留对象参数 `{ deviceToken }`(与 Flutter 的具名参数对齐)。若希望改为位置参数 `bindPushKitToken(deviceToken)`,需同步调整 example 与文档。 +3. **native 注释**:是否在 emclient-ios 侧修正 `EMOptions.h` 中 `apnsCertName` / `pushKitCertName` 的「仅初始化可设置」注释与 `registerForRemoteNotificationsWithCertName:` 运行期赋值的差异。 +4. **Android 厂商推送初始化入口**:本次按 Flutter 5.0.0 的做法完全移除 SDK 侧入口,业务需在原生工程配置。若需要保留 JS 侧初始化能力,需要重新设计 `ChatOptions` 字段。 diff --git a/docs/porting/5.0.0/acceptance-report.md b/docs/porting/5.0.0/acceptance-report.md new file mode 100644 index 00000000..624c514b --- /dev/null +++ b/docs/porting/5.0.0/acceptance-report.md @@ -0,0 +1,415 @@ +# 5.0.0 平版 · 验收报告(供人类审查决策) + +## 1. 任务概述 + +- **任务**:环信 React Native SDK 主版本平版——native 依赖 iOS `HyphenateChat` 4.24.1 → 5.0.0、Android `io.hyphenate:hyphenate-chat` 4.24.1 → 5.0.0;RN 自身版本 1.20.0 → **5.0.0**(本次起与 native 版本号同步)。 +- **工作位置**:仓库 `react-native-chat-sdk`,worktree `.worktree/5.0.0`,分支 `5.0.0`(从 dev/1.20.0 切出)。 +- **代码状态**:5.0.0 平版主体及自动报告器已提交;`fetchGroupMessageReadReceipts` 崩溃修复暂缓,当前无对应代码改动。 +- **依据文档**(同目录): + - `01-api-diff.md` —— 双端对照总变更清单 171 条(include 70 / defer 101 / skip 0)+ 未匹配清单 34 条 + 迁移文档交叉验证。本报告不重复粘贴签名全量,逐条引用其 id。 + - `02-contract.md` —— 跨端契约(已冻结),含 §5 删除清单、§6 defer 处理原则、§7 待决策 5 条。 + - `03-implementation.md` —— 实现记录,含 §2 推断项 12 条、§3 契约空白裁决 2 条、§5 静态检查结果。 + +## 2. 二维对照表(171 条全量) + +列说明:iOS 源 / Android 源 = 01-api-diff.md 双端签名列结论(有/无);三个实现列按 03-implementation.md 填写;状态:✅ 一致 / ⚠️ 有疑问(附说明)/ ⏸️ 待决策(defer 项)/ ❌ 缺失。分区按 01-api-diff.md 章节组织,保留原 id。 + +### client(include 16 / defer 10) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| client_register_account | 有 | 有 | 已实现(删 createAccount) | 已实现(删) | 已实现(删) | ✅ | +| client_fetch_token_with_password | 有 | 有 | 已实现(删) | 已实现(删) | 已实现(删) | ✅ | +| client_login_with_password | 有 | 有 | 已实现(删 login,loginWithToken 固定 token) | 已实现(删 isPassword 分支) | 已实现(固定 token 登录) | ⚠️ loginWithToken payload 保留 `isPassword: false`、iOS payload key 仍为 `pwdOrToken`(推断 2/8) | +| client_login_with_agora_token | 有 | 有 | 已实现(删 loginWithAgoraToken) | 已实现(删) | 已实现(删) | ✅ | +| client_service_check | 有 | 有 | 已实现 | 已实现 | 已实现 | ✅ | +| client_check_type | 有 | 有 | 已实现 | 已实现 | 已实现 | ✅ | +| client_auto_login_state_query | 有 | 有 | 已实现(删 isLoginBefore) | 已实现(删) | 已实现(删) | ⚠️ 原 `isLoginBefore()` 守卫改为 `isConnected()`(推断 5) | +| client_auto_login_behavior | 有(行为) | 有 | 无动作(RN 无自动登录路径) | 无动作 | 无动作 | ✅ 行为变化已写 CHANGELOG | +| client_statistics_manager | 有 | 有 | 无封装(核对关闭) | 无封装 | 无封装 | ✅ | +| statistics_module | 有 | 有 | 无封装(核对关闭) | 无封装 | 无封装 | ✅ | +| client_get_logged_in_devices_with_password | 有 | 有 | 已实现(改 token 鉴权) | 已实现(编译强制改异步 `fetchLoggedInDevicesFromServerWithToken`) | 已实现(token 版) | ✅ | +| client_kick_device_with_password | 有 | 有 | 已实现(改 token 鉴权) | 已实现(token 版) | 已实现(token 版) | ✅ | +| client_kick_all_devices_with_password | 有 | 有 | 已实现(改 token 鉴权) | 已实现(token 版) | 已实现(token 版) | ✅ | +| client_delegate_sync_data_start | 有 | 有 | 已实现(新增 onDataSyncStart) | 已实现 | 已实现 | ✅ | +| client_delegate_sync_data_finished | 有 | 有 | 已实现(新增 onDataSyncFinish) | 已实现 | 已实现 | ✅ | +| client_delegate_on_database_opened | 有 | 有 | 已实现(新增 onDatabaseOpened) | 已实现(errorCode 恒 0) | 已实现 | ✅ | +| client_login_with_username_token_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| client_get_device_config | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| client_delegate_auto_login_did_complete | 有 | 无 | 未实施 | 未实施 | 已删除(用户裁决:契约标作废保留条目;实现层删除 delegate 死代码,native 5.0.0 已无此方法) | ✅ | +| client_delegate_user_account_did_login_from_other_device | 有 | 无 | 已实现(MTonUserDidLoginFromOtherDevice 三层删) | 已实现 | 已实现 | ✅ 该常量本已 deprecated 豁免,本次删净 | +| client_renew_token_no_callback_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ RN 仅用异步 renewToken | +| client_get_logged_in_devices_token_sync_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| client_fetch_logged_in_devices_with_token_new | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| client_is_database_opened_new | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| client_get_device_info_new | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ 疑似内部方法暴露,建议不平版 | +| client_version_bump | 无 | 有 | 不涉及 | 未实施 | 未实施 | ⏸️ RN 自身版本号已另行 bump 5.0.0 | + +### options(include 5 / defer 4) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| options_data_sync_type_enum | 有 | 有 | 已实现(新增 ChatDataSyncType) | 已实现(fromNativeMask) | 已实现(直传掩码) | ⚠️ 双端默认值不一致(iOS Conversations / Android NONE),RN 不设默认(待决策 5) | +| options_data_sync_type | 有 | 有 | 已实现(ChatOptions.dataSyncType) | 已实现 | 已实现 | ⚠️ Android options toJson 补 `dataSyncType` 为对称推断(推断 8) | +| options_is_auto_login | 有 | 有 | 已实现(删 autoLogin) | 已实现(删 key) | 已实现(删 key) | ✅ | +| options_enable_require_read_ack | 有 | 有 | 已实现(删 requireAck) | 已实现(删 key) | 已实现(删 key) | ✅ | +| options_enable_auto_sync_contacts | 有 | 有 | 已实现(删 enableAutoSyncContacts) | 已实现(删 key) | 已实现(删 key) | ✅ | +| options_report_server_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| options_area_code_int_removed | 无 | 有 | 未实施(TS 接口不变) | 已实现(int→AreaCode 枚举映射,含 GLOB(-1) 兜底) | 不涉及 | ⚠️ 编译强制:Android `setAreaCode(int)` 已删,wrapper 必须改映射(a8) | +| options_area_code_enum_new | 无 | 有 | 未实施 | 已实现(同上枚举映射) | 不涉及 | ⚠️ 同上,随上一条一并处理 | +| options_set_area_code_param_changed | 无 | 有 | 未实施 | 已实现(同上) | 不涉及 | ⚠️ 同上 | + +### multidevice(include 2 / defer 0;01 领域列计入 client) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| multidevice_event_conversation_unread_message_count_cleared | 有 | 有 | 已实现(枚举加 65) | 不涉及(int 透传) | 不涉及(int 透传) | ✅ | +| multidevice_event_all_conversation_unread_message_count_cleared | 有 | 有 | 已实现(枚举加 66) | 不涉及(int 透传) | 不涉及(int 透传) | ✅ | + +### chat(含已读回执体系;include 20 / defer 15) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| chat_fetch_conversations_from_server | 有 | 有 | 已实现(删 fetchAllConversations 等) | 已实现(删) | 已实现(删) | ✅ | +| chat_get_pinned_conversations_from_server | 有 | 有 | 已实现(删) | 已实现(删) | 已实现(删) | ✅ | +| chat_get_conversations_with_cursor_filter | 有 | 有 | 已实现(fetchConversationsByOptions 三层删净) | 已实现(删 stub) | 已实现(删) | ⚠️ 契约空白裁决:底层双端全删无替代,含 `ChatConversationFetchOptions` 模型一并删净(03 §3) | +| chat_modify_message | 有 | 有 | 已实现(modifyMessageBody 加 `ext?`) | 已实现(四参版) | 已实现(四参版) | ⚠️ 契约写 `modifyMessage`,实际承载方法是 `modifyMessageBody`(推断 1) | +| chat_send_message_read_ack | 有 | 有 | 已实现(删) | 已实现(删) | 已实现(删) | ✅ | +| chat_send_group_message_read_ack | 有 | 有 | 已实现(删) | 已实现(删) | 已实现(删) | ✅ | +| chat_send_message_read_receipts | 有 | 有 | 已实现(新增,约束 ≤50 条/同会话写进 TypeDoc) | 已实现(msg_ids 逐个查库后调批量接口) | 已实现 | ✅ | +| chat_ack_conversation_read | 有 | 有 | 已实现(删) | 已实现(删) | 已实现(删) | ✅ | +| chat_clear_conversation_unread_message_count | 有 | 有 | 已实现(新增) | 已实现 | 已实现 | ✅ | +| chat_clear_all_conversation_unread_message_count | 有 | 有 | 已实现(新增) | 已实现 | 已实现 | ✅ | +| chat_get_group_message_read_receipts | 有 | 有 | 已实现(新增,≤20 条/同会话) | 已实现 | 已实现 | ✅ | +| chat_fetch_history_messages | 有 | 有 | 已实现(删 MTfetchHistoryMessages;ByOptions 版保留) | 已实现(删) | 已实现(删) | ✅ | +| chat_fetch_group_message_read_receipts | 有 | 有 | 已实现(新增,替代 fetchGroupAcks) | 已实现 | 已实现(返回含 totalCount) | ⚠️ `totalCount` 仅 iOS 返回、`groupId` Android 忽略(待决策 3);默认 `pageSize ?? 20`、`cursor ?? ''` 为推断(推断 4) | +| chat_report_message | 有 | 有 | 已实现(删 reportMessage) | 已实现(删) | 已实现(删) | ✅ | +| chat_mark_all_conversations_as_read | 有 | 有 | 已实现(删) | 已实现(删) | 已实现(删) | ✅ | +| chat_delegate_messages_did_read | 有 | 有 | 已实现(删 onMessagesRead) | 已实现(删) | 已实现(删) | ✅ | +| chat_delegate_group_message_did_read | 有 | 有 | 已实现(删 onGroupMessageRead) | 已实现(删) | 已实现(删) | ✅ | +| chat_delegate_group_message_ack_has_changed | 有 | 有 | 已实现(删 MTonReadAckForGroupMessageUpdated) | 已实现(删) | 已实现(删) | ⚠️ 迁移文档误称改名为 `onReadReceiptForGroupMessageUpdated`(5.0.0 源码不存在),按源码直接删除,群已读更新由 onMessageReadReceipts 承载(矛盾 1) | +| chat_delegate_on_conversation_read | 有 | 有 | 已实现(删 onConversationRead) | 已实现(删) | 已实现(删) | ✅ | +| chat_delegate_on_message_read_receipts | 有 | 有 | 已实现(新增 onMessageReadReceipts) | 已实现 | 已实现(进 supportedEvents) | ✅ | +| chat_get_unread_message_count | 有 | 无 | 已有封装(无动作) | 无动作 | 无动作 | ⏸️ 行为变化(统计范围收窄)已写 CHANGELOG | +| chat_import_conversations | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_resend_message | 有 | 无 | 未实施 | 不涉及 | 已实现(resendMessage→sendMessage,编译强制) | ⚠️ defer 项被编译强制触及,已记录 | +| chat_load_messages_with_type | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_load_messages_with_keyword_legacy | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_load_messages_with_keyword_scope | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_delete_messages_before | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_delegate_conversation_list_did_update | 有 | 无 | 无动作(事件层无感知) | 不涉及 | 已实现(迁 EMConversationDelegate,注册改 addConversationDelegate:queue:) | ⚠️ defer 项被编译强制触及:iOS 回调迁移新协议,RN 事件不变 | +| chat_add_conversation_delegate | 有 | 无 | 未实施 | 未实施 | 已实现(复用于上条注册) | ⏸️ delegate 参数 `_Nullable` 标注疑点保留(i3) | +| conversation_delegate_protocol | 有 | 无 | 未实施 | 不涉及 | 已实现(同上迁移) | ⏸️ 同上 | +| chat_load_all_conversations_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_update_participant_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| chat_async_fetch_history_messages_new | 无 | 有 | 未实施(RN 已有等价 ByOptions 版) | 未实施 | 未实施 | ⏸️ | +| chat_async_delete_conversations_new | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| msg_listener_on_message_recalled_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | + +### message / conversation 模型(include 8 / defer 10) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| message_is_peer_read | 有 | 有 | 已实现(hasReadAck→isPeerRead) | 已实现(只读) | 已实现(只读) | ✅ | +| message_is_read | 有 | 有 | 已实现(hasRead→isRead) | 已实现(**直接映射不取反**,旧 `!isUnread()` 已删) | 已实现 | ✅ Android 语义反转点(N2)已按契约处理,用户复核确认无误 | +| message_is_need_read_receipt | 有 | 有 | 已实现(needGroupAck→isNeedReadReceipt) | 已实现(唯一保留 setter) | 已实现 | ✅ | +| message_group_read_receipt_count | 有 | 有 | 已实现(groupAckCount→groupReadReceiptCount) | 已实现(readReceiptCount()) | 已实现 | ✅ | +| conversation_name_avatar | 有 | 有 | 已实现(新增 displayName?/displayAvatar?,避开既有 name() 方法) | 已实现(toJson 加 name/avatar) | 已实现(toJson 加 name/avatar) | ✅ 命名映射已记录 | +| conversation_mark_message_as_read | 有 | 有 | 已实现(删 markMessageAsRead/markAllMessagesAsRead) | 已实现(删) | 已实现(删) | ✅ | +| group_read_receipt_type | 有 | 有 | 已实现(ChatGroupMessageAck→ChatGroupReadReceipt,from 变对象、删 content) | 已实现(ExtSdkGroupAckHelper 改写) | 已实现 | ⚠️ `ChatGroupMemberInfo.role` 直接 int 赋值(推断 3) | +| message_read_receipt_type | 有 | 有 | 已实现(新增 ChatMessageReadReceipt) | 已实现(新增 Helper) | 已实现(新增 Json category) | ✅ | +| message_get_reaction | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| file_message_body_init_with_data | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| file_message_body_init_with_local_path | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| image_message_body_init_with_data | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| stream_chunk_sequence_number | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| conversation_get_message_param_changed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ getMessage 不再自动标已读(行为变化 4) | +| conversation_search_msg_scope_sync_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| message_create_txt_send_message_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| message_get_user_name_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| message_get_recaller_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | + +### group(include 12 / defer 45) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| group_options_type | 有 | 有 | 已实现(删 ChatGroupOptions) | 已实现(Helper 改 Configs) | 已实现 | ✅ | +| group_style_enum | 有 | 有 | 已实现(删 ChatGroupStyle) | 已实现 | 已实现(toJson 删 style key) | ✅ | +| group_configs_type | 有 | 有 | 已实现(新增 ChatGroupConfigs;ChatGroup.options→configs,isDisabled 提升顶层) | 已实现(六 key) | 已实现(读 EMGroup.settings) | ⚠️ `isDisabled` JSON key 假设为顶层(推断 6);Android `configs.inviteNeedConfirm` 无 getter 不下发(推断 7) | +| group_configs_type_enum | 有 | 有 | 已实现(位序 1/2/4/8/16/32 以 iOS 为准绳) | 已实现(**逐位掩码映射**,未直接 toNativeMask) | 已实现(掩码直传) | ✅ 双端位序不同已处理 | +| group_create_group | 有 | 有 | 已实现(删旧 createGroup) | 已实现(configs 版) | 已实现(configs 版) | ✅ | +| group_create_group_with_avatar | 有 | 有 | 已实现(createGroupEx 参数 options→configs) | 已实现 | 已实现 | ✅ | +| group_update_group_configs | 有 | 有 | 已实现(新增 updateGroupConfigs) | 已实现 | 已实现 | ✅ | +| group_get_public_groups_from_server | 有 | 有 | 已实现(删 fetchPublicGroupsFromServer) | 已实现(删) | 已实现(删) | ✅ | +| group_get_joined_groups_from_server | 有 | 有 | 已实现(删 fetchJoinedGroupsFromServer) | 已实现(删;另删 loadAllGroups 调用) | 已实现(删) | ✅ | +| group_delegate_join_request_declined | 有 | 有 | 已实现(onRequestToJoinDeclined 负载加 applicant) | 已实现(删旧回调) | 已实现(删旧回调) | ✅ | +| group_delegate_user_did_join_group | 有 | 有 | 无动作(自 4.15 用多成员版) | 已实现(清残留注册) | 已实现(删单成员版回调) | ✅ | +| group_delegate_user_did_leave_group | 有 | 有 | 无动作(同上) | 已实现(清残留注册) | 已实现(删单成员版回调) | ✅ | +| group_settings_property | 有 | 无 | 已实现(随 group_configs_type 一并改 configs) | 已实现 | 已实现(读 EMGroup.settings) | ✅ 虽 defer 但实现期已被 configs 重构覆盖 | +| group_is_push_notification_enabled | 有 | 无 | 未实施 | 未实施 | 已实现(toJson 删 noticeEnable key,编译强制) | ⚠️ defer 项被编译强制触及 | +| group_search_public_group | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_groups_without_push_notification | 有 | 无 | 未实施 | 未实施 | 已实现(删 wrapper,编译强制) | ⚠️ defer 项被编译强制触及 | +| group_get_group_specification_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_group_member_list_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_group_blacklist_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_group_mute_list_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_group_file_list_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_group_white_list_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_is_member_in_white_list_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_get_group_announcement_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_add_occupants_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_remove_occupants_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_block_occupants_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_unblock_occupants_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_change_group_subject_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_change_description_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_leave_group_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_destroy_group_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_block_group_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_unblock_group_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_update_group_owner_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_add_admin_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_remove_admin_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_mute_members_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_unmute_members_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_mute_all_members_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_unmute_all_members_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_add_white_list_members_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_remove_white_list_members_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_remove_group_shared_file_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_update_group_announcement_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_update_group_ext_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_join_public_group_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_apply_join_public_group | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ 新名 4.24.1 已有,RN 无改动需求 | +| group_accept_join_application | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ 同上 | +| group_decline_join_application | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ 同上 | +| group_accept_invitation_from_group_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_decline_invitation_from_group | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ 新名 4.24.1 已有 | +| group_is_member_only_renamed | 无 | 有 | 未实施 | 已实现(toJson 删 isMemberOnly key,编译强制) | 已实现(同上) | ⚠️ defer 项被编译强制触及 | +| group_get_users_new | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ iOS `users` 4.24.1 已有(N1);RN 若已封装则双端均无需动 | +| group_get_from_server_fetch_members_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| group_load_all_groups_removed | 无 | 有 | 未实施 | 已实现(getJoinedGroups 删 loadAllGroups() 调用,编译强制:5.0 已包私有) | 不涉及 | ⚠️ defer 项被编译强制触及 | +| group_async_upload_shared_file_overload_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | + +### contact(include 5 / defer 8) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| contact_get_all_contacts_from_server | 有 | 有 | 已实现(删 getAllContactsFromServer) | 已实现(删) | 已实现(删) | ✅ 双端命名不对称,已按语义配对(N7) | +| contact_get_contacts_from_server_with_cursor | 有 | 有 | 已实现(删 fetchContacts) | 已实现(删) | 已实现(删) | ✅ | +| contact_get_contacts_string_list_from_server | 有 | 有 | 已实现(删 fetchAllContacts) | 已实现(删) | 已实现(删) | ✅ | +| contact_delegate_on_friend_sync_start | 有 | 有 | 已实现(删 onContactSyncStart) | 已实现(停发) | 已实现(停发) | ✅ 替代路径 = 连接级 onDataSyncStart/Finish | +| contact_delegate_on_friend_sync_finished | 有 | 有 | 已实现(删 onContactSyncFinish) | 已实现(停发) | 已实现(停发) | ✅ | +| contact_add_contact_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_get_blacklist_from_server_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_add_user_to_blacklist_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_remove_user_from_blacklist_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_accept_invitation_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_decline_invitation_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_get_self_ids_on_other_platform_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| contact_save_black_list | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | + +### room(include 2 / defer 5) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| room_create_chatroom | 有 | 有 | 已实现(删 createChatRoom) | 已实现(删) | 已实现(删) | ✅ | +| room_destroy_chatroom | 有 | 有 | 已实现(删 destroyChatRoom) | 已实现(删) | 已实现(删) | ✅ | +| room_get_all_chat_rooms_removed | 无 | 有 | 已删除(残余 MT 常量清理;TS 公开方法本不存在) | 已删除 | 已删除 | ✅ 用户裁决:三端删除不保留 | +| room_fetch_from_server_fetch_members_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ | +| room_remove_chat_room_listener_removed | 无 | 有 | 不涉及 | 已实现(改调 removeChatRoomChangeListener,编译强制) | 不涉及 | ⚠️ defer 项被编译强制触及 | +| room_listener_on_member_joined_2arg_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ RN 已用三参版 | +| room_listener_on_mute_list_added_removed | 无 | 有 | 未实施 | 未实施 | 未实施 | ⏸️ 注意:onMuteListAdded 仅余 Map 版(muteKVs),旧 mutes/expireTime 负载不再下发(推断 9) | + +### push(include 0 / defer 3) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| push_update_push_display_style_sync | 有 | 无 | 未实施 | 不涉及 | 已实现(updateImPushStyle 改 completion 版,编译强制) | ⚠️ defer 项被编译强制触及 | +| push_update_push_display_name_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | +| push_get_push_options_from_server_sync | 有 | 无 | 未实施 | 未实施 | 未实施 | ⏸️ | + +### error code(include 0 / defer 1) + +| 变更项 | iOS 源 | Android 源 | TypeScript | Android Wrapper | iOS Wrapper | 状态 | +| --- | --- | --- | --- | --- | --- | --- | +| error_contact_add_faild_typo | 有 | 无 | 无动作(RN 按数值 code 映射) | 无动作 | 无动作 | ⏸️ 值 1000 不变,grep 核实后关闭 | + +**统计**:✅ 61 / ⚠️ 23 / ⏸️ 87 / ❌ 0,合计 171。按决策拆分:include 70 = ✅59 + ⚠️11;defer 101 = ⏸️87 + ⚠️12 + ✅2(两条 defer 因实现期被覆盖而关闭:`client_delegate_user_account_did_login_from_other_device` 本已 deprecated 本次三层删净、`group_settings_property` 随 configs 重构一并实现)。 + +## 3. 未匹配 / 疑点清单(34 条全量,压缩表述) + +来自 iOS 清单(i1-i17): + +| 编号 | 现象(压缩) | 处理 / 建议 | +| --- | --- | --- | +| i1 | `EMGroup.users` 非 5.0.0 新增,iOS 迁移文档误列 | **已关闭(用户裁决 2026-09-16)**:忽略,用户将上报 native;RN 不动 | +| i2 | `sendMessageReadReceipts` 注释与签名不符(注释多出 aConversationId/aResult) | **已关闭**:以签名为准,RN 回调只暴露整体 error | +| i3 | `addConversationDelegate` delegate 参数 `_Nullable` 疑标注疏漏 | **已关闭(用户裁决)**:忽略,用户将上报 iOS;RN 按 `_Nonnull` 语义处理 | +| i4 | 大量「5.0.0 替代 API」实际 4.24.1 已存在(20+ 个) | 已关闭:结论性说明,RN 工作以删旧封装为主 | +| i5 | `joinGroupRequestDidReceive` 仅位置移动 | 已关闭:无 API 变化 | +| i6 | `getBlackList` 仅格式调整 | 已关闭:无 API 变化 | +| i7 | 7 个 completion 方法仅位置移动/标注微调 | 已关闭:无 API 变化 | +| i8 | `EMErrorContactAddFaild→Failed` 拼写修正文档未提 | **已关闭(用户裁决)**:RN 按数值映射无需修改;Flutter 若未用数值,5.0.0 版本调整为数值(Flutter 侧事项,非本任务) | +| i9 | iOS 同步 `renewToken:` 5.0.0 保留,与文档一致 | **已关闭(用户裁决)**:已了解。后续只有 token 登录;example 登录改造与 token 获取脚本经用户复审裁决(2026-09-16)**本次不需要处理** | +| i10 | 本地读取替代接口(getAllConversations 等)均未变 | 已关闭:作为 RN 迁移目标接口确认 | +| i11 | `EMClientDelegate.h` import 改 EMOptions.h | 已关闭:对 RN 无直接影响 | +| i12 | `EM_DEPRECATED_IOS` 宏改为忽略参数 | 已关闭:内部工程调整 | +| i13 | 聊天室仅删创建/解散,迁移文档一致 | **已关闭(用户裁决)**:已了解——SDK 不再有创建/解散聊天室能力,今后只能通过 REST API 操作。RN 侧 `createChatRoom`/`destroyChatRoom` 已删 | +| i14 | `EMGroupManagerListener.h` 删旧 delegate 转发 | 已关闭:无额外公开 API | +| i15 | `conversationName`/`conversationAvatar` 是实例方法而非属性 | **用户裁决**:name/avatar 可以为空,native iOS 的写法不对、Android 根本没有写(native 侧问题用户知悉)。RN 行动:TS 侧 `displayName`/`displayAvatar` 已为 optional;iOS wrapper toJson 加 nil 防护(本轮实施) | +| i16 | 4.24.2 侧分支 hotfix 不在 5.0.0 主线 | 保留提示:若 RN 曾基于 4.24.2 平版需注意该 hotfix 在 5.0.0 不存在 | +| i17 | `EMChatMessage+Private.h` +2 行(内部头) | 保留提示:RN iOS 原生层若直接引用 Private 头需另行核对 | + +来自 Android 清单(a1-a10): + +| 编号 | 现象(压缩) | 处理 / 建议 | +| --- | --- | --- | +| a1 | `onReadReceiptForGroupMessageUpdated` 源码不存在,文档误称改名 | **已关闭**:以源码为准不平版该回调,群已读更新由 onMessageReadReceipts 承载 | +| a2 | 文档称删 4 个 `asyncFetchConversationsFromServer` 重载,实际 3 个 | 已关闭:以源码为准按 3 个收录 | +| a3 | `EMClient#getDeviceInfo()` 疑似内部 API 暴露 | **已关闭(用户裁决)**:不平版 | +| a4 | `toNativeMask`/`fromNativeMask` 为 public 但语义属内部 JNI 工具 | **已关闭(用户裁决)**:不平版 | +| a5 | `searchMsgFromDB(..., searchScope)` 同步重载删除文档未指明 | **已关闭(用户复审裁决 2026-09-16)**:初裁「迁移 asyncSearchMsgFromDB」前提经 native 源码核实不成立——5 个同步调用点对应重载在 5.0.0 均存活且未标废弃,`EMChatManager`/`EMConversation` 均无 `asyncSearchMsgFromDB` 可迁移——保持现状不改代码(详见 03-implementation.md 决策落实记录) | +| a6 | createGroup 无 avatar 重载删除文档未明确 | **已关闭(用户裁决)**:原描述有误——三端实现均使用 `groupAvatar` 字段,无 `avatar` 字段问题 | +| a7 | 行为变化 5 项(未读数收窄、getMessage 不标已读、保活移除、推送 Token 判断、前后台检测) | **用户裁决**:需要在注释里说明 → 本轮在对应 TS API 的 TypeDoc 中补行为变化注释(未读数收窄、getMessage 不再自动标已读;后三项对 RN 封装无 API 影响) | +| a8 | `AreaCode` 旧 int 常量 7 个全删,文档只提 setAreaCode 签名变化 | **已关闭(用户裁决)**:修改已确认无误(Android wrapper 枚举映射含 GLOB(-1) 兜底) | +| a9 | `EMLoginExtensionInfo` 未变化 | 已关闭:无变化确认 | +| a10 | 根包其余文件 diff 为空;presence/thread/userinfo/push 无签名变化 | 已关闭:无变化确认 | + +配对新发现跨端不对齐(N1-N7): + +| 编号 | 现象(压缩) | 处理 / 建议 | +| --- | --- | --- | +| N1 | 群成员合并列表接口双端引入版本不同(iOS 4.24.1 已有 / Android 5.0.0 新增) | **已关闭(用户裁决)**:Android 没有 `users`,该字段不添加 | +| N2 | `isRead` 双端变化不对等(Android 改名且取值取反) | **已关闭(用户复审裁决 2026-09-16)**:Android 改名后为 `isRead()`(另有 `isPeerRead()`),RN 当前实现直接映射不取反,用户确认无误 | +| N3 | iOS 批量删群组同步方法 36 条,Android 对应全部保留 | ⏸️ 全部 defer,RN 只用异步封装,建议批量确认「无需处理」 | +| N4 | iOS 删 push 同步方法 3 条,Android push 零变化 | ⏸️ 全部 defer;其中 updateImPushStyle 已被编译强制改 completion 版 | +| N5 | 输入文件统计口径不一致(自称 105/82,实际 150/107 行) | 已关闭:统计一律按合并后实际条目(171) | +| N6 | `getUnreadMessageCount` iOS 新增 / Android 仅行为变化 | **已关闭**:RN 已有封装无动作,行为变化已写 CHANGELOG | +| N7 | contact 服务端拉取接口双端命名不对称 | **已关闭**:按返回类型语义配对,contact 三条 include 已实现 | + +## 4. 问题清单 + +### 4.1 推断项(03 §2,12 条) + +1. `modifyMessageBody` 加 `ext`(契约写的方法名是 `modifyMessage`);`modifyMsgBody` 本就带 ext 不动。 +2. `loginWithToken` payload 保留 `isPassword: false` key(wrapper 忽略)。 +3. `ChatGroupMemberInfo.role` 直接 int 赋值(镜像 `ChatGroupMember` 既有做法,避免循环引用)。 +4. `fetchGroupMessageReadReceipts` 默认 `pageSize ?? 20`、`cursor ?? ''`(照既有分页惯例)。 +5. `removeMessagesFromServerWithMsgIds/WithTimestamp` 的 `isLoginBefore()` 守卫改为 `isConnected()`。 +6. `ChatGroup.isDisabled` JSON key 假设为顶层 `isDisabled`。 +7. Android 群 toJson `configs.inviteNeedConfirm` 不下发(`EMGroup` 5.0.0 无该 getter);TS 侧按 optional 容忍。**用户补充:该字段原在 `EMGroupOptions`,5.0.0 改名为 `EMGroupConfigs`——与 RN 侧 configs 重构一致,无追加动作。** +8. Android options toJson 补 `dataSyncType`(对称推断);iOS login payload key 仍为 `pwdOrToken`。 +9. 聊天室 `onMuteListAdded` 仅余 Map 版(`muteKVs`),旧 `mutes`/`expireTime` 负载不再下发(native 行为变化)。**更新(2026-09-16):用户在 iOS wrapper 手动删除了旧版 `chatroomMuteListDidUpdate:addedMutedMembers:muteExpire:` 回调实现;Android wrapper 确认本就只发 Map 版。** +10. iOS `autoLoginDidCompleteWithError:` wrapper 实现成无触发源死代码(可编译);`onAppActiveNumberReachLimit` 无触发源。**更新(用户裁决):iOS delegate 已删(契约条目标作废保留);`onAppActiveNumberReachLimit` 经复查 Android 仍有触发源(`onDisconnected(8)`,native 错误码 8 存活),事件保留,仅 iOS 失效,已写 CHANGELOG。** +11. 群成员事件此后只发数组版 `{members:[...]}`(单成员版回调 native 已删)。**更新(2026-09-16 复查):native 双端 5.0.0 群组/聊天室成员回调均只保留数组版(iOS `EMGroupManagerDelegate`/`EMChatroomManagerDelegate` 单成员版已删或废弃,Android `EMGroupChangeListener`/`EMChatRoomChangeListener` 同样);RN 旧版单成员事件 `onMemberJoined`/`onMemberExited`/聊天室旧 `onMuteListAdded` 本轮已随 deprecated 清理删除,dispatch 中旧 `type` 分支同步移除。** +12. ~~`EMConversationFilter (Json)` category(ObjC)无调用方但保留~~ → **已删除(用户裁决)**:ExtSdkToJson.h/.m 的 category 与 import 一并移除,无调用方。 + +### 4.2 契约空白裁决(03 §3,2 条) + +- **`fetchConversationsByOptions`**:底层 native 双端全删且无替代 → 裁决三层删净(TS 方法、MT 常量、dispatch case、wrapper stub、`ChatConversationFetchOptions` 模型类),grep 三层零残留。**更新(用户裁决 2026-09-16):残余 `EMConversationFilter` 相关内容也不需要——iOS `EMConversationFilter (Json)` category(ExtSdkToJson.h/.m)已删除;Android 侧核查零残留。** +- **`getAllChatRooms`**:~~仅 Android native 删除、iOS 保留 → Android wrapper 降级为报错~~ → **用户裁决(2026-09-16):三端删除不保留**。已实施:TS 残余 MT 常量清理(公开方法本不存在),Java wrapper/dispatch/常量、objc key/Value/methodMap/dispatch/wrapper/supportedEvents、cpp 常量全部删净。 + +### 4.3 实现期编译强制改动要点(超出 defer「预期无动作」范围,必须改通) + +- Android 3 处:`getLoggedInDevicesFromServer` 改异步 `fetchLoggedInDevicesFromServerWithToken`;`getJoinedGroups` 删 `loadAllGroups()` 调用(5.0 已包私有);`removeChatRoomListener`→`removeChatRoomChangeListener`。另有 `setAreaCode(int)` 删除 → 改 `AreaCode` 枚举映射(含 GLOB(-1) 兜底)。 +- iOS 5 处:mergeMessage 只读属性适配;resendMessage→sendMessage;updateImPushStyle 改 completion 版;删 getGroupsWithoutPushNotification wrapper;群 toJson 删 noticeEnable/style/isMemberOnly key。 +- Android wrapper 已用真实 `hyphenate-chat:5.0.0` AAR(Maven Central)离线 javac 编译验证通过,0 error;iOS 侧编译验证已在阶段四完成(example SPM 路径构建成功,见 04-verification.md §2)。 + +### 4.4 native 5.0.0 仍保留但标记废弃的 API 清单(用户要求单独列举) + +来源:native 仓库 5.0.0 分支头文件/源码中的废弃标注(iOS `__deprecated_msg`、Android `@Deprecated`)。这些是**标记作废但尚未删除**的 API(与「已删除」区分);RN 封装策略:已废弃变体一律不平版/已删除,统一走替代 API。 + +**iOS(sdk-5.0,公开 6 条):** + +| native API | 替代 | RN 处理 | +| --- | --- | --- | +| `EMConversation loadMessagesWithKeyword:timestamp:count:fromUser:searchDirection:scope:completion:`(单 `fromUser` 版) | `fromUsers`(数组)版 | RN 未封装单 fromUser 版,无需动作 | +| `EMFetchServerMessagesOption.from` 属性 | `fromIds` | RN 模型只暴露 `fromIds`,无需动作 | +| `EMChatroom.muteList` 属性 | `muteMembers` | RN `ChatRoom.muteList` 对应项本轮随 deprecated 清理删除,用 `muteKVList` | +| `EMChatroomManagerDelegate userDidJoinChatroom:user:` | `userDidJoinChatroom:user:ext:` | RN 事件走 ext 版负载,无需动作 | +| `EMChatroomManagerDelegate chatroomMuteListDidUpdate:addedMutedMembers:muteExpire:` | Map 版回调 | RN iOS wrapper 已由用户手动删除该回调实现 | +| `EMMultiDevicesDelegate multiDevicesUndisturbEventNotifyFormOtherDeviceData:` | `multiDevicesConversationEvent:conversationId:conversationType:` | RN 未封装该旧回调,无需动作 | + +内部(非公开 API,仅记录):`EMManager init`(Helper/EMManager.h:32,内部基类)。 + +**Android(SDK_5.0.0,公开 4 条):** + +| native API | 替代 | RN 处理 | +| --- | --- | --- | +| `EMChatManager.downloadAttachment(EMMessage)` | `downloadAttachment(EMMessage, EMCallBack)` | RN 未封装无回调版(Promise 封装本就等价回调版),无需动作 | +| `EMChatManager.downloadThumbnail(EMMessage)` | `downloadThumbnail(EMMessage, EMCallBack)` | 同上 | +| `EMImageMessageBody.setThumbnailSecret` / `getThumbnailSecret` | `setSecret` / `getSecret` | RN `ChatFileMessageBody.thumbnailSecret` 本轮随 deprecated 清理删除,用 `secret` | +| `EMImageMessageBody.isSendOriginalImage` | `isOriginalImage` | RN 未封装该方法,无需动作 | + +内部/工具(RN 未封装,仅记录):`EasyUtils.isAppRunningForeground`(util)、adapter 包 `EMAChatManagerListener`/`EMAGroupManagerListenerInterface` 各 1-2 处(内部适配层)。 + +注:Android 5.0.0 本次**无新增废弃**——4.x 的 `@Deprecated` 大多直接删除(见变更清单 removed 条目),上表为 5.0.0 中仍存活的废弃标注全量。 + +## 5. 用户决策记录(2026-09-16 已全部裁决) + +1. **`renewAgoraToken` → `renewToken`**:**已裁决——改名**。native 只有 `renewToken`,TS 公开方法同步改名(参数 `agoraToken`→`token`),MT 常量三层本已是 `renewToken` 无需动。本轮实施。 +2. **`ChatMessage` 四属性改名**(hasReadAck→isPeerRead 等):**已裁决——接受**该 breaking change 进 5.0.0(阶段三已实施)。 +3. **`fetchGroupMessageReadReceipts` 跨端差异**:**已裁决——接受**,TS 保持现状(`totalCount` optional、`groupId` Android 忽略)。 +4. **defer 101 条**:**已裁决——按默认批量通过**「无需处理」(其中已被实现期编译强制改动的 7 条维持实现结果)。 +5. **`dataSyncType` 双端默认值不一致**:**已裁决——接受**,RN 不设显式默认,遵循 native。 +6. **`getAllChatRooms`**:**已裁决——三端删除,不保留**(替代原 Android 降级报错方案)。本轮实施。 +7. **`no_login_smoke` 预期值**:已实证关闭(见下)。 + +另:用户追加的通用裁决—— +- **4.x→5.x 主版本允许 API 移除**:native 删除的 RN 同步删除(不标记作废);**RN 自身 deprecated API 本次一并删除**(17 处 @deprecated,均已有替代 API)。本轮实施。 +- **native 标记作废(未删除)的 API 单独列举**:见 §4.4。 +- **`ChatAreaCode`/`ChatDataSyncType` 枚举归位**:不放独立文件,移入使用者文件 `ChatOptions.ts`。本轮实施。 +- **不再提供自动登录流程**:5.0.0 起由使用者自行模拟(已写 CHANGELOG,example auto_mode 注释已说明)。 +- **iOS 从 5.0.0 起默认 SPM 集成,继续支持 CocoaPods**:文档同步(docs/spm.md、AGENTS.md)。 +- **example 登录改造与 token 获取脚本**:**用户复审裁决(2026-09-16):本次不需要处理**。example 在阶段三已改为 token 登录(LoginPage/auto_mode)。 +- **a5 `searchMsgFromDB`(复审裁决 2026-09-16)**:已关闭——保持现状,不做迁移(前提核实见 §3 a5 行与 03-implementation.md)。 +- **N2 `isRead`(复审裁决 2026-09-16)**:已关闭——当前实现(直接映射不取反)确认无误。 +- **CocoaPods / SPM 发布状态(复审裁决 2026-09-16)**:用户确认 HyphenateChat 5.0.0 已发布到 CocoaPods trunk 与 SPM 远端;阶段四 iOS 验证走的 SPM 路径仅为当时 trunk 未发布的临时路径,podspec `~> 5.0.0` 可直接走默认 CocoaPods 路径。 + +## 6. 验证结果 + +详见同目录 `04-verification.md`(2026-09-16 实测)。摘要: + +| 验证项 | 结果 | 备注 | +| --- | --- | --- | +| `yarn typecheck` / `yarn lint` / `check:circular:dpdm` / `yarn test --no-watchman` | ✅ | 19 suites / 110 tests 全过(含 TS⊆Java⊆ObjC 契约测试);lint 初跑 1 个 prettier 格式错误已修复复跑通过 | +| example 构建(Android) | ✅ | 真实 Maven `hyphenate-chat:5.0.0` AAR,BUILD SUCCESSFUL | +| example 构建(iOS) | ✅(SPM 路径) | 验证时 CocoaPods trunk 无 5.0.0,走 SPM 集成路径完成;**2026-09-16 用户确认 CocoaPods 与 SPM 远端均已发布 5.0.0**,默认 CocoaPods 路径可用 | +| 无登录冒烟(Android / iOS,各 init+7 步) | ✅ | 双端全绿;contact 步初跑跨端分歧(getAllContacts:Android error 3 / iOS 空成功),已校准为 `addContact` 步骤,复测双端一致 201 | +| 契约要素 grep 抽查(MT 常量四层、事件 supportedEvents、消息四 key、isRead 取反、掩码映射、版本号对齐、CHANGELOG) | ✅ | 见 04-verification.md §4 | +| 登录态功能验证(群已读回执、设备管理、dataSync 事件等) | ❌ 部分不通过 | 2026-09-18 单账号 21 步复验:Android 19 passed / 1 crashed / 1 not-run;iOS 20 passed / 1 failed。dataSync 三事件两端已观察;双账号回执事件仍未执行 | +| 阶段四门禁(porting_guard.sh gate) | ✅ | exit=0 无输出 | + +**第二轮(审查决策落实,2026-09-16)验证:** + +| 验证项 | 结果 | 备注 | +| --- | --- | --- | +| `yarn typecheck` / `yarn lint` / `check:circular:dpdm` / `yarn test --no-watchman` | ✅ | 三轮改动(TS 17 处 deprecated 删除 + renewToken 改名 + 枚举归位;Android getAllChatRooms 删净;iOS getAllChatRooms/filter/autoLoginDidComplete 删除 + nil 防护)后复跑全绿,110 tests | +| example 构建(Android) | ✅ | 子代理改动后 `yarn example build:android` BUILD SUCCESSFUL | +| example 构建(iOS) | ✅ | 子代理改动后 `yarn example build:ios` 成功(SPM 路径) | +| 无登录冒烟(Android / iOS) | ✅ | 第二轮改动后双端复测全绿(init + 7 步) | +| 门禁复跑 | ✅ | exit=0 无输出 | + +**第三轮(可追溯运行报告与错误路径复验,2026-09-18):** + +| 验证项 | Android | iOS | 结论 | +| --- | --- | --- | --- | +| 21 步登录态脚本 | 19 passed / 1 crashed / 1 not-run | 20 passed / 1 failed | 正向 API 与两个批量缺失消息错误路径通过;分页缺失消息行为不一致 | +| `fetchGroupMessageReadReceipts` 传本地不存在的 msgId | native `NullPointerException`,进程终止 | 空成功 `{cursor:"", totalCount:0, list:[]}` | ❌ 修复暂缓;最终错误码已裁决双端统一为 110 | +| 三个 dataSync 事件 | 均观察到 | 均观察到 | ✅ | +| `onMessageReadReceipts` / `ChatGroupMemberInfo` | 未覆盖 | 未覆盖 | ⏭️ 需要双账号 | + +权威本地证据:Android `build/reports/5.0.0/20260918070127-android-emulator-5554/`,iOS `build/reports/5.0.0/20260918070518-ios-4BEA133B-4B24-430F-96FC-924632C2CF53/`,跨端对比 `build/reports/5.0.0/comparison-20260918071059.md`。这些目录被 Git 忽略,仅脱敏结论进入本报告。 + +## 7. 第三轮决策与遗留项(2026-09-18) + +1. **缺失消息错误码统一**:用户已裁决 iOS/Android 最终统一为 `INVALID_PARAM`(110);`fetchGroupMessageReadReceipts` 的 Android 崩溃、iOS 空成功及三个回执 API 的统一实现本轮暂缓。 +2. **消息 JSON 既有跨端字段差异**:报告发现 `body.targetLanguageCodes`、`receiverList` 仅 Android 返回;不属于本次 5.0.0 新增字段,建议先标记为既有差异,不在本轮顺手扩 scope,除非用户要求统一。 +3. **双账号验证**:`onMessageReadReceipts` 和非空 `ChatGroupMemberInfo` 仍需协调第二账号读取群消息后复验。 +4. **正向/反向用例拆分**:用户确认将 5.0.0 单账号脚本拆为 positive/negative 两条路径;报告目录结构与双端对比逻辑不变。已知会令 Android native 崩溃的 `fetchGroupMessageReadReceipts` 缺失消息用例暂不执行,只在报告中持续记录;双账号脚本暂不考虑。 +5. **反向用例新增发现**:双端对非法群成员、不存在群、缺失修改消息、空会话 ID 和设备管理无效 token 的错误码一致;两个批量回执 API 均仍返回 110,符合预期。`renewToken("")` 在 Android 返回 104、iOS 却成功;核对 iOS native 5.0.0 源码确认其空 token 分支构造错误后未立即返回。本轮仅记录,不修改 RN wrapper 或 native 行为。 + +## 8. 第四轮决策:项目侧移除多集群支持(2026-09-21) + +用户裁决:产品支持多种集群,但项目实现侧不做「多选」——`example/config.local.json` 只描述当前要跑的那一个环境,`restApi` / `appKey` / `clientId` / `clientSecret` 提到顶层,换环境=直接改/换这个文件。需要多套环境时,使用者自行在 Git 外维护 `config.ngi.json` / `config.ebs.json` 这类副本(不入库、敏感信息人工管理,`.gitignore` 已加 `example/config.*.json` 兜底),用哪套就复制为 `config.local.json`。已移除:`yarn env:use`(含 `scripts/env-use.js`)、`env.ts.` 缓存、`clusters` / `defaultCluster` / 每条资源的 `cluster` 字段、集群级 chatOptions 合并;`env-gettoken.js` 改为直接写 `example/src/env.ts` 且必填字段缺失即 fail fast。**保留私有化部署字段**(`enablePrivateConfig` + server 字段,描述「这一个环境是不是私有化」,与选集群无关)。旧格式配置会得到一句明确的迁移报错。与 Flutter 侧 `ece93914` 同一裁决。详见 `04-verification.md` 第 11 节。 diff --git a/docs/push.md b/docs/push.md index 7e35755d..24f64c3f 100644 --- a/docs/push.md +++ b/docs/push.md @@ -1,16 +1,20 @@ -更新日期: 2024-11-21 +更新日期: 2026-09-23 -离线消息接收,需要设置离线推送配置。 +离线消息接收需要设置离线推送配置。5.0.0 起推送配置分为两部分:**初始化时配置 iOS 证书名**,**运行时绑定设备推送 token**。 + +## 初始化:配置 iOS 证书名 + +`ChatOptions.apnsCertName` 与 `ChatOptions.pushKitCertName` 仅在 iOS 生效,其他平台忽略;两者只在 `ChatClient.init` 时下发给原生,运行时不可修改(详见[迁移指南](./5.0.0-migration-guide/2026-09-23-rn-4.x-to-5.0.0-migration-guide.md) 的「推送与设备 Token」一节)。 ```tsx ChatClient.getInstance() .init( ChatOptions.withAppKey({ appKey: appKey, - pushConfig: new ChatPushConfig({ - deviceId: '', // The data comes from the push configuration of the console. - deviceToken: '', // Provided by various vendors. See `react-native-push-collection` npm for details. - }), + // 控制台配置的 APNs 证书名,仅 iOS 生效 + apnsCertName: '', + // 需要 VoIP 推送时再配置,同样仅 iOS 生效 + pushKitCertName: '', }) ) .then(() => { @@ -22,4 +26,31 @@ ChatClient.getInstance() }); ``` -[详见 pushkit](https://github.com/easemob/react-native-push-collection) +## 运行时:绑定设备推送 token + +拿到推送 SDK 返回的 device token 后调用 `ChatClient.bindDeviceToken`: + +```tsx +await ChatClient.getInstance().bindDeviceToken({ + // 推送 SDK 或 APNs 回调返回的 token + deviceToken: '', + // Android 必填的厂商推送凭据(例如 FCM Sender ID、小米 App ID、OPPO App Key),iOS 忽略 + notifierName: '', +}); +``` + +Android 厂商推送的依赖、manifest 配置与 `EMPushConfig` 初始化需要在原生工程侧完成,SDK 只负责上报 token;各厂商 token 的获取可参考: +[react-native-push-collection](https://github.com/easemob/react-native-push-collection) + +## VoIP 推送(仅 iOS) + +VoIP 推送使用独立的 PushKit 证书与 token,需要业务侧实现 `PKPushRegistry` 拿到十六进制 token 后绑定;`ChatClient.logout(unbindDeviceToken: true)` 会同时解绑 PushKit token: + +```tsx +await ChatClient.getInstance().bindPushKitToken({ + deviceToken: '', +}); + +// 仅在保持登录状态、需要单独解绑时调用 +await ChatClient.getInstance().unbindPushKitToken(); +``` diff --git a/docs/rn_api_overview.md b/docs/rn_api_overview.md index a4c9ff1b..a04605d2 100644 --- a/docs/rn_api_overview.md +++ b/docs/rn_api_overview.md @@ -23,13 +23,9 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatClient.init init} | Initializes the SDK. | | {@link ChatClient.isConnected isConnected} | Checks whether the SDK is connected to the chat server. | | {@link ChatClient.getCurrentUsername getCurrentUsername} | Gets the current logged-in user ID from the server. | -| {@link ChatClient.isLoginBefore isLoginBefore} | Checks whether the current user is logged in to the app. | | {@link ChatClient.getAccessToken getAccessToken} | Gets the token for login. | -| {@link ChatClient.createAccount createAccount} | Creates a new user (open registration). | -| {@link ChatClient.login login} | Logs in to the chat server with a password or an Easemob token. An exception message is thrown if the login fails. | | {@link ChatClient.loginWithToken loginWithToken} | Logs in to the chat server with a token. An exception message is thrown if the login fails. | -| {@link ChatClient.loginWithAgoraToken loginWithAgoraToken} | @deprecated 2023-11-17 Use {@link login} instead. | -| {@link ChatClient.renewAgoraToken renewAgoraToken} | Renews the Agora token. | +| {@link ChatClient.renewToken renewToken} | Renews the token. | | {@link ChatClient.logout logout} | Logs out of the chat app. An exception message is thrown if the logout fails. | | {@link ChatClient.changeAppKey changeAppKey} | Updates the App Key, which is the unique identifier used to access the chat service. | | {@link ChatClient.changeAppId changeAppId} | Updates the App id, which is the unique identifier used to access the chat service. | @@ -66,17 +62,11 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatConnectEventListener.onDisconnected onDisconnected} | Occurs when the SDK disconnects from the chat server. | | {@link ChatConnectEventListener.onTokenWillExpire onTokenWillExpire} | Occurs when the token is about to expire. | | {@link ChatConnectEventListener.onTokenDidExpire onTokenDidExpire} | Occurs when the token has expired. | -| {@link ChatConnectEventListener.onAppActiveNumberReachLimit onAppActiveNumberReachLimit} | The number of daily active users (DAU) or monthly active users (MAU) for the app has reached the upper limit. | | {@link ChatConnectEventListener.onOfflineMessageSyncStart onOfflineMessageSyncStart} | Callback invoked when the synchronization of offline messages starts. | | {@link ChatConnectEventListener.onOfflineMessageSyncFinish onOfflineMessageSyncFinish} | Callback invoked when the synchronization of offline messages finishes. | -| {@link ChatConnectEventListener.onUserDidLoginFromOtherDevice onUserDidLoginFromOtherDevice} | Occurs when the current user account is logged in to another device. | -| {@link ChatConnectEventListener.onUserDidLoginFromOtherDeviceWithInfo onUserDidLoginFromOtherDeviceWithInfo} | Occurs when the current user account is logged in to another device. | -| {@link ChatConnectEventListener.onUserDidRemoveFromServer onUserDidRemoveFromServer} | Occurs when the current chat user is removed from the server. | -| {@link ChatConnectEventListener.onUserDidForbidByServer onUserDidForbidByServer} | Occurs when the current chat user is banned from accessing the server. | -| {@link ChatConnectEventListener.onUserDidChangePassword onUserDidChangePassword} | Occurs when the current chat user changed the password. | -| {@link ChatConnectEventListener.onUserDidLoginTooManyDevice onUserDidLoginTooManyDevice} | Occurs when the current chat user logged in to many devices. | -| {@link ChatConnectEventListener.onUserKickedByOtherDevice onUserKickedByOtherDevice} | Occurs when the current chat user is kicked out of the app by another device. | -| {@link ChatConnectEventListener.onUserAuthenticationFailed onUserAuthenticationFailed} | Occurs when the current chat user authentication failed. | +| {@link ChatConnectEventListener.onDataSyncStart onDataSyncStart} | Occurs when the SDK starts to synchronize data of the specified type from the server. | +| {@link ChatConnectEventListener.onDataSyncFinish onDataSyncFinish} | Occurs when the SDK finishes synchronizing data of the specified type from the server. | +| {@link ChatConnectEventListener.onDatabaseOpened onDatabaseOpened} | Occurs when the local database is opened. | | Event | Description | | :----- | :---------- | @@ -98,13 +88,12 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatManager.removeAllMessageListener removeAllMessageListener} | Removes all message listeners. | | {@link ChatManager.sendMessage sendMessage} | Sends a message. | | {@link ChatManager.resendMessage resendMessage} | Resends a message. | -| {@link ChatManager.sendMessageReadAck sendMessageReadAck} | Sends the read receipt to the server. | -| {@link ChatManager.sendGroupMessageReadAck sendGroupMessageReadAck} | Sends the group message receipt to the server. | -| {@link ChatManager.sendConversationReadAck sendConversationReadAck} | Sends the conversation read receipt to the server. | +| {@link ChatManager.sendMessageReadReceipts sendMessageReadReceipts} | Sends read receipts of messages to the server. | +| {@link ChatManager.clearConversationUnreadMessageCount clearConversationUnreadMessageCount} | Clears the unread message count of the specified conversation. | +| {@link ChatManager.clearAllConversationUnreadMessageCount clearAllConversationUnreadMessageCount} | Clears the unread message counts of all conversations. | | {@link ChatManager.recallMessage recallMessage} | For a one-to-one chat conversation, only the message sender can recall the message that is sent successfully. If the message expires, the recall fails. | | {@link ChatManager.getMessage getMessage} | Gets a message from the local database by message ID. | | {@link ChatManager.getMessagesWithIds getMessagesWithIds} | Gets messages with the specified IDs from the local database. | -| {@link ChatManager.markAllConversationsAsRead markAllConversationsAsRead} | Marks all conversations as read. | | {@link ChatManager.getUnreadCount getUnreadCount} | Gets the count of the unread messages. | | {@link ChatManager.insertMessage insertMessage} | Inserts a message to the conversation in the local database. | | {@link ChatManager.updateMessage updateMessage} | Updates the local message. | @@ -116,35 +105,27 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatManager.downloadBigImage downloadBigImage} | Downloads the big image of the image message. | | {@link ChatManager.voiceMessageToText voiceMessageToText} | Converts the voice in the voice message to text. | | {@link ChatManager.voiceFileToText voiceFileToText} | Converts the voice file to text. | -| {@link ChatManager.fetchHistoryMessages fetchHistoryMessages} | Uses the pagination to get messages in the specified conversation from the server. | | {@link ChatManager.fetchHistoryMessagesByOptions fetchHistoryMessagesByOptions} | retrieve the history message for the specified session from the server. | -| {@link ChatManager.searchMsgFromDB searchMsgFromDB} | Retrieves messages with keywords in a conversation from the local database. | | {@link ChatManager.getMsgsWithKeyword getMsgsWithKeyword} | Retrieves messages with keywords from the local database. | | {@link ChatManager.getConvsMsgsWithKeyword getConvsMsgsWithKeyword} | Loads messages with the specified keyword from the local database, returning a dictionary containing conversation IDs and message ID arrays. | -| {@link ChatManager.fetchGroupAcks fetchGroupAcks} | Uses the pagination to get read receipts for group messages from the server. | +| {@link ChatManager.getGroupMessageReadReceipts getGroupMessageReadReceipts} | Gets read receipts of group messages from the local database. | +| {@link ChatManager.fetchGroupMessageReadReceipts fetchGroupMessageReadReceipts} | Uses the pagination to get read receipts for a group message from the server. | | {@link ChatManager.removeConversationFromServer removeConversationFromServer} | Deletes the specified conversation and its historical messages from the server. | | {@link ChatManager.getConversation getConversation} | Gets the conversation by conversation ID and conversation type. | | {@link ChatManager.getAllConversations getAllConversations} | Gets all conversations from the local database. | -| {@link ChatManager.fetchAllConversations fetchAllConversations} | @deprecated 2023-07-24 Use {@link fetchConversationsFromServerWithCursor} instead. | | {@link ChatManager.deleteConversation deleteConversation} | Deletes a conversation and its local messages from the local database. | | {@link ChatManager.getLatestMessage getLatestMessage} | Gets the latest message from the conversation. | | {@link ChatManager.getLatestReceivedMessage getLatestReceivedMessage} | Gets the latest received message from the conversation. | | {@link ChatManager.getConversationUnreadCount getConversationUnreadCount} | Gets the unread message count of the conversation. | | {@link ChatManager.getConversationMessageCount getConversationMessageCount} | Gets the message count of the conversation. | -| {@link ChatManager.markMessageAsRead markMessageAsRead} | Marks a message as read. | -| {@link ChatManager.markAllMessagesAsRead markAllMessagesAsRead} | Marks all messages as read. | | {@link ChatManager.updateConversationMessage updateConversationMessage} | Updates a message in the local database. | | {@link ChatManager.deleteMessage deleteMessage} | Deletes a message from the local database. | | {@link ChatManager.deleteMessagesWithTimestamp deleteMessagesWithTimestamp} | Deletes messages sent or received in a certain period from the local database. | | {@link ChatManager.deleteConversationAllMessages deleteConversationAllMessages} | Deletes all messages in the conversation from both the memory and local database. | | {@link ChatManager.deleteMessagesBeforeTimestamp deleteMessagesBeforeTimestamp} | Deletes local messages with timestamp that is before the specified one. | -| {@link ChatManager.getMessagesWithMsgType getMessagesWithMsgType} | Retrieves messages of a certain type in a conversation from the local database. | | {@link ChatManager.getMsgsWithMsgType getMsgsWithMsgType} | Retrieves messages of a certain type in the conversation from the local database. | -| {@link ChatManager.getMessages getMessages} | Retrieves messages of a specified quantity in a conversation from the local database. | | {@link ChatManager.getMsgs getMsgs} | Retrieves messages of a specified quantity in a conversation from the local database. | -| {@link ChatManager.getMessagesWithKeyword getMessagesWithKeyword} | Gets messages that the specified user sends in a conversation in a certain period. | | {@link ChatManager.getConvMsgsWithKeyword getConvMsgsWithKeyword} | Gets messages that the specified user sends in a conversation in a certain period. | -| {@link ChatManager.getMessageWithTimestamp getMessageWithTimestamp} | Retrieves messages that are sent and received in a certain period in a conversation in the local database. | | {@link ChatManager.getMsgWithTimestamp getMsgWithTimestamp} | Retrieves messages that are sent and received in a certain period in a conversation in the local database. | | {@link ChatManager.translateMessage translateMessage} | Translates a text message. | | {@link ChatManager.fetchSupportedLanguages fetchSupportedLanguages} | Gets all languages supported by the translation service. | @@ -153,9 +134,7 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatManager.removeReaction removeReaction} | Deletes a Reaction. | | {@link ChatManager.fetchReactionList fetchReactionList} | Gets the list of Reactions. | | {@link ChatManager.fetchReactionDetail fetchReactionDetail} | Gets the Reaction details. | -| {@link ChatManager.reportMessage reportMessage} | Reports an inappropriate message. | | {@link ChatManager.getReactionList getReactionList} | Gets the list of Reactions from a message. | -| {@link ChatManager.groupAckCount groupAckCount} | Gets the number of members that have read the group message. | | {@link ChatManager.createChatThread createChatThread} | Creates a message thread. | | {@link ChatManager.joinChatThread joinChatThread} | Joins a message thread. | | {@link ChatManager.leaveChatThread leaveChatThread} | Leaves a message thread. | @@ -170,18 +149,13 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatManager.fetchChatThreadFromServer fetchChatThreadFromServer} | Gets the details of the message thread from the server. | | {@link ChatManager.getMessageThread getMessageThread} | Gets the details of the message thread from the memory. | | {@link ChatManager.getThreadConversation getThreadConversation} | Gets the thread conversation by conversation ID. | -| {@link ChatManager.fetchConversationsFromServerWithPage fetchConversationsFromServerWithPage} | Gets conversations from the server with pagination. | | {@link ChatManager.removeMessagesFromServerWithMsgIds removeMessagesFromServerWithMsgIds} | Deletes messages from the conversation (from both local storage and server). | | {@link ChatManager.removeMessagesFromServerWithTimestamp removeMessagesFromServerWithTimestamp} | Deletes messages from the conversation (from both local storage and server). | -| {@link ChatManager.fetchConversationsFromServerWithCursor fetchConversationsFromServerWithCursor} | Gets the list of conversations from the server with pagination. | -| {@link ChatManager.fetchPinnedConversationsFromServerWithCursor fetchPinnedConversationsFromServerWithCursor} | Get the list of pinned conversations from the server with pagination. | | {@link ChatManager.pinConversation pinConversation} | Sets whether to pin a conversation. | -| {@link ChatManager.modifyMessageBody modifyMessageBody} | Modifies a message. | | {@link ChatManager.modifyMsgBody modifyMsgBody} | Modifies a message both in the local storage and server. | | {@link ChatManager.fetchCombineMessageDetail fetchCombineMessageDetail} | Gets the list of original messages included in a combined message. | | {@link ChatManager.addRemoteAndLocalConversationsMark addRemoteAndLocalConversationsMark} | Marks conversations. | | {@link ChatManager.deleteRemoteAndLocalConversationsMark deleteRemoteAndLocalConversationsMark} | Unmarks conversations. | -| {@link ChatManager.fetchConversationsByOptions fetchConversationsByOptions} | Gets the conversations from the server by conversation filter options. | | {@link ChatManager.deleteAllMessageAndConversation deleteAllMessageAndConversation} | Clears all conversations and all messages in them. | | {@link ChatManager.pinMessage pinMessage} | Pins a message. | | {@link ChatManager.unpinMessage unpinMessage} | Unpins a message. | @@ -200,12 +174,10 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatMessageEventListener.onMessagesReceived onMessagesReceived} | Occurs when a message is received. | | {@link ChatMessageEventListener.onStreamMessagesReceived onStreamMessagesReceived} | Occurs when the SDK receives new messages. | | {@link ChatMessageEventListener.onCmdMessagesReceived onCmdMessagesReceived} | Occurs when a command message is received. | -| {@link ChatMessageEventListener.onMessagesRead onMessagesRead} | Occurs when a read receipt is received for a message. | -| {@link ChatMessageEventListener.onGroupMessageRead onGroupMessageRead} | Occurs when a read receipt is received for a group message. | +| {@link ChatMessageEventListener.onMessageReadReceipts onMessageReadReceipts} | Occurs when message read receipts are received. | | {@link ChatMessageEventListener.onMessagesDelivered onMessagesDelivered} | Occurs when a delivery receipt is received. | | {@link ChatMessageEventListener.onMessagesRecalledInfo onMessagesRecalledInfo} | Occurs when a received message is recalled. | | {@link ChatMessageEventListener.onConversationsUpdate onConversationsUpdate} | Occurs when the conversation is updated. | -| {@link ChatMessageEventListener.onConversationRead onConversationRead} | Occurs when a conversation read receipt is received. | | {@link ChatMessageEventListener.onMessageReactionDidChange onMessageReactionDidChange} | Occurs when a message reaction changes. | | {@link ChatMessageEventListener.onChatMessageThreadCreated onChatMessageThreadCreated} | Occurs when a message thread is created. | | {@link ChatMessageEventListener.onChatMessageThreadUpdated onChatMessageThreadUpdated} | Occurs when a message thread is updated. | @@ -222,7 +194,6 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatContactManager.removeAllContactListener removeAllContactListener} | Removes all contact listeners. | | {@link ChatContactManager.addContact addContact} | Adds a new contact. | | {@link ChatContactManager.deleteContact deleteContact} | Deletes a contact and all the related conversations. | -| {@link ChatContactManager.getAllContactsFromServer getAllContactsFromServer} | Gets the contact list from the server. | | {@link ChatContactManager.getAllContactsFromDB getAllContactsFromDB} | Gets the contact list from the local database. | | {@link ChatContactManager.addUserToBlockList addUserToBlockList} | Adds a contact to the block list. | | {@link ChatContactManager.removeUserFromBlockList removeUserFromBlockList} | Removes the contact from the block list. | @@ -233,8 +204,6 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatContactManager.getSelfIdsOnOtherPlatform getSelfIdsOnOtherPlatform} | Gets the unique IDs of the current user on the other devices. The ID is in the format of `{user_ID} + "/" + {resource_ID}`. | | {@link ChatContactManager.getAllContacts getAllContacts} | Gets all contacts from the local database. | | {@link ChatContactManager.getContact getContact} | Gets the contact by user ID from local database. | -| {@link ChatContactManager.fetchAllContacts fetchAllContacts} | Gets all contacts from the server. | -| {@link ChatContactManager.fetchContacts fetchContacts} | Gets the contacts from the server. | | {@link ChatContactManager.setContactRemark setContactRemark} | Set the contact's remark. | | Event | Description | @@ -244,8 +213,6 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatContactEventListener.onContactInvited onContactInvited} | Occurs when a friend request is received by the current user. | | {@link ChatContactEventListener.onFriendRequestAccepted onFriendRequestAccepted} | Occurs when a friend request is accepted by the current user. | | {@link ChatContactEventListener.onFriendRequestDeclined onFriendRequestDeclined} | Occurs when a friend request is declined by the current user. | -| {@link ChatContactEventListener.onContactSyncStart onContactSyncStart} | Occurs when the contact list starts to be synchronized from the server. | -| {@link ChatContactEventListener.onContactSyncFinish onContactSyncFinish} | Occurs when the contact list synchronization from the server is finished. | | {@link ChatContactEventListener.onContactInfoUpdate onContactInfoUpdate} | Occurs when the information of a contact is updated. | ## ChatGroupManager | Method | Description | @@ -253,11 +220,8 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatGroupManager.setNativeListener setNativeListener} | The group manager class, which defines how to manage groups, like group creation and destruction and member management. | | {@link ChatGroupManager.getGroupWithId getGroupWithId} | Gets the group instance from the memory by group ID. | | {@link ChatGroupManager.getJoinedGroups getJoinedGroups} | Gets the list of groups that the current user has joined. | -| {@link ChatGroupManager.fetchJoinedGroupsFromServer fetchJoinedGroupsFromServer} | Gets the list of groups that the current user has joined. | -| {@link ChatGroupManager.fetchPublicGroupsFromServer fetchPublicGroupsFromServer} | Gets public groups from the server with pagination. | -| {@link ChatGroupManager.createGroup createGroup} | Creates a group instance. | | {@link ChatGroupManager.createGroupEx createGroupEx} | Creates a group instance. | -| {@link ChatGroupManager.fetchGroupInfoFromServer fetchGroupInfoFromServer} | Gets the group information from the server. | +| {@link ChatGroupManager.updateGroupConfigs updateGroupConfigs} | Updates the configs of a group. | | {@link ChatGroupManager.fetchGroupInfoWithoutMembersFromServer fetchGroupInfoWithoutMembersFromServer} | Gets the group information from the server. | | {@link ChatGroupManager.fetchMemberListFromServer fetchMemberListFromServer} | Uses the pagination to get the member list of the group from the server. | | {@link ChatGroupManager.fetchMemberInfoListFromServer fetchMemberInfoListFromServer} | Uses the pagination to get the member information list of the group from the server. | @@ -325,9 +289,7 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatGroupEventListener.onAdminAdded onAdminAdded} | Occurs when a member is set as an admin. | | {@link ChatGroupEventListener.onAdminRemoved onAdminRemoved} | Occurs when the administrative privileges of an admin are removed. | | {@link ChatGroupEventListener.onOwnerChanged onOwnerChanged} | Occurs when the group ownership is transferred. | -| {@link ChatGroupEventListener.onMemberJoined onMemberJoined} | Occurs when a user joins a group. | | {@link ChatGroupEventListener.onMembersJoined onMembersJoined} | Occurs when multiple users join a group. | -| {@link ChatGroupEventListener.onMemberExited onMemberExited} | Occurs when a member voluntarily leaves the group. | | {@link ChatGroupEventListener.onMembersExited onMembersExited} | Occurs when multiple users leave a group. | | {@link ChatGroupEventListener.onAnnouncementChanged onAnnouncementChanged} | Occurs when the group announcement is updated. | | {@link ChatGroupEventListener.onSharedFileAdded onSharedFileAdded} | Occurs when a shared file is added to the group. | @@ -346,14 +308,11 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatRoomManager.addRoomListener addRoomListener} | Adds a chat room listener. | | {@link ChatRoomManager.removeRoomListener removeRoomListener} | Removes the chat room listener. | | {@link ChatRoomManager.removeAllRoomListener removeAllRoomListener} | Removes all the chat room listeners. | -| {@link ChatRoomManager.joinChatRoom joinChatRoom} | Joins the chat room. | | {@link ChatRoomManager.joinChatRoomEx joinChatRoomEx} | Joins the chat room. | | {@link ChatRoomManager.leaveChatRoom leaveChatRoom} | Leaves the chat room. | | {@link ChatRoomManager.fetchPublicChatRoomsFromServer fetchPublicChatRoomsFromServer} | Gets chat room data from the server with pagination. | | {@link ChatRoomManager.fetchChatRoomInfoFromServer fetchChatRoomInfoFromServer} | Gets the details of the chat room from the server. | | {@link ChatRoomManager.getChatRoomWithId getChatRoomWithId} | Gets the chat room by ID from the local database. | -| {@link ChatRoomManager.createChatRoom createChatRoom} | Creates a chat room. | -| {@link ChatRoomManager.destroyChatRoom destroyChatRoom} | Destroys a chat room. | | {@link ChatRoomManager.changeChatRoomSubject changeChatRoomSubject} | Changes the chat room name. | | {@link ChatRoomManager.changeChatRoomDescription changeChatRoomDescription} | Modifies the chat room description. | | {@link ChatRoomManager.fetchChatRoomMembers fetchChatRoomMembers} | Gets the chat room member list. | @@ -386,7 +345,6 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatRoomEventListener.onMemberJoined onMemberJoined} | Occurs when a member joins the chat room. All chat room members, except the new member, receive this event. | | {@link ChatRoomEventListener.onMemberExited onMemberExited} | Occurs when a member exits the chat room. All chat room members, except the member exiting the chat room, receive this event. | | {@link ChatRoomEventListener.onMemberRemoved onMemberRemoved} | Occurs when a member is removed from a chat room. The member that is kicked out of the chat room receive this event. | -| {@link ChatRoomEventListener.onMuteListAdded onMuteListAdded} | Occurs when the chat room member(s) is/are added to the mute list. The muted members receive this event. | | {@link ChatRoomEventListener.onMuteListAddedV2 onMuteListAddedV2} | Occurs when the chat room member(s) is/are added to the mute list. The muted members receive this event. | | {@link ChatRoomEventListener.onMuteListRemoved onMuteListRemoved} | Occurs when the chat room member(s) is/are removed from the mute list. The members that are removed from the mute list receive this event. | | {@link ChatRoomEventListener.onAdminAdded onAdminAdded} | Occurs when a chat room member is set as an admin. The member set as the chat room admin receives this event. | @@ -463,7 +421,6 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatMessage.createCustomMessage createCustomMessage} | Creates a custom message for sending. | | {@link ChatMessage.createReceiveMessage createReceiveMessage} | Creates a received message instance. | | {@link ChatMessage.reactionList reactionList} | Gets the list of Reactions. | -| {@link ChatMessage.groupReadCount groupReadCount} | Gets the count of read receipts of a group message. | | {@link ChatMessage.threadInfo threadInfo} | Gets details of a message thread. | | {@link ChatMessage.getPinInfo getPinInfo} | Get the list of pinned messages in the conversation. | | {@link ChatMessage.messagePriority messagePriority} | Set the chat room message priority. | @@ -477,20 +434,14 @@ ChatSDK is a highly reliable global communication platform where your users can | {@link ChatConversation.getLatestMessage getLatestMessage} | Gets the latest message from the conversation. | | {@link ChatConversation.getLatestReceivedMessage getLatestReceivedMessage} | Gets the latest message received in the conversation. | | {@link ChatConversation.setConversationExtension setConversationExtension} | Sets the extension information of the conversation. | -| {@link ChatConversation.markMessageAsRead markMessageAsRead} | Marks a message as read. | -| {@link ChatConversation.markAllMessagesAsRead markAllMessagesAsRead} | Marks all messages as read. | | {@link ChatConversation.updateMessage updateMessage} | Updates a message in the local database. | | {@link ChatConversation.deleteMessage deleteMessage} | Deletes a message from the local database. | | {@link ChatConversation.deleteMessagesWithTimestamp deleteMessagesWithTimestamp} | Deletes messages sent or received in a certain period from the local database. | | {@link ChatConversation.deleteAllMessages deleteAllMessages} | Deletes all the messages of the conversation. | -| {@link ChatConversation.getMessagesWithMsgType getMessagesWithMsgType} | Gets messages of a certain type that a specified user sends in a conversation. | | {@link ChatConversation.getMsgsWithMsgType getMsgsWithMsgType} | Gets messages of a certain type in the conversation from the local database. | -| {@link ChatConversation.getMessages getMessages} | Gets messages of a certain quantity in a conversation from the local database. | | {@link ChatConversation.getMsgs getMsgs} | Gets messages of a specified quantity in a conversation from the local database. | | {@link ChatConversation.getMessagesWithIds getMessagesWithIds} | Gets messages with the specified IDs from the local database. | -| {@link ChatConversation.getMessagesWithKeyword getMessagesWithKeyword} | Gets messages with keywords in a conversation in the local database. | | {@link ChatConversation.getMsgsWithKeyword getMsgsWithKeyword} | Gets messages that the specified user sends in a conversation in a certain period. | -| {@link ChatConversation.getMessageWithTimestamp getMessageWithTimestamp} | Gets messages that are sent and received in a certain period in a conversation in the local database. | | {@link ChatConversation.getMsgWithTimestamp getMsgWithTimestamp} | Gets messages that are sent and received in a certain period in a conversation in the local database. | | {@link ChatConversation.removeMessagesFromServerWithMsgIds removeMessagesFromServerWithMsgIds} | Deletes messages from the conversation (from both local storage and server). | | {@link ChatConversation.removeMessagesFromServerWithTimestamp removeMessagesFromServerWithTimestamp} | Deletes messages from the conversation (from both local storage and server). | diff --git a/docs/rn_api_overview.zh.md b/docs/rn_api_overview.zh.md index f4633a4d..9a5b179f 100644 --- a/docs/rn_api_overview.zh.md +++ b/docs/rn_api_overview.zh.md @@ -23,13 +23,9 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatClient.init init} | 初始化 SDK。 | | {@link ChatClient.isConnected isConnected} | 检查 SDK 是否连接到 Chat 服务器。 | | {@link ChatClient.getCurrentUsername getCurrentUsername} | 从服务器获取当前登录用户的用户 ID。 | -| {@link ChatClient.isLoginBefore isLoginBefore} | 检查当前用户是否登录。 | | {@link ChatClient.getAccessToken getAccessToken} | 获取登录 token。 | -| {@link ChatClient.createAccount createAccount} | 注册新用户(开放注册)。 | -| {@link ChatClient.login login} | 通过密码或环信 token 登录 Chat 服务器。 | | {@link ChatClient.loginWithToken loginWithToken} | 通过用户ID和token登录。 | -| {@link ChatClient.loginWithAgoraToken loginWithAgoraToken} | 通过用户ID和token登录。 | -| {@link ChatClient.renewAgoraToken renewAgoraToken} | 更新声网 token。 | +| {@link ChatClient.renewToken renewToken} | 更新 token。 | | {@link ChatClient.logout logout} | 退出登录。 | | {@link ChatClient.changeAppKey changeAppKey} | 修改 App Key。 | | {@link ChatClient.changeAppId changeAppId} | 更新 app iD。 | @@ -37,7 +33,7 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatClient.getLoggedInDevicesFromServer getLoggedInDevicesFromServer} | 获取指定账号下登录的在线设备列表。 | | {@link ChatClient.kickDevice kickDevice} | 将特定账号登录的指定设备下线。 | | {@link ChatClient.kickAllDevices kickAllDevices} | 将指定账号登录的所有设备都踢下线。 | -| {@link ChatClient.updatePushConfig updatePushConfig} | 更新推送设置。 | +| {@link ChatClient.bindDeviceToken bindDeviceToken} | 绑定设备的推送 token。 | | {@link ChatClient.bindPushKitToken bindPushKitToken} | 绑定 Apple PushKit token,用于 VoIP 推送通知。 | | {@link ChatClient.unbindPushKitToken unbindPushKitToken} | 解绑通过 {@link ChatClient.bindPushKitToken} 绑定的 Apple PushKit token。 | | {@link ChatClient.getRTCTokenInfoWithChannelName getRTCTokenInfoWithChannelName} | 根据频道名称(channelName)获取与 Agora Chat 用户 ID 匹配的声网 RTC token、token 过期时间和 RTC UID。 | @@ -65,20 +61,14 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | Event | Description | | :----- | :---------- | | {@link ChatConnectEventListener.onConnected onConnected} | 成功连接到 chat 服务器时触发的回调。 | -| {@link ChatConnectEventListener.onDisconnected onDisconnected} | 和 chat 服务器断开连接时触发的回调。 | +| {@link ChatConnectEventListener.onDisconnected onDisconnected} | SDK 与 chat 服务器断开连接时触发。 | | {@link ChatConnectEventListener.onTokenWillExpire onTokenWillExpire} | Agora token 即将过期时触发。 | | {@link ChatConnectEventListener.onTokenDidExpire onTokenDidExpire} | Agora token 已过期时触发。 | -| {@link ChatConnectEventListener.onAppActiveNumberReachLimit onAppActiveNumberReachLimit} | 应用程序的日活跃用户数量(DAU)或月活跃用户数量(MAU)达到上限时回调。 | | {@link ChatConnectEventListener.onOfflineMessageSyncStart onOfflineMessageSyncStart} | 开始接收离线消息的时候触发。 | | {@link ChatConnectEventListener.onOfflineMessageSyncFinish onOfflineMessageSyncFinish} | 结束接收离线消息的时候触发。 | -| {@link ChatConnectEventListener.onUserDidLoginFromOtherDevice onUserDidLoginFromOtherDevice} | 其他设备登录通知。 | -| {@link ChatConnectEventListener.onUserDidLoginFromOtherDeviceWithInfo onUserDidLoginFromOtherDeviceWithInfo} | 用户在其它设备登录。 | -| {@link ChatConnectEventListener.onUserDidRemoveFromServer onUserDidRemoveFromServer} | 用户被移除通知。 | -| {@link ChatConnectEventListener.onUserDidForbidByServer onUserDidForbidByServer} | 被服务器禁止连接通知。 | -| {@link ChatConnectEventListener.onUserDidChangePassword onUserDidChangePassword} | 用户密码变更通知。 | -| {@link ChatConnectEventListener.onUserDidLoginTooManyDevice onUserDidLoginTooManyDevice} | 登录设备数量超限通知。 | -| {@link ChatConnectEventListener.onUserKickedByOtherDevice onUserKickedByOtherDevice} | 被其他设备踢掉通知。 | -| {@link ChatConnectEventListener.onUserAuthenticationFailed onUserAuthenticationFailed} | 鉴权失败通知。 典型触发通知场景:token 过期、token 验证失败。 | +| {@link ChatConnectEventListener.onDataSyncStart onDataSyncStart} | SDK 开始从服务器同步指定类型的数据时触发。 | +| {@link ChatConnectEventListener.onDataSyncFinish onDataSyncFinish} | SDK 完成从服务器同步指定类型的数据时触发。 | +| {@link ChatConnectEventListener.onDatabaseOpened onDatabaseOpened} | 本地数据库打开时触发。 | | Event | Description | | :----- | :---------- | @@ -100,13 +90,12 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatManager.removeAllMessageListener removeAllMessageListener} | 移除所有消息监听器。 | | {@link ChatManager.sendMessage sendMessage} | 发送一条消息。 | | {@link ChatManager.resendMessage resendMessage} | 重发消息。 | -| {@link ChatManager.sendMessageReadAck sendMessageReadAck} | 发送消息的已读回执。 | -| {@link ChatManager.sendGroupMessageReadAck sendGroupMessageReadAck} | 发送群消息已读回执。 | -| {@link ChatManager.sendConversationReadAck sendConversationReadAck} | 发送会话的已读回执。 | +| {@link ChatManager.sendMessageReadReceipts sendMessageReadReceipts} | 向服务器发送消息已读回执。 | +| {@link ChatManager.clearConversationUnreadMessageCount clearConversationUnreadMessageCount} | 清除指定会话的未读消息数。 | +| {@link ChatManager.clearAllConversationUnreadMessageCount clearAllConversationUnreadMessageCount} | 清除所有会话的未读消息数。 | | {@link ChatManager.recallMessage recallMessage} | 撤回发送成功的消息。 | | {@link ChatManager.getMessage getMessage} | 从本地数据库获取指定 ID 的消息对象。 | | {@link ChatManager.getMessagesWithIds getMessagesWithIds} | 通过消息 ID 列表获取本地数据库中的消息。 | -| {@link ChatManager.markAllConversationsAsRead markAllConversationsAsRead} | 将所有的会话都设成已读。 | | {@link ChatManager.getUnreadCount getUnreadCount} | 获取未读消息数。 | | {@link ChatManager.insertMessage insertMessage} | 在本地会话中插入一条消息。 | | {@link ChatManager.updateMessage updateMessage} | 更新本地消息。 | @@ -118,35 +107,28 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatManager.downloadBigImage downloadBigImage} | 下载图片消息的大图。 | | {@link ChatManager.voiceMessageToText voiceMessageToText} | 将语音消息中的语音转换为文本。 | | {@link ChatManager.voiceFileToText voiceFileToText} | 将语音文件转换为文本。 | -| {@link ChatManager.fetchHistoryMessages fetchHistoryMessages} | 分页获取指定会话的历史消息。 | | {@link ChatManager.fetchHistoryMessagesByOptions fetchHistoryMessagesByOptions} | 根据消息拉取参数配置从服务器分页获取指定会话的历史消息。 | -| {@link ChatManager.searchMsgFromDB searchMsgFromDB} | 从本地数据库获取指定会话中包含特定关键字的消息。 | | {@link ChatManager.getMsgsWithKeyword getMsgsWithKeyword} | 获取所有会话在一定时间内的会话中发送的消息。 | | {@link ChatManager.getConvsMsgsWithKeyword getConvsMsgsWithKeyword} | 通过关键字搜索本地消息。 | -| {@link ChatManager.fetchGroupAcks fetchGroupAcks} | Uses the pagination to get read receipts for group messages from the server. | +| {@link ChatManager.getGroupMessageReadReceipts getGroupMessageReadReceipts} | 从本地数据库获取群组消息的已读回执。 | +| {@link ChatManager.fetchGroupMessageReadReceipts fetchGroupMessageReadReceipts} | 分页从服务端获取群组消息的已读回执。 | | {@link ChatManager.removeConversationFromServer removeConversationFromServer} | 删除服务端的指定会话及其历史消息。 | | {@link ChatManager.getConversation getConversation} | 根据会话ID和会话类型获取会话。 | | {@link ChatManager.getAllConversations getAllConversations} | 获取本地数据库中所有会话。 | -| {@link ChatManager.fetchAllConversations fetchAllConversations} | 从服务器获取会话列表。 | | {@link ChatManager.deleteConversation deleteConversation} | 删除指定会话及其本地历史消息。 | -| {@link ChatManager.getLatestMessage getLatestMessage} | 从会话中获取最新消息。 | +| {@link ChatManager.deleteConversations deleteConversations} | 从本地数据库批量删除会话。 | +| {@link ChatManager.getLatestMessage getLatestMessage} | 获取会话中的最新消息。 | | {@link ChatManager.getLatestReceivedMessage getLatestReceivedMessage} | 获取最新收到的会话消息。 | | {@link ChatManager.getConversationUnreadCount getConversationUnreadCount} | 获取会话未读消息数。 | | {@link ChatManager.getConversationMessageCount getConversationMessageCount} | 获取会话的消息数。 | -| {@link ChatManager.markMessageAsRead markMessageAsRead} | 将消息标记为已读。 | -| {@link ChatManager.markAllMessagesAsRead markAllMessagesAsRead} | 将所有消息标记为已读。 | | {@link ChatManager.updateConversationMessage updateConversationMessage} | 更新本地数据库中的消息。 | | {@link ChatManager.deleteMessage deleteMessage} | 从本地数据库中删除一条消息。 | | {@link ChatManager.deleteMessagesWithTimestamp deleteMessagesWithTimestamp} | 从本地数据库中删除一段时间内发送或接收的消息。 | | {@link ChatManager.deleteConversationAllMessages deleteConversationAllMessages} | 从内存和本地数据库中删除会话中的所有消息。 | | {@link ChatManager.deleteMessagesBeforeTimestamp deleteMessagesBeforeTimestamp} | 删除指定时间戳之前的所有本地消息。 | -| {@link ChatManager.getMessagesWithMsgType getMessagesWithMsgType} | 从本地数据库中检索会话中某种类型的消息。 | | {@link ChatManager.getMsgsWithMsgType getMsgsWithMsgType} | 从本地数据库中检索会话中某种类型的消息。 | -| {@link ChatManager.getMessages getMessages} | 从本地数据库中检索会话中指定数量的消息。 | | {@link ChatManager.getMsgs getMsgs} | 从本地数据库中检索会话中指定数量的消息。 | -| {@link ChatManager.getMessagesWithKeyword getMessagesWithKeyword} | 获取指定用户在一定时间内的会话中发送的消息。 | | {@link ChatManager.getConvMsgsWithKeyword getConvMsgsWithKeyword} | 获取指定用户在一定时间内的会话中发送的消息。 | -| {@link ChatManager.getMessageWithTimestamp getMessageWithTimestamp} | 检索本地数据库中某个会话在一定时间内发送和接收的消息。 | | {@link ChatManager.getMsgWithTimestamp getMsgWithTimestamp} | 检索本地数据库中某个会话在一定时间内发送和接收的消息。 | | {@link ChatManager.translateMessage translateMessage} | 翻译消息。 | | {@link ChatManager.fetchSupportedLanguages fetchSupportedLanguages} | 查询翻译服务支持的语言。 | @@ -155,9 +137,7 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatManager.removeReaction removeReaction} | 删除 Reaction。 | | {@link ChatManager.fetchReactionList fetchReactionList} | 获取 Reaction 列表。 | | {@link ChatManager.fetchReactionDetail fetchReactionDetail} | 获取 Reaction 详情。 | -| {@link ChatManager.reportMessage reportMessage} | 举报消息。 | | {@link ChatManager.getReactionList getReactionList} | 获取指定消息的 Reaction 列表。 | -| {@link ChatManager.groupAckCount groupAckCount} | 获取群组消息的已读人数。 | | {@link ChatManager.createChatThread createChatThread} | 创建子区。 | | {@link ChatManager.joinChatThread joinChatThread} | 加入子区。 | | {@link ChatManager.leaveChatThread leaveChatThread} | 退出子区。 | @@ -172,19 +152,13 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatManager.fetchChatThreadFromServer fetchChatThreadFromServer} | 从服务器获取子区详情。 | | {@link ChatManager.getMessageThread getMessageThread} | 获取本地子区详情。 | | {@link ChatManager.getThreadConversation getThreadConversation} | 根据指定会话 ID 获取本地子区会话对象。 | -| {@link ChatManager.fetchConversationsFromServerWithPage fetchConversationsFromServerWithPage} | 从服务器分页获取会话列表。 | | {@link ChatManager.removeMessagesFromServerWithMsgIds removeMessagesFromServerWithMsgIds} | 从会话中删除消息(从本地存储和服务器)。 | | {@link ChatManager.removeMessagesFromServerWithTimestamp removeMessagesFromServerWithTimestamp} | 从会话中删除消息(从本地存储和服务器)。 | -| {@link ChatManager.fetchConversationsFromServerWithCursor fetchConversationsFromServerWithCursor} | 分页从服务器获取会话列表。 | -| {@link ChatManager.fetchPinnedConversationsFromServerWithCursor fetchPinnedConversationsFromServerWithCursor} | 分页从服务器获取置顶会话。 | -| {@link ChatManager.fetchConversationsFromDB fetchConversationsFromDB} | 从本地数据库分页获取会话列表。 | | {@link ChatManager.pinConversation pinConversation} | 设置会话是否置顶。 | -| {@link ChatManager.modifyMessageBody modifyMessageBody} | 修改文本消息。 | | {@link ChatManager.modifyMsgBody modifyMsgBody} | 更新消息内容。本地和服务器都更新。 | | {@link ChatManager.fetchCombineMessageDetail fetchCombineMessageDetail} | 获取合并类型消息中的原始消息列表。 | | {@link ChatManager.addRemoteAndLocalConversationsMark addRemoteAndLocalConversationsMark} | 标记会话。 | | {@link ChatManager.deleteRemoteAndLocalConversationsMark deleteRemoteAndLocalConversationsMark} | 取消标记会话。 | -| {@link ChatManager.fetchConversationsByOptions fetchConversationsByOptions} | 按会话过滤选项从服务端获取会话。 | | {@link ChatManager.deleteAllMessageAndConversation deleteAllMessageAndConversation} | 清空所有会话及其消息。 | | {@link ChatManager.pinMessage pinMessage} | 置顶消息。 | | {@link ChatManager.unpinMessage unpinMessage} | 取消置顶消息。 | @@ -203,12 +177,10 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatMessageEventListener.onMessagesReceived onMessagesReceived} | 收到消息回调。 | | {@link ChatMessageEventListener.onStreamMessagesReceived onStreamMessagesReceived} | 收到流式消息回调。 | | {@link ChatMessageEventListener.onCmdMessagesReceived onCmdMessagesReceived} | 收到命令消息回调。 | -| {@link ChatMessageEventListener.onMessagesRead onMessagesRead} | 收到单聊消息已读回执的回调。 | -| {@link ChatMessageEventListener.onGroupMessageRead onGroupMessageRead} | 收到群组消息的已读回执的回调。 | +| {@link ChatMessageEventListener.onMessageReadReceipts onMessageReadReceipts} | 收到消息已读回执时触发。 | | {@link ChatMessageEventListener.onMessagesDelivered onMessagesDelivered} | 收到消息已送达回执的回调。 | | {@link ChatMessageEventListener.onMessagesRecalledInfo onMessagesRecalledInfo} | 收到消息撤销通知的回调。 | -| {@link ChatMessageEventListener.onConversationsUpdate onConversationsUpdate} | 会话更新事件回调。 | -| {@link ChatMessageEventListener.onConversationRead onConversationRead} | 收到会话已读回执的回调。 | +| {@link ChatMessageEventListener.onConversationsUpdate onConversationsUpdate} | 会话更新时触发。 | | {@link ChatMessageEventListener.onMessageReactionDidChange onMessageReactionDidChange} | 消息表情回复(Reaction)变化监听器。 | | {@link ChatMessageEventListener.onChatMessageThreadCreated onChatMessageThreadCreated} | 子区创建回调。 | | {@link ChatMessageEventListener.onChatMessageThreadUpdated onChatMessageThreadUpdated} | 子区更新回调。 | @@ -225,7 +197,6 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatContactManager.removeAllContactListener removeAllContactListener} | 移除所有联系人监听器。 | | {@link ChatContactManager.addContact addContact} | 添加好友。 | | {@link ChatContactManager.deleteContact deleteContact} | 删除联系人及其相关的会话。 | -| {@link ChatContactManager.getAllContactsFromServer getAllContactsFromServer} | 从服务器获取联系人列表。 | | {@link ChatContactManager.getAllContactsFromDB getAllContactsFromDB} | 从本地数据库获取联系人列表。 | | {@link ChatContactManager.addUserToBlockList addUserToBlockList} | 将指定用户加入黑名单。 | | {@link ChatContactManager.removeUserFromBlockList removeUserFromBlockList} | 将指定用户移除黑名单。 | @@ -236,8 +207,6 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatContactManager.getSelfIdsOnOtherPlatform getSelfIdsOnOtherPlatform} | 获取登录用户在其他登录设备上唯一 ID,该 ID 由 user ID + "/" + resource 组成。 | | {@link ChatContactManager.getAllContacts getAllContacts} | 从本地数据库获取所有所有联系人。 | | {@link ChatContactManager.getContact getContact} | 从本地数据库获取指定联系人备注信息。 | -| {@link ChatContactManager.fetchAllContacts fetchAllContacts} | 从服务器获取所有联系人。 | -| {@link ChatContactManager.fetchContacts fetchContacts} | 从服务器分页获取联系人 | | {@link ChatContactManager.setContactRemark setContactRemark} | 设置联系人备注。 | | Event | Description | @@ -247,8 +216,6 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatContactEventListener.onContactInvited onContactInvited} | 当前用户收到好友请求的回调。 | | {@link ChatContactEventListener.onFriendRequestAccepted onFriendRequestAccepted} | 当前用户同意好友请求的回调。 | | {@link ChatContactEventListener.onFriendRequestDeclined onFriendRequestDeclined} | 拒绝好友请求的回调。 | -| {@link ChatContactEventListener.onContactSyncStart onContactSyncStart} | 开始从服务器同步联系人列表时触发。 | -| {@link ChatContactEventListener.onContactSyncFinish onContactSyncFinish} | 从服务器同步联系人列表完成时触发。 | | {@link ChatContactEventListener.onContactInfoUpdate onContactInfoUpdate} | 联系人信息更新时触发。 | ## ChatGroupManager | Method | Description | @@ -256,11 +223,8 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatGroupManager.setNativeListener setNativeListener} | 群组管理类,用于管理群组的创建,删除及成员管理等操作。 | | {@link ChatGroupManager.getGroupWithId getGroupWithId} | 根据群组 ID,从内存中获取群组对象。 | | {@link ChatGroupManager.getJoinedGroups getJoinedGroups} | 从本地数据库获取当前用户已加入的群组。 | -| {@link ChatGroupManager.fetchJoinedGroupsFromServer fetchJoinedGroupsFromServer} | 以分页方式从服务器获取当前用户已加入的群组。 | -| {@link ChatGroupManager.fetchPublicGroupsFromServer fetchPublicGroupsFromServer} | 分页从服务器获取公开群组。 | -| {@link ChatGroupManager.createGroup createGroup} | 创建群组。 | | {@link ChatGroupManager.createGroupEx createGroupEx} | 创建群组对象。支持设置头像。 | -| {@link ChatGroupManager.fetchGroupInfoFromServer fetchGroupInfoFromServer} | 从服务器获取群组详情。 | +| {@link ChatGroupManager.updateGroupConfigs updateGroupConfigs} | 更新群组配置。 | | {@link ChatGroupManager.fetchGroupInfoWithoutMembersFromServer fetchGroupInfoWithoutMembersFromServer} | 获取群组信息,但不包含群组成员信息。 | | {@link ChatGroupManager.fetchMemberListFromServer fetchMemberListFromServer} | 从服务器分页获取群组成员。 | | {@link ChatGroupManager.fetchMemberInfoListFromServer fetchMemberInfoListFromServer} | 从服务器分页获取群组成员信息列表。 | @@ -328,9 +292,7 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatGroupEventListener.onAdminAdded onAdminAdded} | 成员设置为管理员的回调。 | | {@link ChatGroupEventListener.onAdminRemoved onAdminRemoved} | 取消成员的管理员权限的回调。 | | {@link ChatGroupEventListener.onOwnerChanged onOwnerChanged} | 转移群主权限的回调。 | -| {@link ChatGroupEventListener.onMemberJoined onMemberJoined} | 新成员加入群组的回调。 | | {@link ChatGroupEventListener.onMembersJoined onMembersJoined} | 多个群成员加入群组的回调。 | -| {@link ChatGroupEventListener.onMemberExited onMemberExited} | 群组成员主动退出回调。 | | {@link ChatGroupEventListener.onMembersExited onMembersExited} | 多个群组成员主动退出回调。 | | {@link ChatGroupEventListener.onAnnouncementChanged onAnnouncementChanged} | 群公告更新回调。 | | {@link ChatGroupEventListener.onSharedFileAdded onSharedFileAdded} | 群组添加共享文件回调。 | @@ -349,14 +311,11 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatRoomManager.addRoomListener addRoomListener} | 注册聊天室监听器。 | | {@link ChatRoomManager.removeRoomListener removeRoomListener} | 移除聊天室监听器。 | | {@link ChatRoomManager.removeAllRoomListener removeAllRoomListener} | 移除所有聊天室监听器。 | -| {@link ChatRoomManager.joinChatRoom joinChatRoom} | 加入聊天室。 | | {@link ChatRoomManager.joinChatRoomEx joinChatRoomEx} | 加入聊天室。 | | {@link ChatRoomManager.leaveChatRoom leaveChatRoom} | 退出聊天室。 | | {@link ChatRoomManager.fetchPublicChatRoomsFromServer fetchPublicChatRoomsFromServer} | 从服务器分页获取公开聊天室。 | | {@link ChatRoomManager.fetchChatRoomInfoFromServer fetchChatRoomInfoFromServer} | 从服务器获取聊天室详情。 | | {@link ChatRoomManager.getChatRoomWithId getChatRoomWithId} | 根据聊天室 ID 从本地数据库获取聊天室。 | -| {@link ChatRoomManager.createChatRoom createChatRoom} | 创建聊天室。 | -| {@link ChatRoomManager.destroyChatRoom destroyChatRoom} | 解散聊天室。 | | {@link ChatRoomManager.changeChatRoomSubject changeChatRoomSubject} | 修改聊天室名称。 | | {@link ChatRoomManager.changeChatRoomDescription changeChatRoomDescription} | 修改聊天室描述信息。 | | {@link ChatRoomManager.fetchChatRoomMembers fetchChatRoomMembers} | 获取聊天室成员用户 ID 列表。 | @@ -389,7 +348,6 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatRoomEventListener.onMemberJoined onMemberJoined} | 聊天室加入新成员回调。 | | {@link ChatRoomEventListener.onMemberExited onMemberExited} | 聊天室成员主动退出回调。 | | {@link ChatRoomEventListener.onMemberRemoved onMemberRemoved} | 聊天室成员被移除回调。 | -| {@link ChatRoomEventListener.onMuteListAdded onMuteListAdded} | 有成员被禁言回调。 | | {@link ChatRoomEventListener.onMuteListAddedV2 onMuteListAddedV2} | 增加禁言成员时候回调。 | | {@link ChatRoomEventListener.onMuteListRemoved onMuteListRemoved} | 有成员从禁言列表中移除回调。 | | {@link ChatRoomEventListener.onAdminAdded onAdminAdded} | 有成员设置为聊天室管理员的回调。 | @@ -466,7 +424,6 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatMessage.createCustomMessage createCustomMessage} | 创建一条待发送的自定义类型消息。 | | {@link ChatMessage.createReceiveMessage createReceiveMessage} | 创建一条接收消息。 | | {@link ChatMessage.reactionList reactionList} | 获取 Reaction 列表。 | -| {@link ChatMessage.groupReadCount groupReadCount} | 获取群组消息的已读人数。 | | {@link ChatMessage.threadInfo threadInfo} | 获取指定子区的详情。 | | {@link ChatMessage.getPinInfo getPinInfo} | 获取消息的置顶信息。 | | {@link ChatMessage.messagePriority messagePriority} | 设置消息优先级。仅仅聊天室生效。 | @@ -480,20 +437,14 @@ ChatSDK 是一个高度可靠的全球交流平台,您的用户可以在其中 | {@link ChatConversation.getLatestMessage getLatestMessage} | 获取指定会话的最新消息。 | | {@link ChatConversation.getLatestReceivedMessage getLatestReceivedMessage} | 获取指定会话中最近接收到的消息。 | | {@link ChatConversation.setConversationExtension setConversationExtension} | 设置指定会话的自定义扩展信息。 | -| {@link ChatConversation.markMessageAsRead markMessageAsRead} | 标记指定消息为已读。 | -| {@link ChatConversation.markAllMessagesAsRead markAllMessagesAsRead} | 标记所有消息为已读。 | | {@link ChatConversation.updateMessage updateMessage} | 更新本地数据库的指定消息。 | | {@link ChatConversation.deleteMessage deleteMessage} | 删除本地数据库中的指定消息。 | | {@link ChatConversation.deleteMessagesWithTimestamp deleteMessagesWithTimestamp} | 删除消息。 | | {@link ChatConversation.deleteAllMessages deleteAllMessages} | 删除会话的所有消息。 | -| {@link ChatConversation.getMessagesWithMsgType getMessagesWithMsgType} | 从本地数据库获取会话中的指定用户发送的某些类型的消息。 | | {@link ChatConversation.getMsgsWithMsgType getMsgsWithMsgType} | 从本地数据库中检索会话中某种类型的消息。 | -| {@link ChatConversation.getMessages getMessages} | 从本地数据库中检索会话中一定数量的消息。 | | {@link ChatConversation.getMsgs getMsgs} | 从本地数据库中检索会话中指定数量的消息。 | | {@link ChatConversation.getMessagesWithIds getMessagesWithIds} | 从本地获取会话中指定 ID 的消息。 | -| {@link ChatConversation.getMessagesWithKeyword getMessagesWithKeyword} | 检索本地数据库中会话中带有关键字的消息。 | | {@link ChatConversation.getMsgsWithKeyword getMsgsWithKeyword} | 获取指定用户在一定时间段内在会话中发送的消息。 | -| {@link ChatConversation.getMessageWithTimestamp getMessageWithTimestamp} | 获取本地数据库中某个会话在一定时间内发送和接收的消息。 | | {@link ChatConversation.getMsgWithTimestamp getMsgWithTimestamp} | 检索本地数据库中某个会话在一定时间内发送和接收的消息。 | | {@link ChatConversation.removeMessagesFromServerWithMsgIds removeMessagesFromServerWithMsgIds} | 从会话中删除消息(从本地存储和服务器)。 | | {@link ChatConversation.removeMessagesFromServerWithTimestamp removeMessagesFromServerWithTimestamp} | 从会话中删除消息(从本地存储和服务器)。 | diff --git a/docs/spec/2026-09-20-connect-event-listener-spec.md b/docs/spec/2026-09-20-connect-event-listener-spec.md new file mode 100644 index 00000000..8e5c6840 --- /dev/null +++ b/docs/spec/2026-09-20-connect-event-listener-spec.md @@ -0,0 +1,152 @@ +# 连接事件监听器收敛设计说明(ChatConnectEventListener) + +- 日期:2026-09-20 +- 分支/版本:5.0.0(worktree `.worktree/5.0.0`,依赖原生 SDK iOS/Android 5.0.0) +- 状态:已实现(随 5.0.0 发布,属 breaking change);2026-09-21 有语义修订,见 §7 +- 关联:`docs/porting/5.0.0/02-contract.md` §4.1 末尾"连接事件收敛"裁决记录 + +## 1. 背景与问题 + +4.x 时代的 `ChatConnectEventListener` 中,除 `onConnected` 外,服务器强制下线被拆成 8 个独立回调: + +`onAppActiveNumberReachLimit`、`onUserDidLoginFromOtherDeviceWithInfo`、`onUserDidRemoveFromServer`、`onUserDidForbidByServer`、`onUserDidChangePassword`、`onUserDidLoginTooManyDevice`、`onUserKickedByOtherDevice`、`onUserAuthenticationFailed`,外加一个无参数的 `onDisconnected`。 + +问题: + +1. **与底层模型不同构**:Android 原生 `EMConnectionListener` 只有一个 `onDisconnected(int errorCode)`(外加 `onLogout(int, EMLoginExtensionInfo)`),8 个回调是 wrapper 层手工拆码人造的;iOS 5.0.0 也已把多数原因收敛进 `userAccountDidForcedToLogout:(EMError *)`。 +2. **维护成本高**:每增删一个断开原因要动 6 处(Consts.ts、ChatClient 订阅、ChatClient handler、ChatEvents 接口、Java wrapper、ObjC wrapper),加契约测试豁免名单。 +3. **跨端行为不一致**:iOS 强制下线时会同时发 `onDisconnected` + 具体事件,Android 只发具体事件。 +4. 仓库内已有相反哲学的成功先例:`ChatMultiDeviceEventListener` 用"一个回调 + 事件枚举参数"承载几十种多设备事件。 + +## 2. 原生侧事实核查(源码/头文件级) + +Android 依据 `hyphenate-chat-5.0.0-sources.jar` 中 `EMClient.MyConnectionListener.onDisconnected(int, EMALogoutInfo)` 的真实分发逻辑;iOS 依据 vendored 5.0.0 头文件(`example/ios/Pods/.../Headers/`)。 + +| 码 | 含义 | Android 5.0.0 通道 | iOS 5.0.0 通道 | +| --- | --- | --- | --- | +| 2 `NETWORK_ERROR` | 网络错误 | `onDisconnected(2)`(注册监听时若未连接会补发一次) | 无码,仅 `connectionStateDidChange(NO)` | +| 8 `APP_ACTIVE_NUMBER_REACH_LIMITATION` | DAU/MAU 达上限 | `onDisconnected` + `onLogout` 双发 | **无通道**(delegate 已删) | +| 108/109 | token 已/将过期 | 仅 `onTokenExpired/onTokenWillExpire`,**不伴随断开事件**,但会本地登出 | 同左 | +| 202 `USER_AUTHENTICATION_FAILED` | 认证失败 | **无生产者**(token-only 登录后只走 login 回调) | `ForcedToLogout` 文档未列出,存疑 | +| 206 `USER_LOGIN_ANOTHER_DEVICE` | 他端登录 | `onDisconnected` + `onLogout` **双发**(info 仅此码有值) | 独立 delegate,info 为 `_Nullable` | +| 207 `USER_REMOVED` | 账号被删除 | 双发 | 独立 delegate | +| 213 `USER_BIND_ANOTHER_DEVICE` | 已绑定其他设备 | 双发(4.x wrapper 漏映射) | 无通道 | +| 214 `USER_LOGIN_TOO_MANY_DEVICES` | 登录设备过多 | 双发 | `ForcedToLogout` | +| 216 `USER_KICKED_BY_CHANGE_PASSWORD` | 密码已修改 | 双发 | `ForcedToLogout` | +| 217 `USER_KICKED_BY_OTHER_DEVICE` | 被他端踢下线 | 双发 | `ForcedToLogout` | +| 220 `USER_DEVICE_CHANGED` | 登录设备变更需重新登录 | 双发(4.x wrapper 漏映射) | 无通道 | +| 305 `SERVER_SERVICE_RESTRICTED` | app 被服务器禁用(双端官网口径一致) | 双发 | `userDidForbidByServer`(无码,需合成) | + +关键结论: + +- Android 的 `onDisconnected(int)` 对**所有码无条件触发**;`onLogout` 仅对 8/206/207/213/214/216/217/220/305 九个码额外触发。206 两个回调都发,跳过 `onDisconnected(206)` 是必需的。 +- `onUserAuthenticationFailed`(202)在 5.0.0 双端都接近死代码。 +- 旧实现 bug:Android `onLogout` 不过滤 206,导致每次强制下线都多发一个空参数的 `onUserDidLoginFromOtherDeviceWithInfo`;iOS `LoginFromOtherDeviceWithInfo` 的 `_Nullable` info 直接塞字典字面量,有 `NSInvalidArgumentException` 崩溃隐患。 + +## 3. 设计决策 + +统一为带错误码的单回调(用户裁决,2026-09-20): + +```ts +export enum ChatDisconnectErrorCode { + NETWORK_ERROR = 2, // 仅 Android;iOS 网络断开无码 + APP_ACTIVE_NUMBER_REACH_LIMIT = 8, // 仅 Android + USER_AUTHENTICATION_FAILED = 202, // 保留码位;5.0.0 双端基本无触发源 + USER_LOGIN_FROM_OTHER_DEVICE = 206, // 唯一带 info 的码 + USER_REMOVED_FROM_SERVER = 207, + USER_BIND_ANOTHER_DEVICE = 213, // 仅 Android + USER_LOGIN_TOO_MANY_DEVICES = 214, + USER_KICKED_BY_CHANGE_PASSWORD = 216, + USER_KICKED_BY_OTHER_DEVICE = 217, + USER_DEVICE_CHANGED = 220, // 仅 Android + SERVER_SERVICE_RESTRICTED = 305, // app 被服务器禁用 +} + +export interface ChatConnectEventListener { + onConnected?(): void; + onDisconnected?(errorCode?: number, info?: { deviceName?: string; ext?: string }): void; + // onTokenWillExpire / onTokenDidExpire / onOfflineMessageSync* / onDataSync* / onDatabaseOpened 保持不变 +} +``` + +语义约定(写入 JSDoc): + +- 无 `errorCode` 或为 `NETWORK_ERROR`:网络波动断开,SDK 自动重连,用户保持登录态。 +- 其余 `errorCode`:被服务器强制下线,需重新登录。 +- token 过期导致的登出**不**触发 `onDisconnected`,仅触发 `onTokenDidExpire`——只监听断开回调会漏掉该场景,文档已点明。 +- 枚举值与原生 `EMError` 数值一致,未来新增断开原因只需加枚举成员 + 注释,无需动接线。 + +## 4. 实现规格 + +### 4.1 事件契约 + +- 唯一 MT key:`onDisconnected`;负载 `{errorCode?: number, deviceName?: string, ext?: string}`,`deviceName`/`ext` 仅 206 携带。 +- 被删的 8 个 MT 常量在 TS(`Consts.ts`)/ Java(`ExtSdkMethodType.java`)/ ObjC(`ExtSdkMethodTypeObjc.h/.m` 含 Value 映射)三层同步删除,含双端 dispatch 的 "no implement" stub 与 `ExtSdkApiObjcRN.mm` 的 `supportedEvents`。 +- C++ 层(`modules/cpp`)本就不含这些事件常量,无需变更。 + +### 4.2 TS 层(`src/`) + +- `ChatEvents.ts`:新增 `ChatDisconnectErrorCode` 枚举;接口删除 8 个回调,`onDisconnected` 改为 `(errorCode?, info?)`。 +- `ChatClient.ts`:删除 8 段订阅与 8 个转发 handler;`onDisconnected` 解析负载,仅当 `deviceName`/`ext` 存在时组装 `info`。 + +### 4.3 Android(`modules/java/`) + +- `onDisconnected(int)`:206 跳过(由 `onLogout` 合并设备信息后发同一事件),其余码直接透传 `{errorCode}`。 +- `onLogout(int, EMLoginExtensionInfo)`:仅处理 206,发 `{errorCode, deviceName, ext}`;其余码忽略(已由 `onDisconnected(int)` 送达)。修复旧版双发 bug。 +- 魔法数字改为 `EMError.USER_LOGIN_ANOTHER_DEVICE` 常量引用。 + +### 4.4 iOS(`modules/objc/`) + +- 新增内部方法 `emitDisconnectedWithCode:params:`:组装 `{errorCode[, deviceName, ext]}` 发 `onDisconnected`,并置 `forcedLogoutPending = YES`。 +- `userAccountDidForcedToLogout:` 透传 `aError.code`(含 nil 保护);`userAccountDidRemoveFromServer` → 207;`userDidForbidByServer` → 305(`EMErrorServerServingForbidden`);`userAccountDidLoginFromOtherDeviceWithInfo:` → 206 + info(逐字段 nil 保护,修崩溃隐患)。 +- `connectionStateDidChange:`:连接成功复位 `forcedLogoutPending` 并发 `onConnected`;断开时若 `forcedLogoutPending` 置位则吞掉一次(强制下线后 SDK 拆除连接会重复触发),否则发无码 `onDisconnected`。 + +## 5. 测试与验证 + +- 单元测试(`src/__tests__/unit/ChatClient.events.test.ts`)新增 3 条:错误码透传、206 携带设备信息、网络断开无码;契约测试(三层方法名/事件名一致性、事件接线完整性)随常量同步删除自动通过。 +- 本地已验证:`yarn typecheck`、`yarn lint`、`yarn test`(19 套件 115 用例)全绿。 +- example(`example/src/listeners.ts` 的 `CONNECT_METHODS`)已同步删减。 +- **遗留运行时验证项**(native wrapper 无自动化覆盖,按仓库规则需真机实测后推送): + 1. iOS 强制下线时 `connectionStateDidChange(NO)` 是否真的伴随触发(决定 `forcedLogoutPending` 防御是否必需/充分); + 2. iOS `userAccountDidForcedToLogout:` 实际下发的码集合(头文件文档不完整)。 + +## 6. 迁移说明(breaking) + +使用方删除 8 个回调的实现,改为在 `onDisconnected` 中按 `errorCode` 分支处理;需要"他端登录"设备信息时判断 `errorCode === ChatDisconnectErrorCode.USER_LOGIN_FROM_OTHER_DEVICE` 并读取 `info`。详见 `CHANGELOG.md` / `CHANGELOG.zh.md` 5.0.0 节。 + +## 7. 修订(2026-09-21):补全断开码、修正语义规则 + +依据 native 5.0.0 源码复查(emclient-linux `src/emsessionmanager.cpp:755-933`、emclient-ios `newSDK/HyphenateSDK/EMConnectionListener.mm:31-58`、emclient-android `hyphenatechatsdk/src/com/hyphenate/chat/EMClient.java:1661-1773`),并对齐 Flutter 侧同主题规格(`im_flutter_sdk` `docs/spec/2026-09-21-connection-event-normalization-spec.md`),对 §2/§3 做如下修正: + +### 7.1 断开码补全 + +`ChatDisconnectErrorCode` 由 11 个码扩充为断开路径全量 19 个码(值与 native `EMError` 一致): + +- 退出原因(本地登录态失效,需重新登录):8、104、110、202、204、206、207、213、214、216、217、220、304、305。其中 **104/110/204 为本次新增**——core 已对这几个码执行登出,Android 以 `onDisconnected(code)` 送达(无 `onLogout`),iOS 折算为无码断开。 +- 连接原因(保持登录态,SDK 自动重连):2、4、300、303、306。其中 **4/300/303/306 为本次新增**——Android 弱网下的常见断开码是 300/303 而非 2。 +- 108(token 过期)仍只走 `onTokenDidExpire`,不属于断开事件,不收录。 + +§2 表格的错误更正: + +- 8 在 iOS **有通道**:`userAccountDidForcedToLogout:` 的 `aError.code` 可为 8(原表"无通道"依据的是 delegate 删除,但 forcedToLogout 的透传码覆盖了它)。 +- 202 双端**均有真实触发源**:iOS `forcedToLogout(202)`;Android `doReconnect()` 刷新 token 失败时以 202 断开并登出(原表"无生产者"不成立)。 +- 213 在 iOS 已登出但只表现为无码断开;220 在 iOS 与 206 共用 `userAccountDidLoginFromOtherDeviceWithInfo:`,无法区分。 +- iOS `connectionStateDidChange:` 断开不携带任何原因码,因此 iOS 上 104/110/204/213 这类"静默登出"与普通网络断开无法从事件区分——这是平台限制,接入侧如需确认登录态,以后续 API 的未登录错误兜底。 + +### 7.2 语义规则修正 + +§3 的"其余 `errorCode` 均为强制下线"规则**作废**,替换为: + +- `errorCode` 原样透传,SDK 不过滤;枚举仅供阅读与比较,**未列出的码(含 native 未来新增)也会原样送达**。 +- 按 §7.1 的两组划分处理:退出原因回登录页;连接原因等自动重连。 +- 未列出的码默认按连接原因处理,除非 native SDK 另有说明。 +- 无 `errorCode` 表示平台未提供原因(如 iOS 网络断开、App 切后台),按连接事件处理,不得据此判定退出。 +- 退出共有两个事件通道:`onDisconnected`(退出原因码)与 `onTokenDidExpire`(token 过期),接入方必须两个都处理。 + +### 7.3 平台可达性注释的取舍 + +原枚举注释中"8/213/220 仅 Android 可达"的标注删除(8 在 iOS 可达,见 §7.1;跨端可达性会随平台版本漂移,以实际收到的码为准)。仅保留 2 的平台差异说明(iOS 网络断开无码),它影响"无码即网络断开"的解读。 + +### 7.4 接线不变 + +本次修订只改 TS 枚举、文档与单测;Android wrapper(`onDisconnected(int)` 透传 + `onLogout` 合并 206)与 iOS wrapper(`forcedToLogout` 透传 `aError.code`、固定折算 206/207/305、无码断开)原本就是透传/折算设计,新增码自动生效,无需改动。 diff --git a/docs/spm.md b/docs/spm.md index 3ac72c8e..1d5ac64b 100644 --- a/docs/spm.md +++ b/docs/spm.md @@ -1,23 +1,13 @@ -# iOS 集成方式:CocoaPods 与 Swift Package Manager +# iOS 集成方式:Swift Package Manager 与 CocoaPods -ChatSdk 的 iOS 端同时支持两种 `HyphenateChat` 依赖集成方式,由 `ChatSdk.podspec` 自动选择: +自 ChatSdk 5.0.0(HyphenateChat iOS 5.0.0)起,**iOS 默认推荐 Swift Package Manager(SPM)集成,CocoaPods 继续支持**。ChatSdk 的 iOS 端同时支持两种 `HyphenateChat` 依赖集成方式,由 `ChatSdk.podspec` 自动选择: | 条件 | 集成方式 | | --- | --- | -| React Native >= 0.75 且 `USE_FRAMEWORKS=dynamic pod install` | Swift Package Manager(`spm_dependency`) | -| 其他情况(RN < 0.75、静态链接、默认 `pod install`) | CocoaPods(`HyphenateChat` pod) | +| React Native >= 0.75 且 `USE_FRAMEWORKS=dynamic pod install` | Swift Package Manager(`spm_dependency`)——**推荐/默认** | +| 其他情况(RN < 0.75、静态链接、默认 `pod install`) | CocoaPods(`HyphenateChat` pod)——继续支持 | -## CocoaPods(默认) - -无需任何额外配置,保持现有流程: - -```sh -cd ios && pod install -``` - -SDK 依赖 `HyphenateChat ~> 4.19.1` pod。注意:CocoaPods trunk 将于 2026-12-02 进入只读状态,已发布的版本仍可 `pod install`,但无法再从 trunk 获取新版本。 - -## Swift Package Manager(可选) +## Swift Package Manager(默认推荐) 要求: @@ -34,7 +24,7 @@ USE_FRAMEWORKS=dynamic pod install - 包地址: - product:`HyphenateChat` -- 版本约束:`upToNextMinorVersion`,最低 `4.19.1` +- 版本约束:`upToNextMinorVersion`,最低 `5.0.0` - `ShengwangInfra_iOS` 由该 SPM 包自动带入 注意事项: @@ -44,6 +34,16 @@ USE_FRAMEWORKS=dynamic pod install - 静态链接(默认或 `USE_FRAMEWORKS=static`)下请勿使用 SPM 路径,可能遇到 duplicate symbols / undefined symbols 等链接错误;此时会自动回退到 CocoaPods 方式。 - 源码中的 `#import ` 等导入方式在两种集成方式下均无需修改。 +## CocoaPods(继续支持) + +无需任何额外配置,保持现有流程: + +```sh +cd ios && pod install +``` + +SDK 依赖 `HyphenateChat ~> 5.0.0` pod。注意:CocoaPods trunk 将于 2026-12-02 进入只读状态,已发布的版本仍可 `pod install`,但无法再从 trunk 获取新版本——这也是 5.0.0 起推荐 SPM 的原因之一。 + ## 选择逻辑 `ChatSdk.podspec` 中的判断逻辑: @@ -52,8 +52,8 @@ USE_FRAMEWORKS=dynamic pod install if defined?(:spm_dependency) && ENV['USE_FRAMEWORKS'] == 'dynamic' spm_dependency(s, ...) else - s.dependency 'HyphenateChat', '~> 4.19.1' + s.dependency 'HyphenateChat', '~> 5.0.0' end ``` -即:只有宿主 App 明确使用动态链接且 RN 版本支持时才走 SPM,其余一律保持 CocoaPods 行为,对现有使用者无破坏性变更。 +即:宿主 App 使用动态链接且 RN 版本支持时走 SPM(推荐路径),其余保持 CocoaPods 行为,对现有使用者无破坏性变更。 diff --git a/eslint.config.mjs b/eslint.config.mjs index a7ae1eb8..8a429a23 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -33,6 +33,8 @@ export default defineConfig([ '**/build/', 'coverage/', '**/Pods/', + // 生成文件:scripts/env-gettoken.js 输出,不入 git + 'example/src/env.ts', ], }, ]); diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index b1bbb4db..6582db88 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -90,7 +90,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 3 - versionName "1.21.0" + versionName "5.0.0" } signingConfigs { debug { diff --git a/example/ci/him23707_keyword_senders.json b/example/ci/him23707_keyword_senders.json new file mode 100644 index 00000000..1b493448 --- /dev/null +++ b/example/ci/him23707_keyword_senders.json @@ -0,0 +1,97 @@ +{ + "steps": [ + { + "api": "ChatManager.sendMessage", + "id": "send", + "params": { + "targetId": "$config.accounts.1.id", + "content": "him23707kwretest20260922b", + "chatType": 0 + }, + "expect": { + "success": true + }, + "delayAfterMs": 5000 + }, + { + "api": "ChatManager.getMsgs", + "id": "dump-conv", + "params": { + "convId": "$config.accounts.1.id", + "convType": 0, + "startMsgId": "", + "loadCount": 20, + "direction": 0 + }, + "expect": { + "success": true + } + }, + { + "api": "ChatManager.getConvMsgsWithKeyword", + "id": "search-omit-senders", + "params": { + "convId": "$config.accounts.1.id", + "convType": 0, + "keywords": "him23707kwretest20260922b", + "timestamp": 9999999999999, + "count": 50, + "direction": 0, + "searchScope": 2 + }, + "expect": { + "success": true + } + }, + { + "api": "ChatManager.getConvMsgsWithKeyword", + "id": "search-omit-senders-scope0", + "params": { + "convId": "$config.accounts.1.id", + "convType": 0, + "keywords": "him23707kwretest20260922b", + "timestamp": 9999999999999, + "count": 50, + "direction": 0, + "searchScope": 0 + }, + "expect": { + "success": true + } + }, + { + "api": "ChatManager.getConvMsgsWithKeyword", + "id": "search-matching-senders", + "params": { + "convId": "$config.accounts.1.id", + "convType": 0, + "keywords": "him23707kwretest20260922b", + "timestamp": 9999999999999, + "count": 50, + "direction": 0, + "searchScope": 2, + "senders": ["$config.accounts.0.id"] + }, + "expect": { + "success": true + } + }, + { + "api": "ChatManager.getConvMsgsWithKeyword", + "id": "search-other-senders", + "params": { + "convId": "$config.accounts.1.id", + "convType": 0, + "keywords": "him23707kwretest20260922b", + "timestamp": 9999999999999, + "count": 50, + "direction": 0, + "searchScope": 2, + "senders": ["$config.accounts.1.id"] + }, + "expect": { + "success": true + } + } + ] +} diff --git a/example/ci/him_delete_conversations.json b/example/ci/him_delete_conversations.json new file mode 100644 index 00000000..827193e3 --- /dev/null +++ b/example/ci/him_delete_conversations.json @@ -0,0 +1,61 @@ +{ + "steps": [ + { + "api": "ChatManager.sendMessage", + "id": "send-peer", + "params": { + "targetId": "$config.accounts.1.id", + "content": "himdelconv20260922a", + "chatType": 0 + }, + "expect": { + "success": true + }, + "delayAfterMs": 3000 + }, + { + "api": "ChatManager.sendMessage", + "id": "send-group", + "params": { + "targetId": "$config.groups.0.id", + "content": "himdelconv20260922b", + "chatType": 1 + }, + "expect": { + "success": true + }, + "delayAfterMs": 3000 + }, + { + "api": "ChatManager.getAllConversations", + "id": "before-delete", + "params": {}, + "expect": { + "success": true + } + }, + { + "api": "ChatManager.deleteConversations", + "id": "delete-conversations", + "params": { + "convIds": [ + "$config.accounts.1.id", + "$config.groups.0.id", + "nonexistent_user_himdel" + ], + "deleteMessages": true + }, + "expect": { + "success": true + } + }, + { + "api": "ChatManager.getAllConversations", + "id": "after-delete", + "params": {}, + "expect": { + "success": true + } + } + ] +} diff --git a/example/ci/him_delete_conversations_edge.json b/example/ci/him_delete_conversations_edge.json new file mode 100644 index 00000000..509a897f --- /dev/null +++ b/example/ci/him_delete_conversations_edge.json @@ -0,0 +1,15 @@ +{ + "steps": [ + { + "api": "ChatManager.deleteConversations", + "id": "delete-all-missing", + "params": { + "convIds": ["nonexistent_user_himdel_x", "nonexistent_user_himdel_y"], + "deleteMessages": true + }, + "expect": { + "success": true + } + } + ] +} diff --git a/example/ci/him_unread_count.json b/example/ci/him_unread_count.json new file mode 100644 index 00000000..1fe52514 --- /dev/null +++ b/example/ci/him_unread_count.json @@ -0,0 +1,12 @@ +{ + "steps": [ + { + "api": "ChatManager.getUnreadCount", + "id": "unread-count", + "params": {}, + "expect": { + "success": true + } + } + ] +} diff --git a/example/ci/no_login_smoke.json b/example/ci/no_login_smoke.json index b15b0354..758d4dee 100644 --- a/example/ci/no_login_smoke.json +++ b/example/ci/no_login_smoke.json @@ -1,23 +1,24 @@ { - "init": { "appKey": "easemob#easeim", "autoLogin": false }, + "init": { "appKey": "easemob-demo#zuoyu" }, + "login": false, "steps": [ { - "api": "ChatManager.fetchConversationsFromServerWithCursor", - "id": "chat.fetchConversationsFromServerWithCursor", - "params": { "pageSize": 20 }, + "api": "ChatManager.searchMessagesFromServer", + "id": "chat.searchMessagesFromServer", + "params": { "keywordList": ["rn-ci"], "pageSize": 10, "pageNum": 1 }, "expect": { "errorCode": 201 } }, { - "api": "ChatContactManager.fetchAllContacts", - "id": "contact.fetchAllContacts", - "params": {}, + "api": "ChatContactManager.addContact", + "id": "contact.addContact", + "params": { "userId": "rn-ci-nonexistent-user", "reason": "rn-ci" }, "expect": { "errorCode": 201 } }, { - "api": "ChatGroupManager.fetchJoinedGroupsFromServer", - "id": "group.fetchJoinedGroupsFromServer", - "params": { "pageSize": 20, "pageNum": 0 }, - "expect": { "errorCode": 201 } + "api": "ChatGroupManager.getJoinedGroups", + "id": "group.getJoinedGroups", + "params": {}, + "expect": { "success": true } }, { "api": "ChatRoomManager.fetchPublicChatRoomsFromServer", diff --git a/example/ci/port_4_24_1.json b/example/ci/port_4_24_1.json index a54e0eb2..1436a0e0 100644 --- a/example/ci/port_4_24_1.json +++ b/example/ci/port_4_24_1.json @@ -1,7 +1,6 @@ { "init": { "appKey": "easemob-demo#zuoyu", - "autoLogin": false, "ntpServers": ["ntp1.aliyun.com"] }, "steps": [ @@ -42,9 +41,9 @@ "expect": { "success": true } }, { - "api": "ChatGroupManager.fetchJoinedGroupsFromServer", + "api": "ChatGroupManager.getJoinedGroups", "id": "group.fetchAfterUpdate", - "params": { "pageSize": 20, "pageNum": 0 }, + "params": {}, "expect": { "success": true } }, { diff --git a/example/ci/port_5_0_0_negative.json b/example/ci/port_5_0_0_negative.json new file mode 100644 index 00000000..3b01e32a --- /dev/null +++ b/example/ci/port_5_0_0_negative.json @@ -0,0 +1,102 @@ +{ + "_comment": "5.0.0 平版新增/修改 API 反向路径(auto 模式)。只使用单账号可稳定构造的非法参数、缺失资源和无效 token;无参数本地查询没有可控错误输入,不强造反例。fetchGroupMessageReadReceipts 的缺失消息场景已确认会令 Android native 进程崩溃,按用户决策暂不执行,继续由 docs/porting/5.0.0/04-verification.md 和报告 issue candidate 记录。三个缺失消息回执 API 的目标契约为 500 MESSAGE_INVALID;本脚本执行其中两个不会崩溃的批量 API,当前返回 110 时应标记失败。", + "steps": [ + { + "api": "ChatGroupManager.createGroupEx", + "id": "create_group_invalid_member", + "params": { + "groupName": "rn-port-500-negative", + "desc": "invalid member must reject group creation", + "inviteMembers": ["rn-ci-nonexistent-user"], + "configs": { + "maxCount": 200, + "inviteNeedConfirm": false, + "ext": "negative", + "isPublic": false, + "joinApprovalRequired": false, + "allowInvites": true + } + }, + "expect": { "errorCode": 600 } + }, + { + "api": "ChatGroupManager.updateGroupConfigs", + "id": "update_group_missing", + "params": { + "groupId": "not-exist-group-id-500", + "types": 32, + "configs": { + "maxCount": 200, + "inviteNeedConfirm": false, + "ext": "negative", + "isPublic": false, + "joinApprovalRequired": false, + "allowInvites": true + } + }, + "expect": { "errorCode": 600 } + }, + { + "api": "ChatManager.modifyMsgBody", + "id": "modify_message_missing", + "params": { + "msgId": "not-exist-msg-id-500", + "content": "must not be applied", + "ext": { "port": "5.0.0", "path": "negative" } + }, + "expect": { "errorCode": 500 } + }, + { + "api": "ChatManager.clearConversationUnreadMessageCount", + "id": "clear_conversation_empty", + "params": { "convId": "" }, + "expect": { "errorCode": 110 } + }, + { + "api": "ChatManager.sendMessageReadReceipts", + "id": "receipt_missing", + "params": { "msgIds": ["not-exist-msg-id-500"] }, + "expect": { "errorCode": 500 } + }, + { + "api": "ChatManager.getGroupMessageReadReceipts", + "id": "group_receipt_missing", + "params": { "msgIds": ["not-exist-group-msg-id-500"] }, + "expect": { "errorCode": 500 } + }, + { + "api": "ChatClient.getLoggedInDevicesFromServer", + "id": "devices_invalid_token", + "params": { + "userId": "$config.accounts.0.id", + "token": "rn-ci-invalid-token" + }, + "expect": { "errorCode": 303 } + }, + { + "api": "ChatClient.renewToken", + "id": "renew_invalid_token", + "params": { "token": "" }, + "expect": { "errorCode": 104 } + }, + { + "api": "ChatClient.kickDevice", + "id": "kick_device_invalid_token", + "params": { + "userId": "$config.accounts.0.id", + "token": "rn-ci-invalid-token", + "resource": "rn-ci-nonexistent-resource" + }, + "expect": { "errorCode": 303 } + }, + { + "api": "ChatClient.kickAllDevices", + "id": "kick_all_invalid_token", + "params": { + "userId": "$config.accounts.0.id", + "token": "rn-ci-invalid-token" + }, + "expect": { "errorCode": 303 } + } + ] +} diff --git a/example/ci/port_5_0_0_positive.json b/example/ci/port_5_0_0_positive.json new file mode 100644 index 00000000..cb788be0 --- /dev/null +++ b/example/ci/port_5_0_0_positive.json @@ -0,0 +1,164 @@ +{ + "_comment": "5.0.0 平版新增/修改 API 正向路径(auto 模式)。数据源为 env.ts(yarn env:gettoken 生成)。辅助步骤 sendMessage/destroyGroup 仅用于准备与清理,不计入被测 API。modifyMsgBody 按公开契约期待成功;未开通消息编辑服务的集群会返回 305,并由报告如实标记为环境限制。kickDevice 使用 getLoggedInDevicesFromServer 返回的真实 resource,会踢掉当前设备,因此与 kickAllDevices 固定放在最后。单账号只验证回执 API 的成功调用;非空回执、onMessageReadReceipts 与 ChatGroupMemberInfo 暂不覆盖。", + "steps": [ + { + "api": "ChatManager.getUnreadCount", + "id": "unread_before", + "params": {}, + "expect": { "success": true } + }, + { + "api": "ChatManager.sendMessage", + "id": "msg_self", + "params": { + "targetId": "$config.accounts.0.id", + "content": "port-5.0.0 positive self-chat", + "chatType": 0 + }, + "delayAfterMs": 1000, + "expect": { "success": true } + }, + { + "api": "ChatManager.modifyMsgBody", + "id": "modify_self", + "params": { + "msgId": "$step.msg_self.msgId", + "content": "port-5.0.0 positive self-chat (modified)", + "ext": { "port": "5.0.0", "path": "positive" } + }, + "expect": { "success": true } + }, + { + "api": "ChatManager.clearConversationUnreadMessageCount", + "id": "clear_conv", + "params": { "convId": "$config.accounts.0.id" }, + "expect": { "success": true } + }, + { + "api": "ChatManager.clearAllConversationUnreadMessageCount", + "id": "clear_all", + "params": {}, + "expect": { "success": true } + }, + { + "api": "ChatManager.getUnreadCount", + "id": "unread_after", + "params": {}, + "expect": { "success": true } + }, + { + "api": "ChatManager.getAllConversations", + "id": "convs", + "params": {}, + "expect": { "success": true } + }, + { + "api": "ChatGroupManager.createGroupEx", + "id": "grp", + "params": { + "groupName": "rn-port-500-positive", + "desc": "5.0.0 positive-path verification", + "configs": { + "maxCount": 200, + "inviteNeedConfirm": false, + "ext": "", + "isPublic": false, + "joinApprovalRequired": false, + "allowInvites": true + } + }, + "expect": { "success": true } + }, + { + "api": "ChatGroupManager.updateGroupConfigs", + "id": "grp_update", + "params": { + "groupId": "$step.grp.groupId", + "types": 34, + "configs": { + "maxCount": 300, + "inviteNeedConfirm": false, + "ext": "rn-port-500-positive", + "isPublic": false, + "joinApprovalRequired": false, + "allowInvites": true + } + }, + "expect": { "success": true } + }, + { + "api": "ChatManager.sendMessage", + "id": "gmsg", + "params": { + "targetId": "$step.grp.groupId", + "content": "port-5.0.0 positive group receipt", + "chatType": 1, + "isNeedReadReceipt": true + }, + "delayAfterMs": 1000, + "expect": { "success": true } + }, + { + "api": "ChatManager.getGroupMessageReadReceipts", + "id": "grp_receipts_local", + "params": { "msgIds": ["$step.gmsg.msgId"] }, + "expect": { "success": true } + }, + { + "api": "ChatManager.fetchGroupMessageReadReceipts", + "id": "grp_receipts_server", + "params": { + "msgId": "$step.gmsg.msgId", + "groupId": "$step.grp.groupId", + "pageSize": 20 + }, + "expect": { "success": true } + }, + { + "api": "ChatManager.sendMessageReadReceipts", + "id": "receipt_group", + "params": { "msgIds": ["$step.gmsg.msgId"] }, + "expect": { "success": true } + }, + { + "api": "ChatClient.getLoggedInDevicesFromServer", + "id": "devices", + "params": { + "userId": "$config.accounts.0.id", + "token": "$config.accounts.0.mm" + }, + "expect": { "success": true } + }, + { + "api": "ChatClient.renewToken", + "id": "renew", + "params": { "token": "$config.accounts.0.mm" }, + "expect": { "success": true } + }, + { + "api": "ChatGroupManager.destroyGroup", + "id": "grp_destroy", + "params": { "groupId": "$step.grp.groupId" }, + "expect": { "success": true } + }, + { + "api": "ChatClient.kickDevice", + "id": "kick_current_device", + "params": { + "userId": "$config.accounts.0.id", + "token": "$config.accounts.0.mm", + "resource": "$step.devices.0.resource" + }, + "expect": { "success": true } + }, + { + "api": "ChatClient.kickAllDevices", + "id": "kick_all", + "params": { + "userId": "$config.accounts.0.id", + "token": "$config.accounts.0.mm" + }, + "expect": { "success": true } + } + ] +} diff --git a/example/ci/single_account.json b/example/ci/single_account.json index 68a23658..68fab953 100644 --- a/example/ci/single_account.json +++ b/example/ci/single_account.json @@ -1,21 +1,21 @@ { "steps": [ { - "api": "ChatManager.fetchConversationsFromServerWithCursor", - "id": "chat.fetchConversationsFromServerWithCursor", - "params": { "pageSize": 20 }, + "api": "ChatManager.getAllConversations", + "id": "chat.getAllConversations", + "params": {}, "expect": { "success": true } }, { - "api": "ChatContactManager.fetchAllContacts", - "id": "contact.fetchAllContacts", + "api": "ChatContactManager.getAllContacts", + "id": "contact.getAllContacts", "params": {}, "expect": { "success": true } }, { - "api": "ChatGroupManager.fetchJoinedGroupsFromServer", - "id": "group.fetchJoinedGroupsFromServer", - "params": { "pageSize": 20, "pageNum": 0 }, + "api": "ChatGroupManager.getJoinedGroups", + "id": "group.getJoinedGroups", + "params": {}, "expect": { "success": true } }, { diff --git a/example/ios/ChatSdkExample.xcodeproj/project.pbxproj b/example/ios/ChatSdkExample.xcodeproj/project.pbxproj index 9cc39c0c..2574fc6b 100644 --- a/example/ios/ChatSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ChatSdkExample.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 0C80B921A6F3F58F76C31292 /* libPods-ChatSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ChatSdkExample.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; D6901EB1901A6525BE0711B2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; + E89D7A5D784D7B99C0AC7904 /* libPods-ChatSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7560D25548704F71D4FD7912 /* libPods-ChatSdkExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -21,7 +21,7 @@ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ChatSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-ChatSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ChatSdkExample/Pods-ChatSdkExample.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-ChatSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ChatSdkExample/Pods-ChatSdkExample.release.xcconfig"; sourceTree = ""; }; - 5DCACB8F33CDC322A6C60F78 /* libPods-ChatSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ChatSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7560D25548704F71D4FD7912 /* libPods-ChatSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ChatSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ChatSdkExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ChatSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -32,7 +32,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0C80B921A6F3F58F76C31292 /* libPods-ChatSdkExample.a in Frameworks */, + E89D7A5D784D7B99C0AC7904 /* libPods-ChatSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -55,7 +55,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 5DCACB8F33CDC322A6C60F78 /* libPods-ChatSdkExample.a */, + 7560D25548704F71D4FD7912 /* libPods-ChatSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -267,7 +267,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.21.0; + MARKETING_VERSION = 5.0.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -294,7 +294,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.21.0; + MARKETING_VERSION = 5.0.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -356,6 +356,19 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -426,6 +439,19 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-runtimeexecutor/React_runtimeexecutor.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 01103fd1..17fd00ac 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,13 +1,13 @@ PODS: - boost (1.84.0) - - ChatSdk (1.21.0): + - ChatSdk (5.0.0): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine - - HyphenateChat (~> 4.25.0) + - HyphenateChat (~> 5.0.0) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -37,7 +37,7 @@ PODS: - hermes-engine (0.14.1): - hermes-engine/Pre-built (= 0.14.1) - hermes-engine/Pre-built (0.14.1) - - HyphenateChat (4.25.0): + - HyphenateChat (5.0.0): - ShengwangInfra_iOS (~> 1.3.0) - RCT-Folly (2024.11.18.00): - boost @@ -2792,7 +2792,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ShengwangInfra_iOS (1.3.16) + - ShengwangInfra_iOS (1.3.5) - SocketRocket (0.7.1) - Yoga (0.0.0) @@ -3069,14 +3069,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - ChatSdk: a25bd2b60c00eb6427c7832ad4a5aa10db6ba528 + ChatSdk: 54edfd073d3846f43863699364cc48835d7cebf4 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 07433c4ab85da44d0b5ceac374ff4e1d57c7aa23 fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac glog: 0456694f3aaf09460b660ea327cfc11defbeea4c hermes-engine: f37e1d6f1eb198649a60b3f17b6f685a6ac12470 - HyphenateChat: ea10cee23c4855b4cc6b8cf95d15a2f54a39fb9b + HyphenateChat: 32bb7305bfb333d3902ef9497b6892470bf1fa61 RCT-Folly: b29feb752b08042c62badaef7d453f3bb5e6ae23 RCTDeprecation: f05668eea7b0e209e77e8ab1dc6a5e3b342f6f65 RCTRequired: c1ddbcb9ab4d51c36bda2a96556d064cb8825357 @@ -3154,10 +3154,10 @@ SPEC CHECKSUMS: RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 RNGestureHandler: dc3a8533b9b2f093cc469b9c253bbb993b0df24d RNScreens: 40460e1c9a818613c7004427f953005971f314fc - ShengwangInfra_iOS: b72ce9eb087e6056e3f2bb8c4869c1f335a09267 + ShengwangInfra_iOS: 0ae2d429ec428cdbaebb39ac883ec64c5c20f7bb SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: d2ed98551adb569fc38be0148666854e1ff60692 PODFILE CHECKSUM: 183fb0b8a101b0dcc63804542364171c2ea7d5b2 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/package.json b/example/package.json index b7ab2df3..235b1c7b 100644 --- a/example/package.json +++ b/example/package.json @@ -1,6 +1,6 @@ { "name": "react-native-chat-sdk-example", - "version": "1.21.0", + "version": "5.0.0", "private": true, "scripts": { "android": "react-native run-android", diff --git a/example/src/auto/auto_mode.ts b/example/src/auto/auto_mode.ts index 4980dcaa..a2b66a70 100644 --- a/example/src/auto/auto_mode.ts +++ b/example/src/auto/auto_mode.ts @@ -18,14 +18,15 @@ import { navigateReplace } from '../navigation'; * 中的自定义 babel 插件在打包时内联(@react-native/babel-preset 0.83 不再内置 * inline-environment-variables)。未传时内联为 `undefined`,即人工模式。 * - * 数据源:默认取打包进来的 `src/env.ts`(scripts/generate-env.js 生成, + * 数据源:默认取打包进来的 `src/env.ts`(占位模板由 scripts/generate-env.js 生成, + * 真实凭据由 scripts/env-gettoken.js 按 config.local.json 直接生成 env.ts; * 与人工模式同一份数据);打包期传入 `API_CONFIG=` 可整体覆盖。 * * 脚本格式: * ```json * { * "init": { "appKey": "..." }, // 可选,缺省由 env.ts 推导 - * "login": { "userId": "...", "password": "..." }, // 可选,缺省由 env.ts 推导 + * "login": { "userId": "...", "token": "..." }, // 可选,缺省由 env.ts 推导;显式 false 表示跳过登录(保持未登录态) * "steps": [ * { "api": "ChatManager.sendMessage", "id": "m1", * "params": { "targetId": "$config.accounts.0.id", "content": "$prev.body.content" }, @@ -53,7 +54,7 @@ interface ScriptStep { interface Script { init?: Record; - login?: { userId?: string; password?: string; token?: string }; + login?: { userId?: string; token?: string } | false; steps?: ScriptStep[]; } @@ -158,11 +159,9 @@ function resolveValue(value: unknown, ctx: RefContext): unknown { const CHAT_OPTIONS_KEYS = [ 'appKey', 'appId', - 'autoLogin', 'debugModel', 'acceptInvitationAlways', 'autoAcceptGroupInvitation', - 'requireAck', 'requireDeliveryAck', 'deleteMessagesAsExitGroup', 'deleteMessagesAsExitChatRoom', @@ -171,7 +170,8 @@ const CHAT_OPTIONS_KEYS = [ 'usingHttpsOnly', 'serverTransfer', 'isAutoDownload', - 'pushConfig', + 'apnsCertName', + 'pushKitCertName', 'areaCode', 'logTag', 'logTimestamp', @@ -194,11 +194,7 @@ const CHAT_OPTIONS_KEYS = [ 'webSocketPort', 'dohVendor', 'enableUserInfo', - 'enableAutoSyncContacts', - 'enableChatroomConversation', - 'autoLoadConversations', - 'apnsCertName', - 'pushKitCertName', + 'dataSyncType', ]; function deriveInitParams( @@ -223,21 +219,23 @@ function deriveInitParams( delete picked[key]; } } - // auto-mode 一律关闭 autoLogin(ChatOptions 默认 true): - // 避免设备上残留的持久化会话被自动登录——login 步骤必须真实走凭据登录, + // native 5.0 已无自动登录:login 步骤必须真实走凭据登录, // no-login 冒烟也必须保持未登录态 - picked.autoLogin = false; return picked.appKey != null || picked.appId != null ? picked : null; } function deriveLoginParams( script: Script, config: Record -): { userId: string; password?: string; token?: string } | null { +): { userId: string; token?: string } | null { + // 显式 "login": false:脚本要求保持未登录态(如 no-login 冒烟), + // 不做 env.ts 账号回退,否则本地 env.ts 有账号时会误登录 + if (script.login === false) { + return null; + } if (script.login != null && script.login.userId != null) { return { userId: String(script.login.userId), - password: script.login.password, token: script.login.token, }; } @@ -246,14 +244,10 @@ function deriveLoginParams( if (typeof userId !== 'string' || userId.length === 0) { return null; } - const password = config.loginPassword ?? account?.mm; - const token = config.loginToken; + // 5.0 起无密码登录:账号的 mm 字段与 loginToken 均为 token + const token = config.loginToken ?? account?.mm; return { userId, - password: - typeof password === 'string' && password.length > 0 - ? password - : undefined, token: typeof token === 'string' && token.length > 0 ? token : undefined, }; } @@ -263,9 +257,9 @@ async function runInit( hooks: AutoModeHooks ): Promise { const optionsJson = JSON.stringify(initParams, null, 2); - const options = new ChatOptions( - initParams as { appKey: string; appId: string } - ); + const options = initParams.appKey + ? ChatOptions.withAppKey(initParams as unknown as { appKey: string }) + : ChatOptions.withAppId(initParams as unknown as { appId: string }); await ChatClient.getInstance().init(options); addLog('api.ChatClient.init', okResult()); registerAllListeners(); @@ -274,24 +268,19 @@ async function runInit( } async function runLogin( - loginParams: { userId: string; password?: string; token?: string }, + loginParams: { userId: string; token?: string }, hooks: AutoModeHooks ): Promise { // login 失败自动重试至多 5 次(间隔 1s):native init 返回后 SDK 内部可能尚未就绪 let lastError: unknown = null; for (let attempt = 1; attempt <= LOGIN_MAX_ATTEMPTS; attempt++) { try { - const isPassword = loginParams.password != null; - const pwdOrToken = isPassword - ? String(loginParams.password) - : String(loginParams.token ?? ''); - await ChatClient.getInstance().login( + await ChatClient.getInstance().loginWithToken( loginParams.userId, - pwdOrToken, - isPassword + String(loginParams.token ?? '') ); addLog( - 'api.ChatClient.login', + 'api.ChatClient.loginWithToken', okResult({ userId: loginParams.userId, attempt }) ); hooks.markLoggedIn(loginParams.userId); @@ -299,7 +288,7 @@ async function runLogin( return; } catch (e) { lastError = e; - addLog('api.ChatClient.login', { + addLog('api.ChatClient.loginWithToken', { ...errResult(e), attempt, maxAttempts: LOGIN_MAX_ATTEMPTS, diff --git a/example/src/listeners.ts b/example/src/listeners.ts index c6e706c5..960794ff 100644 --- a/example/src/listeners.ts +++ b/example/src/listeners.ts @@ -33,29 +33,21 @@ const CONNECT_METHODS = [ 'onDisconnected', 'onTokenWillExpire', 'onTokenDidExpire', - 'onAppActiveNumberReachLimit', 'onOfflineMessageSyncStart', 'onOfflineMessageSyncFinish', - 'onUserDidLoginFromOtherDevice', - 'onUserDidLoginFromOtherDeviceWithInfo', - 'onUserDidRemoveFromServer', - 'onUserDidForbidByServer', - 'onUserDidChangePassword', - 'onUserDidLoginTooManyDevice', - 'onUserKickedByOtherDevice', - 'onUserAuthenticationFailed', + 'onDataSyncStart', + 'onDataSyncFinish', + 'onDatabaseOpened', ]; const MESSAGE_METHODS = [ 'onMessagesReceived', 'onStreamMessagesReceived', 'onCmdMessagesReceived', - 'onMessagesRead', - 'onGroupMessageRead', + 'onMessageReadReceipts', 'onMessagesDelivered', 'onMessagesRecalledInfo', 'onConversationsUpdate', - 'onConversationRead', 'onMessageReactionDidChange', 'onChatMessageThreadCreated', 'onChatMessageThreadUpdated', @@ -80,9 +72,7 @@ const GROUP_METHODS = [ 'onAdminAdded', 'onAdminRemoved', 'onOwnerChanged', - 'onMemberJoined', 'onMembersJoined', - 'onMemberExited', 'onMembersExited', 'onAnnouncementChanged', 'onSharedFileAdded', @@ -102,8 +92,6 @@ const CONTACT_METHODS = [ 'onContactInvited', 'onFriendRequestAccepted', 'onFriendRequestDeclined', - 'onContactSyncStart', - 'onContactSyncFinish', 'onContactInfoUpdate', ]; diff --git a/example/src/pages/InitPage.tsx b/example/src/pages/InitPage.tsx index da3a6271..e20e9be0 100644 --- a/example/src/pages/InitPage.tsx +++ b/example/src/pages/InitPage.tsx @@ -24,7 +24,6 @@ import { appKey } from '../env'; function buildDefaultTemplate(): string { const template: Record = { appKey: appKey[0] ?? '', - autoLogin: false, debugModel: true, }; // const id = appId[0]; @@ -52,7 +51,9 @@ export function InitPage({ navigation }: Props) { setRunning(true); try { const parsed = JSON.parse(json); - const options = new ChatOptions(parsed); + const options = parsed.appKey + ? ChatOptions.withAppKey(parsed) + : ChatOptions.withAppId(parsed); await ChatClient.getInstance().init(options); const result = okResult(); addLog('api.ChatClient.init', result); diff --git a/example/src/pages/LoginPage.tsx b/example/src/pages/LoginPage.tsx index 2548db8e..34dd6c3c 100644 --- a/example/src/pages/LoginPage.tsx +++ b/example/src/pages/LoginPage.tsx @@ -2,7 +2,6 @@ import { useState } from 'react'; import { ScrollView, StyleSheet, - Switch, Text, TextInput, TouchableOpacity, @@ -21,25 +20,24 @@ type Props = NativeStackScreenProps; export function LoginPage({ navigation }: Props) { const sdk = useSdkState(); - const [isPassword, setIsPassword] = useState(true); // 预填自 env.ts(scripts/generate-env.js 生成),可手改 const [userId, setUserId] = useState(accounts[0]?.id ?? ''); - const [pwdOrToken, setPwdOrToken] = useState(accounts[0]?.mm ?? ''); + const [token, setToken] = useState(accounts[0]?.mm ?? ''); const [resultText, setResultText] = useState(null); const [running, setRunning] = useState(false); const onLogin = async () => { setRunning(true); try { - await ChatClient.getInstance().login(userId, pwdOrToken, isPassword); + await ChatClient.getInstance().loginWithToken(userId, token); const result = okResult({ userId }); - addLog('api.ChatClient.login', result); + addLog('api.ChatClient.loginWithToken', result); setResultText(JSON.stringify(result, null, 2)); sdk.markLoggedIn(userId); navigation.push('Search'); } catch (e) { const result = errResult(e); - addLog('api.ChatClient.login', result); + addLog('api.ChatClient.loginWithToken', result); setResultText(JSON.stringify(result, null, 2)); } finally { setRunning(false); @@ -79,13 +77,6 @@ export function LoginPage({ navigation }: Props) { {sdk.loggedIn ? `已登录:${sdk.currentUser ?? ''}` : '未登录'} - - - {isPassword ? '密码登录' : 'token 登录'} - - - - diff --git a/example/src/registry/apis/chat.ts b/example/src/registry/apis/chat.ts index 8580e7c3..45b4a7bc 100644 --- a/example/src/registry/apis/chat.ts +++ b/example/src/registry/apis/chat.ts @@ -4,6 +4,7 @@ import { ChatMessage, ChatMessageChatType, ChatMessageSearchOption, + ChatTextMessageBody, ChatVoiceParam, } from 'react-native-chat-sdk'; import type { ApiEntry } from '../api_entry'; @@ -31,29 +32,77 @@ function sendCallbacks(tag: string) { }; } +/** 等服务端发送回调后再返回,确保后续步骤拿到可从本地库查回的消息 ID。 */ +function sendMessageAndWait(message: ChatMessage): Promise { + return new Promise((resolve, reject) => { + let settled = false; + ChatClient.getInstance() + .chatManager.sendMessage(message, { + onProgress: (localMsgId: string, progress: number) => { + addLog('ChatManager.sendMessage.onProgress', { + localMsgId, + progress, + }); + }, + onError: ( + localMsgId: string, + error: { code: number; description: string } + ) => { + addLog('ChatManager.sendMessage.onError', { + localMsgId, + code: error.code, + description: error.description, + }); + if (!settled) { + settled = true; + reject(error); + } + }, + onSuccess: (sentMessage: ChatMessage) => { + addLog('ChatManager.sendMessage.onSuccess', { + msgId: sentMessage.msgId, + }); + if (!settled) { + settled = true; + resolve(sentMessage); + } + }, + }) + .catch((error) => { + if (!settled) { + settled = true; + reject(error); + } + }); + }); +} + export const chatApis: ApiEntry[] = [ { - name: 'ChatManager.fetchConversationsFromServerWithCursor', + name: 'ChatManager.getAllConversations', group: 'ChatManager', - description: '从服务器分页获取会话列表。cursor/pageSize 均可选。', - paramsTemplate: JSON.stringify({ pageSize: 20 }, null, 2), - invoke: async (params) => { - return ChatClient.getInstance().chatManager.fetchConversationsFromServerWithCursor( - params.cursor === undefined ? undefined : String(params.cursor), - params.pageSize === undefined ? undefined : Number(params.pageSize) - ); + description: + '获取全部会话列表(本地数据,服务端拉取接口已在 5.0 移除)。无参数。', + paramsTemplate: '{}', + invoke: async () => { + return ChatClient.getInstance().chatManager.getAllConversations(); }, }, { - name: 'ChatManager.fetchConversationsFromDB', + name: 'ChatManager.deleteConversations', group: 'ChatManager', description: - '从本地数据库分页获取会话列表。cursor/pageSize 均可选。需先将 ChatOptions.autoLoadConversations 设为 false。', - paramsTemplate: JSON.stringify({ pageSize: 20 }, null, 2), + '【5.0.0 新增】批量删除本地会话,可选是否同时删除会话内消息。' + + 'convIds:会话 ID 列表(不存在的 ID 会被忽略);deleteMessages:默认 true。', + paramsTemplate: JSON.stringify( + { convIds: ['ID'], deleteMessages: true }, + null, + 2 + ), invoke: async (params) => { - return ChatClient.getInstance().chatManager.fetchConversationsFromDB( - params.cursor === undefined ? undefined : String(params.cursor), - params.pageSize === undefined ? undefined : Number(params.pageSize) + return ChatClient.getInstance().chatManager.deleteConversations( + (params.convIds ?? []).map(String), + params.deleteMessages ?? true ); }, }, @@ -62,6 +111,7 @@ export const chatApis: ApiEntry[] = [ group: 'ChatManager', description: '发送文本消息。chatType:0 单聊 / 1 群聊 / 2 聊天室。' + + 'isNeedReadReceipt 可选:群消息已读回执开关(验证 sendMessageReadReceipts 时开启)。' + '返回已发送消息 JSON(含 msgId、localTime),发送进度/成败回调见悬浮日志。' + 'round-trip:返回的消息 JSON 可作为后续消息类 API 的输入。', paramsTemplate: JSON.stringify( @@ -70,6 +120,7 @@ export const chatApis: ApiEntry[] = [ content: 'hello', chatType: 0, webhookEnv: '', + isNeedReadReceipt: false, }, null, 2 @@ -83,11 +134,181 @@ export const chatApis: ApiEntry[] = [ if (params.webhookEnv !== undefined && params.webhookEnv !== '') { msg.webhookEnv = String(params.webhookEnv); } - await ChatClient.getInstance().chatManager.sendMessage( - msg, - sendCallbacks('ChatManager.sendMessage') + if (params.isNeedReadReceipt === true) { + msg.isNeedReadReceipt = true; + } + return sendMessageAndWait(msg); + }, + }, + { + name: 'ChatManager.getConvMsgsWithKeyword', + group: 'ChatManager', + description: + '按关键字从本地库查询指定会话的消息(ChatConversation.getMsgsWithKeyword 的底层入口)。' + + 'convId:会话 ID;convType:0 单聊 / 1 群聊 / 2 聊天室;' + + 'senders 可选:发送者 ID 列表,省略则不过滤发送者。', + paramsTemplate: JSON.stringify( + { + convId: 'ID', + convType: 0, + keywords: 'hello', + timestamp: -1, + count: 50, + direction: 0, + searchScope: 2, + senders: ['ID'], + }, + null, + 2 + ), + invoke: async (params) => { + const ret = + await ChatClient.getInstance().chatManager.getConvMsgsWithKeyword({ + convId: String(params.convId), + convType: (params.convType ?? + ChatConversationType.PeerChat) as ChatConversationType, + keywords: String(params.keywords), + timestamp: params.timestamp as number | undefined, + count: params.count as number | undefined, + direction: params.direction as number | undefined, + senders: params.senders as string[] | undefined, + searchScope: params.searchScope as number | undefined, + }); + return ret.map((msg) => ({ msgId: msg.msgId, from: msg.from })); + }, + }, + { + name: 'ChatManager.getMsgs', + group: 'ChatManager', + description: + '从本地库加载指定会话的消息(无关键字过滤,用于对照验证)。' + + 'convId:会话 ID;convType:0 单聊 / 1 群聊 / 2 聊天室;startMsgId 传空串则从最新开始。', + paramsTemplate: JSON.stringify( + { + convId: 'ID', + convType: 0, + startMsgId: '', + loadCount: 20, + direction: 0, + }, + null, + 2 + ), + invoke: async (params) => { + const ret = await ChatClient.getInstance().chatManager.getMsgs({ + convId: String(params.convId), + convType: (params.convType ?? + ChatConversationType.PeerChat) as ChatConversationType, + startMsgId: String(params.startMsgId ?? ''), + loadCount: params.loadCount as number | undefined, + direction: params.direction as number | undefined, + }); + return ret.map((msg) => ({ msgId: msg.msgId, from: msg.from })); + }, + }, + { + name: 'ChatManager.sendMessageReadReceipts', + group: 'ChatManager', + description: + '【5.0.0 新增】发送消息已读回执(替代已删除的 sendMessageReadAck)。' + + 'msgIds:消息 ID 列表,最多 50 条且须属于同一会话;' + + '仅对 isNeedReadReceipt=true 的消息生效。', + paramsTemplate: JSON.stringify({ msgIds: ['ID'] }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().chatManager.sendMessageReadReceipts( + (params.msgIds ?? []).map(String) ); - return msg; + }, + }, + { + name: 'ChatManager.clearConversationUnreadMessageCount', + group: 'ChatManager', + description: + '【5.0.0 新增】清空指定会话的本地未读数并同步多设备(不发已读回执)。' + + 'convId:会话 ID。', + paramsTemplate: JSON.stringify({ convId: 'ID' }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().chatManager.clearConversationUnreadMessageCount( + String(params.convId) + ); + }, + }, + { + name: 'ChatManager.clearAllConversationUnreadMessageCount', + group: 'ChatManager', + description: + '【5.0.0 新增】清空所有会话的本地未读数并同步多设备(不发已读回执)。无参数。', + paramsTemplate: '{}', + invoke: async () => { + return ChatClient.getInstance().chatManager.clearAllConversationUnreadMessageCount(); + }, + }, + { + name: 'ChatManager.getGroupMessageReadReceipts', + group: 'ChatManager', + description: + '【5.0.0 新增】从本地获取群消息已读回执。' + + 'msgIds:消息 ID 列表,最多 20 条且须属于同一会话。' + + '返回 ChatMessageReadReceipt 列表。', + paramsTemplate: JSON.stringify({ msgIds: ['ID'] }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().chatManager.getGroupMessageReadReceipts( + (params.msgIds ?? []).map(String) + ); + }, + }, + { + name: 'ChatManager.fetchGroupMessageReadReceipts', + group: 'ChatManager', + description: + '【5.0.0 新增】分页从服务器获取群消息已读回执(替代已删除的 fetchGroupAcks)。' + + 'msgId:消息 ID;groupId:群 ID;cursor/pageSize 可选。' + + '返回 {cursor, list, totalCount?}(totalCount 仅 iOS)。', + paramsTemplate: JSON.stringify( + { msgId: 'ID', groupId: 'ID', cursor: '', pageSize: 20 }, + null, + 2 + ), + invoke: async (params) => { + return ChatClient.getInstance().chatManager.fetchGroupMessageReadReceipts( + String(params.msgId), + String(params.groupId), + params.cursor === undefined ? undefined : String(params.cursor), + params.pageSize === undefined ? undefined : Number(params.pageSize) + ); + }, + }, + { + name: 'ChatManager.modifyMsgBody', + group: 'ChatManager', + description: + '修改消息(5.0.0 起支持同时修改 ext 扩展)。msgId:消息 ID;' + + 'content 可选:新的文本内容(仅文本/自定义消息可改 body);' + + 'ext 可选:新的扩展(整体覆盖)。返回修改后的消息 JSON。', + paramsTemplate: JSON.stringify( + { msgId: 'ID', content: 'modified', ext: { k: 'v' } }, + null, + 2 + ), + invoke: async (params) => { + return ChatClient.getInstance().chatManager.modifyMsgBody({ + msgId: String(params.msgId), + body: + params.content === undefined + ? undefined + : new ChatTextMessageBody({ content: String(params.content) }), + ext: params.ext, + }); + }, + }, + { + name: 'ChatManager.getUnreadCount', + group: 'ChatManager', + description: + '获取所有会话未读数总和(5.0.0 起不再统计聊天室与免打扰会话)。无参数。返回数字。', + paramsTemplate: '{}', + invoke: async () => { + return ChatClient.getInstance().chatManager.getUnreadCount(); }, }, { diff --git a/example/src/registry/apis/client.ts b/example/src/registry/apis/client.ts index 585e6d2a..7a7eb30f 100644 --- a/example/src/registry/apis/client.ts +++ b/example/src/registry/apis/client.ts @@ -1,7 +1,92 @@ import { ChatClient } from 'react-native-chat-sdk'; import type { ApiEntry } from '../api_entry'; +/** + * ChatClient 级 API(init/login/logout 为页面专用,不在此注册)。 + * 设备管理三方法自 5.0.0 起改为 token 鉴权,token 用 `$config.accounts.0.mm` 引用。 + */ export const clientApis: ApiEntry[] = [ + { + name: 'ChatClient.getLoggedInDevicesFromServer', + group: 'ChatClient', + description: + '获取指定账号的在线登录设备列表(5.0.0 起 token 鉴权)。' + + 'userId:用户 ID;token:用户 token。返回设备列表(resource/deviceUUID/deviceName)。', + paramsTemplate: JSON.stringify({ userId: 'ID', token: 'TOKEN' }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().getLoggedInDevicesFromServer( + String(params.userId), + String(params.token) + ); + }, + }, + { + name: 'ChatClient.kickDevice', + group: 'ChatClient', + description: + '将指定账号在指定设备上踢下线(5.0.0 起 token 鉴权)。' + + 'resource:设备 ID,取自 getLoggedInDevicesFromServer 返回的 resource。' + + '注意:踢当前设备会导致本端登出。', + paramsTemplate: JSON.stringify( + { userId: 'ID', token: 'TOKEN', resource: 'RESOURCE' }, + null, + 2 + ), + invoke: async (params) => { + return ChatClient.getInstance().kickDevice( + String(params.userId), + String(params.token), + String(params.resource) + ); + }, + }, + { + name: 'ChatClient.kickAllDevices', + group: 'ChatClient', + description: + '将指定账号在所有设备上踢下线(5.0.0 起 token 鉴权)。' + + '注意:包含当前设备,调用后本端登出,脚本中应作为最后一步。', + paramsTemplate: JSON.stringify({ userId: 'ID', token: 'TOKEN' }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().kickAllDevices( + String(params.userId), + String(params.token) + ); + }, + }, + { + name: 'ChatClient.renewToken', + group: 'ChatClient', + description: + '更新用户 token(5.0.0 由 renewAgoraToken 改名)。token:新的用户 token。', + paramsTemplate: JSON.stringify({ token: 'TOKEN' }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().renewToken(String(params.token)); + }, + }, + { + name: 'ChatClient.bindDeviceToken', + group: 'ChatClient', + description: + '绑定设备的推送 token(双端公用,5.0.0 取代 updatePushConfig)。' + + 'deviceToken:APNs 或厂商推送 SDK 返回的 token;' + + 'notifierName:Android 必填的厂商推送凭据(如 FCM Sender ID),iOS 忽略该参数。' + + 'iOS 的 APNs 证书名请在 ChatOptions.apnsCertName 中配置。', + paramsTemplate: JSON.stringify( + { deviceToken: 'DEVICE_TOKEN', notifierName: 'NOTIFIER_NAME' }, + null, + 2 + ), + invoke: async (params) => { + return ChatClient.getInstance().bindDeviceToken({ + deviceToken: String(params.deviceToken), + notifierName: + params.notifierName === undefined + ? undefined + : String(params.notifierName), + }); + }, + }, { name: 'ChatClient.bindPushKitToken', group: 'ChatClient', diff --git a/example/src/registry/apis/contact.ts b/example/src/registry/apis/contact.ts index 8b8da955..fc500d66 100644 --- a/example/src/registry/apis/contact.ts +++ b/example/src/registry/apis/contact.ts @@ -3,12 +3,25 @@ import type { ApiEntry } from '../api_entry'; export const contactApis: ApiEntry[] = [ { - name: 'ChatContactManager.fetchAllContacts', + name: 'ChatContactManager.getAllContacts', group: 'ChatContactManager', - description: '从服务器获取全部联系人列表。无参数。', + description: '获取全部联系人列表(本地数据)。无参数。', paramsTemplate: '{}', invoke: async () => { - return ChatClient.getInstance().contactManager.fetchAllContacts(); + return ChatClient.getInstance().contactManager.getAllContacts(); + }, + }, + { + name: 'ChatContactManager.addContact', + group: 'ChatContactManager', + description: + '添加好友(服务端接口,未登录返回 201)。userId:用户 ID;reason 可选,申请理由。', + paramsTemplate: JSON.stringify({ userId: 'ID', reason: 'reason' }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().contactManager.addContact( + String(params.userId), + params.reason === undefined ? '' : String(params.reason) + ); }, }, ]; diff --git a/example/src/registry/apis/group.ts b/example/src/registry/apis/group.ts index e107204a..7b704194 100644 --- a/example/src/registry/apis/group.ts +++ b/example/src/registry/apis/group.ts @@ -3,17 +3,100 @@ import type { ApiEntry } from '../api_entry'; export const groupApis: ApiEntry[] = [ { - name: 'ChatGroupManager.fetchJoinedGroupsFromServer', + name: 'ChatGroupManager.createGroupEx', group: 'ChatGroupManager', - description: '从服务器分页获取当前用户加入的群组列表。', - paramsTemplate: JSON.stringify({ pageSize: 20, pageNum: 0 }, null, 2), + description: + '创建群组(5.0.0 起 options 参数改为 configs)。' + + 'configs:{maxCount, inviteNeedConfirm, ext, isPublic, joinApprovalRequired, allowInvites};' + + 'groupAvatar/desc/inviteMembers/inviteReason 可选。返回群 JSON(含 groupId)。', + paramsTemplate: JSON.stringify( + { + groupName: 'my group', + groupAvatar: '', + desc: 'group description', + inviteMembers: ['ID'], + inviteReason: 'welcome', + configs: { + maxCount: 200, + inviteNeedConfirm: false, + ext: '', + isPublic: false, + joinApprovalRequired: false, + allowInvites: true, + }, + }, + null, + 2 + ), invoke: async (params) => { - return ChatClient.getInstance().groupManager.fetchJoinedGroupsFromServer( - Number(params.pageSize), - Number(params.pageNum) + return ChatClient.getInstance().groupManager.createGroupEx({ + groupName: String(params.groupName), + groupAvatar: + params.groupAvatar === undefined || params.groupAvatar === '' + ? undefined + : String(params.groupAvatar), + desc: params.desc === undefined ? undefined : String(params.desc), + inviteMembers: (params.inviteMembers ?? []).map(String), + inviteReason: + params.inviteReason === undefined + ? undefined + : String(params.inviteReason), + configs: params.configs, + }); + }, + }, + { + name: 'ChatGroupManager.updateGroupConfigs', + group: 'ChatGroupManager', + description: + '【5.0.0 新增】按位掩码更新群配置(仅群主/管理员)。' + + 'types:ChatGroupConfigsType 掩码(AllowInvites=1, MaxUsers=2, InviteNeedConfirm=4, ' + + 'JoinApprovalRequired=8, IsPublic=16, Ext=32,可按位或组合);configs:新配置。返回群 JSON。', + paramsTemplate: JSON.stringify( + { + groupId: 'ID', + types: 2, + configs: { + maxCount: 300, + inviteNeedConfirm: false, + ext: '', + isPublic: false, + joinApprovalRequired: false, + allowInvites: true, + }, + }, + null, + 2 + ), + invoke: async (params) => { + return ChatClient.getInstance().groupManager.updateGroupConfigs( + String(params.groupId), + Number(params.types), + params.configs ); }, }, + { + name: 'ChatGroupManager.destroyGroup', + group: 'ChatGroupManager', + description: + '解散群组(仅群主)。groupId:群 ID。用于 createGroupEx 验证后的清理。', + paramsTemplate: JSON.stringify({ groupId: 'ID' }, null, 2), + invoke: async (params) => { + return ChatClient.getInstance().groupManager.destroyGroup( + String(params.groupId) + ); + }, + }, + { + name: 'ChatGroupManager.getJoinedGroups', + group: 'ChatGroupManager', + description: '获取当前用户加入的群组列表(本地数据)。无参数。', + paramsTemplate: '{}', + invoke: async () => { + return ChatClient.getInstance().groupManager.getJoinedGroups(); + }, + }, { name: 'ChatGroupManager.updateGroupNamecard', group: 'ChatGroupManager', diff --git a/example/src/registry/index.ts b/example/src/registry/index.ts index 93e16b78..16fc04df 100644 --- a/example/src/registry/index.ts +++ b/example/src/registry/index.ts @@ -1,6 +1,6 @@ import type { ApiEntry } from './api_entry'; -import { chatApis } from './apis/chat'; import { clientApis } from './apis/client'; +import { chatApis } from './apis/chat'; import { groupApis } from './apis/group'; import { contactApis } from './apis/contact'; import { userInfoApis } from './apis/userinfo'; @@ -13,8 +13,8 @@ import { pushApis } from './apis/push'; * 注意:ChatClient.init / login / logout 为页面专用,不在此注册。 */ export const ALL_APIS: ApiEntry[] = [ - ...chatApis, ...clientApis, + ...chatApis, ...groupApis, ...contactApis, ...userInfoApis, diff --git a/modules/cpp/common/ExtSdkMethodType.cpp b/modules/cpp/common/ExtSdkMethodType.cpp index 86ac18b7..7b85d923 100644 --- a/modules/cpp/common/ExtSdkMethodType.cpp +++ b/modules/cpp/common/ExtSdkMethodType.cpp @@ -8,19 +8,15 @@ EXT_SDK_NAMESPACE_BEGIN /// EMClient methods const std::string ExtSdkMethodType::init = "init"; -const std::string ExtSdkMethodType::createAccount = "createAccount"; const std::string ExtSdkMethodType::login = "login"; const std::string ExtSdkMethodType::logout = "logout"; const std::string ExtSdkMethodType::changeAppKey = "changeAppKey"; -const std::string ExtSdkMethodType::isLoggedInBefore = "isLoggedInBefore"; -const std::string ExtSdkMethodType::updateCurrentUserNick = "updateCurrentUserNick"; -const std::string ExtSdkMethodType::uploadLog = "uploadLog"; const std::string ExtSdkMethodType::compressLogs = "compressLogs"; const std::string ExtSdkMethodType::kickDevice = "kickDevice"; const std::string ExtSdkMethodType::kickAllDevices = "kickAllDevices"; const std::string ExtSdkMethodType::getLoggedInDevicesFromServer = "getLoggedInDevicesFromServer"; const std::string ExtSdkMethodType::getCurrentUser = "getCurrentUser"; -// 2026-09-24 1.21.0 +const std::string ExtSdkMethodType::bindDeviceToken = "bindDeviceToken"; const std::string ExtSdkMethodType::bindPushKitToken = "bindPushKitToken"; const std::string ExtSdkMethodType::unbindPushKitToken = "unbindPushKitToken"; @@ -28,11 +24,13 @@ const std::string ExtSdkMethodType::onConnected = "onConnected"; const std::string ExtSdkMethodType::onDisconnected = "onDisconnected"; const std::string ExtSdkMethodType::onMultiDeviceEvent = "onMultiDeviceEvent"; const std::string ExtSdkMethodType::onSendDataToFlutter = "onSendDataToFlutter"; +const std::string ExtSdkMethodType::onDataSyncStart = "onDataSyncStart"; +const std::string ExtSdkMethodType::onDataSyncFinish = "onDataSyncFinish"; +const std::string ExtSdkMethodType::onDatabaseOpened = "onDatabaseOpened"; /// EMContactManager methods const std::string ExtSdkMethodType::addContact = "addContact"; const std::string ExtSdkMethodType::deleteContact = "deleteContact"; -const std::string ExtSdkMethodType::getAllContactsFromServer = "getAllContactsFromServer"; const std::string ExtSdkMethodType::getAllContactsFromDB = "getAllContactsFromDB"; const std::string ExtSdkMethodType::addUserToBlockList = "addUserToBlockList"; const std::string ExtSdkMethodType::removeUserFromBlockList = "removeUserFromBlockList"; @@ -48,61 +46,49 @@ const std::string ExtSdkMethodType::onContactChanged = "onContactChanged"; /// EMChatManager methods const std::string ExtSdkMethodType::sendMessage = "sendMessage"; const std::string ExtSdkMethodType::resendMessage = "resendMessage"; -const std::string ExtSdkMethodType::ackMessageRead = "ackMessageRead"; -const std::string ExtSdkMethodType::ackGroupMessageRead = "ackGroupMessageRead"; -const std::string ExtSdkMethodType::ackConversationRead = "ackConversationRead"; +const std::string ExtSdkMethodType::sendMessageReadReceipts = "sendMessageReadReceipts"; +const std::string ExtSdkMethodType::clearConversationUnreadMessageCount = "clearConversationUnreadMessageCount"; +const std::string ExtSdkMethodType::clearAllConversationUnreadMessageCount = "clearAllConversationUnreadMessageCount"; +const std::string ExtSdkMethodType::getGroupMessageReadReceipts = "getGroupMessageReadReceipts"; +const std::string ExtSdkMethodType::fetchGroupMessageReadReceipts = "fetchGroupMessageReadReceipts"; const std::string ExtSdkMethodType::recallMessage = "recallMessage"; const std::string ExtSdkMethodType::getConversation = "getConversation"; -const std::string ExtSdkMethodType::markAllChatMsgAsRead = "markAllChatMsgAsRead"; const std::string ExtSdkMethodType::getUnreadMessageCount = "getUnreadMessageCount"; const std::string ExtSdkMethodType::updateChatMessage = "updateChatMessage"; const std::string ExtSdkMethodType::downloadAttachment = "downloadAttachment"; const std::string ExtSdkMethodType::downloadThumbnail = "downloadThumbnail"; const std::string ExtSdkMethodType::importMessages = "importMessages"; const std::string ExtSdkMethodType::loadAllConversations = "loadAllConversations"; -const std::string ExtSdkMethodType::getConversationsFromServer = "getConversationsFromServer"; const std::string ExtSdkMethodType::deleteConversation = "deleteConversation"; -const std::string ExtSdkMethodType::fetchHistoryMessages = "fetchHistoryMessages"; +const std::string ExtSdkMethodType::deleteConversations = "deleteConversations"; const std::string ExtSdkMethodType::searchChatMsgFromDB = "searchChatMsgFromDB"; const std::string ExtSdkMethodType::getMessage = "getMessage"; -const std::string ExtSdkMethodType::asyncFetchGroupAcks = "asyncFetchGroupAcks"; -const std::string ExtSdkMethodType::getConversationsFromDBWithCursor = "getConversationsFromDBWithCursor"; /// EMChatManager listener const std::string ExtSdkMethodType::onMessagesReceived = "onMessagesReceived"; const std::string ExtSdkMethodType::onCmdMessagesReceived = "onCmdMessagesReceived"; -const std::string ExtSdkMethodType::onMessagesRead = "onMessagesRead"; -const std::string ExtSdkMethodType::onGroupMessageRead = "onGroupMessageRead"; +const std::string ExtSdkMethodType::onMessageReadReceipts = "onMessageReadReceipts"; const std::string ExtSdkMethodType::onMessagesDelivered = "onMessagesDelivered"; -const std::string ExtSdkMethodType::onMessagesRecalled = "onMessagesRecalled"; +const std::string ExtSdkMethodType::onMessagesRecalledInfo = "onMessagesRecalledInfo"; +const std::string ExtSdkMethodType::onStreamMessagesReceived = "onStreamMessagesReceived"; const std::string ExtSdkMethodType::onConversationUpdate = "onConversationUpdate"; -const std::string ExtSdkMethodType::onConversationHasRead = "onConversationHasRead"; /// EMMessage listener const std::string ExtSdkMethodType::onMessageProgressUpdate = "onMessageProgressUpdate"; const std::string ExtSdkMethodType::onMessageError = "onMessageError"; const std::string ExtSdkMethodType::onMessageSuccess = "onMessageSuccess"; -const std::string ExtSdkMethodType::onMessageReadAck = "onMessageReadAck"; -const std::string ExtSdkMethodType::onMessageDeliveryAck = "onMessageDeliveryAck"; -const std::string ExtSdkMethodType::onMessageStatusChanged = "onMessageStatusChanged"; /// EMConversation const std::string ExtSdkMethodType::getUnreadMsgCount = "getUnreadMsgCount"; -const std::string ExtSdkMethodType::markAllMessagesAsRead = "markAllMessagesAsRead"; -const std::string ExtSdkMethodType::markMessageAsRead = "markMessageAsRead"; const std::string ExtSdkMethodType::syncConversationExt = "syncConversationExt"; -const std::string ExtSdkMethodType::syncConversationName = "syncConversationName"; const std::string ExtSdkMethodType::removeMessage = "removeMessage"; const std::string ExtSdkMethodType::getLatestMessage = "getLatestMessage"; const std::string ExtSdkMethodType::getLatestMessageFromOthers = "getLatestMessageFromOthers"; const std::string ExtSdkMethodType::clearAllMessages = "clearAllMessages"; const std::string ExtSdkMethodType::insertMessage = "insertMessage"; -const std::string ExtSdkMethodType::appendMessage = "appendMessage"; const std::string ExtSdkMethodType::updateConversationMessage = "updateConversationMessage"; -// 根据消息id获取消息 -const std::string ExtSdkMethodType::loadMsgWithId = "loadMsgWithId"; // 根据起始消息id获取消息 const std::string ExtSdkMethodType::loadMsgWithStartId = "loadMsgWithStartId"; // 根据关键字获取消息 @@ -118,9 +104,6 @@ const std::string ExtSdkMethodType::leaveChatRoom = "leaveChatRoom"; const std::string ExtSdkMethodType::fetchPublicChatRoomsFromServer = "fetchPublicChatRoomsFromServer"; const std::string ExtSdkMethodType::fetchChatRoomInfoFromServer = "fetchChatRoomInfoFromServer"; const std::string ExtSdkMethodType::getChatRoom = "getChatRoom"; -const std::string ExtSdkMethodType::getAllChatRooms = "getAllChatRooms"; -const std::string ExtSdkMethodType::createChatRoom = "createChatRoom"; -const std::string ExtSdkMethodType::destroyChatRoom = "destroyChatRoom"; const std::string ExtSdkMethodType::changeChatRoomSubject = "changeChatRoomSubject"; const std::string ExtSdkMethodType::changeChatRoomDescription = "changeChatRoomDescription"; const std::string ExtSdkMethodType::fetchChatRoomMembers = "fetchChatRoomMembers"; @@ -152,10 +135,8 @@ const std::string ExtSdkMethodType::chatRoomChange = "onChatRoomChanged"; /// EMGroupManager const std::string ExtSdkMethodType::getGroupWithId = "getGroupWithId"; const std::string ExtSdkMethodType::getJoinedGroups = "getJoinedGroups"; -const std::string ExtSdkMethodType::getGroupsWithoutPushNotification = "getGroupsWithoutPushNotification"; -const std::string ExtSdkMethodType::getJoinedGroupsFromServer = "getJoinedGroupsFromServer"; -const std::string ExtSdkMethodType::getPublicGroupsFromServer = "getPublicGroupsFromServer"; const std::string ExtSdkMethodType::createGroup = "createGroup"; +const std::string ExtSdkMethodType::updateGroupConfigs = "updateGroupConfigs"; const std::string ExtSdkMethodType::getGroupSpecificationFromServer = "getGroupSpecificationFromServer"; const std::string ExtSdkMethodType::getGroupMemberListFromServer = "getGroupMemberListFromServer"; const std::string ExtSdkMethodType::getGroupBlockListFromServer = "getGroupBlockListFromServer"; @@ -195,29 +176,20 @@ const std::string ExtSdkMethodType::acceptJoinApplication = "acceptJoinApplicati const std::string ExtSdkMethodType::declineJoinApplication = "declineJoinApplication"; const std::string ExtSdkMethodType::acceptInvitationFromGroup = "acceptInvitationFromGroup"; const std::string ExtSdkMethodType::declineInvitationFromGroup = "declineInvitationFromGroup"; -const std::string ExtSdkMethodType::ignoreGroupPush = "ignoreGroupPush"; /// EMGroupManagerListener const std::string ExtSdkMethodType::onGroupChanged = "onGroupChanged"; /// EMPushManager -const std::string ExtSdkMethodType::getImPushConfig = "getImPushConfig"; const std::string ExtSdkMethodType::getImPushConfigFromServer = "getImPushConfigFromServer"; const std::string ExtSdkMethodType::updatePushNickname = "updatePushNickname"; -const std::string ExtSdkMethodType::updateHMSPushToken = "updateHMSPushToken"; -const std::string ExtSdkMethodType::updateFCMPushToken = "updateFCMPushToken"; /// ImPushConfig -const std::string ExtSdkMethodType::imPushNoDisturb = "imPushNoDisturb"; const std::string ExtSdkMethodType::updateImPushStyle = "updateImPushStyle"; -const std::string ExtSdkMethodType::updateGroupPushService = "updateGroupPushService"; -const std::string ExtSdkMethodType::getNoDisturbGroups = "getNoDisturbGroups"; /// EMUserInfoManager const std::string ExtSdkMethodType::updateOwnUserInfo = "updateOwnUserInfo"; -const std::string ExtSdkMethodType::updateOwnUserInfoWithType = "updateOwnUserInfoWithType"; const std::string ExtSdkMethodType::fetchUserInfoById = "fetchUserInfoById"; -const std::string ExtSdkMethodType::fetchUserInfoByIdWithType = "fetchUserInfoByIdWithType"; EXT_SDK_NAMESPACE_END \ No newline at end of file diff --git a/modules/cpp/common/ExtSdkMethodType.h b/modules/cpp/common/ExtSdkMethodType.h index 19de370f..c3566e66 100644 --- a/modules/cpp/common/ExtSdkMethodType.h +++ b/modules/cpp/common/ExtSdkMethodType.h @@ -14,19 +14,15 @@ class ExtSdkMethodType { public: /// EMClient methods static const std::string init; - static const std::string createAccount; static const std::string login; static const std::string logout; static const std::string changeAppKey; - static const std::string isLoggedInBefore; - static const std::string updateCurrentUserNick; - static const std::string uploadLog; static const std::string compressLogs; static const std::string kickDevice; static const std::string kickAllDevices; static const std::string getLoggedInDevicesFromServer; static const std::string getCurrentUser; - // 2026-09-24 1.21.0 + static const std::string bindDeviceToken; static const std::string bindPushKitToken; static const std::string unbindPushKitToken; @@ -34,11 +30,13 @@ class ExtSdkMethodType { static const std::string onDisconnected; static const std::string onMultiDeviceEvent; static const std::string onSendDataToFlutter; + static const std::string onDataSyncStart; + static const std::string onDataSyncFinish; + static const std::string onDatabaseOpened; /// EMContactManager methods static const std::string addContact; static const std::string deleteContact; - static const std::string getAllContactsFromServer; static const std::string getAllContactsFromDB; static const std::string addUserToBlockList; static const std::string removeUserFromBlockList; @@ -54,61 +52,49 @@ class ExtSdkMethodType { /// EMChatManager methods static const std::string sendMessage; static const std::string resendMessage; - static const std::string ackMessageRead; - static const std::string ackGroupMessageRead; - static const std::string ackConversationRead; + static const std::string sendMessageReadReceipts; + static const std::string clearConversationUnreadMessageCount; + static const std::string clearAllConversationUnreadMessageCount; + static const std::string getGroupMessageReadReceipts; + static const std::string fetchGroupMessageReadReceipts; static const std::string recallMessage; static const std::string getConversation; - static const std::string markAllChatMsgAsRead; static const std::string getUnreadMessageCount; static const std::string updateChatMessage; static const std::string downloadAttachment; static const std::string downloadThumbnail; static const std::string importMessages; static const std::string loadAllConversations; - static const std::string getConversationsFromServer; static const std::string deleteConversation; - static const std::string fetchHistoryMessages; + static const std::string deleteConversations; static const std::string searchChatMsgFromDB; static const std::string getMessage; - static const std::string asyncFetchGroupAcks; - static const std::string getConversationsFromDBWithCursor; /// EMChatManager listener static const std::string onMessagesReceived; static const std::string onCmdMessagesReceived; - static const std::string onMessagesRead; - static const std::string onGroupMessageRead; + static const std::string onMessageReadReceipts; static const std::string onMessagesDelivered; - static const std::string onMessagesRecalled; + static const std::string onMessagesRecalledInfo; + static const std::string onStreamMessagesReceived; static const std::string onConversationUpdate; - static const std::string onConversationHasRead; /// EMMessage listener static const std::string onMessageProgressUpdate; static const std::string onMessageError; static const std::string onMessageSuccess; - static const std::string onMessageReadAck; - static const std::string onMessageDeliveryAck; - static const std::string onMessageStatusChanged; /// EMConversation static const std::string getUnreadMsgCount; - static const std::string markAllMessagesAsRead; - static const std::string markMessageAsRead; static const std::string syncConversationExt; - static const std::string syncConversationName; static const std::string removeMessage; static const std::string getLatestMessage; static const std::string getLatestMessageFromOthers; static const std::string clearAllMessages; static const std::string insertMessage; - static const std::string appendMessage; static const std::string updateConversationMessage; - // 根据消息id获取消息 - static const std::string loadMsgWithId; // 根据起始消息id获取消息 static const std::string loadMsgWithStartId; // 根据关键字获取消息 @@ -124,9 +110,6 @@ class ExtSdkMethodType { static const std::string fetchPublicChatRoomsFromServer; static const std::string fetchChatRoomInfoFromServer; static const std::string getChatRoom; - static const std::string getAllChatRooms; - static const std::string createChatRoom; - static const std::string destroyChatRoom; static const std::string changeChatRoomSubject; static const std::string changeChatRoomDescription; static const std::string fetchChatRoomMembers; @@ -158,10 +141,8 @@ class ExtSdkMethodType { /// EMGroupManager static const std::string getGroupWithId; static const std::string getJoinedGroups; - static const std::string getGroupsWithoutPushNotification; - static const std::string getJoinedGroupsFromServer; - static const std::string getPublicGroupsFromServer; static const std::string createGroup; + static const std::string updateGroupConfigs; static const std::string getGroupSpecificationFromServer; static const std::string getGroupMemberListFromServer; static const std::string getGroupBlockListFromServer; @@ -201,30 +182,21 @@ class ExtSdkMethodType { static const std::string declineJoinApplication; static const std::string acceptInvitationFromGroup; static const std::string declineInvitationFromGroup; - static const std::string ignoreGroupPush; /// EMGroupManagerListener static const std::string onGroupChanged; /// EMPushManager - static const std::string getImPushConfig; static const std::string getImPushConfigFromServer; static const std::string updatePushNickname; - static const std::string updateHMSPushToken; - static const std::string updateFCMPushToken; /// ImPushConfig - static const std::string imPushNoDisturb; static const std::string updateImPushStyle; - static const std::string updateGroupPushService; - static const std::string getNoDisturbGroups; /// EMUserInfoManager static const std::string updateOwnUserInfo; - static const std::string updateOwnUserInfoWithType; static const std::string fetchUserInfoById; - static const std::string fetchUserInfoByIdWithType; }; EXT_SDK_NAMESPACE_END diff --git a/modules/java/com/chatsdk/common/ExtSdkMethodType.java b/modules/java/com/chatsdk/common/ExtSdkMethodType.java index ab295d0a..de0e27a1 100644 --- a/modules/java/com/chatsdk/common/ExtSdkMethodType.java +++ b/modules/java/com/chatsdk/common/ExtSdkMethodType.java @@ -3,24 +3,19 @@ public class ExtSdkMethodType { /// EMClient methods public static final String init = "init"; - public static final String createAccount = "createAccount"; public static final String login = "login"; public static final String logout = "logout"; public static final String changeAppKey = "changeAppKey"; public static final String changeAppId = "changeAppId"; - public static final String isLoggedInBefore = "isLoggedInBefore"; - public static final String updateCurrentUserNick = "updateCurrentUserNick"; // deprecated 2026-05-21 - public static final String uploadLog = "uploadLog"; public static final String compressLogs = "compressLogs"; public static final String kickDevice = "kickDevice"; public static final String kickAllDevices = "kickAllDevices"; public static final String getLoggedInDevicesFromServer = "getLoggedInDevicesFromServer"; public static final String getCurrentUser = "getCurrentUser"; public static final String getToken = "getToken"; - public static final String loginWithAgoraToken = "loginWithAgoraToken"; public static final String isConnected = "isConnected"; public static final String renewToken = "renewToken"; - // 2026-09-24 1.21.0 + public static final String bindDeviceToken = "bindDeviceToken"; public static final String bindPushKitToken = "bindPushKitToken"; public static final String unbindPushKitToken = "unbindPushKitToken"; @@ -36,22 +31,15 @@ public class ExtSdkMethodType { public static final String onTokenWillExpire = "onTokenWillExpire"; public static final String onTokenDidExpire = "onTokenDidExpire"; - public static final String onUserDidLoginFromOtherDevice = "onUserDidLoginFromOtherDevice"; - public static final String onUserDidLoginFromOtherDeviceWithInfo = "onUserDidLoginFromOtherDeviceWithInfo"; public static final String onOfflineMessageSyncStart = "onOfflineMessageSyncStart"; public static final String onOfflineMessageSyncFinish = "onOfflineMessageSyncFinish"; - public static final String onUserDidRemoveFromServer = "onUserDidRemoveFromServer"; - public static final String onUserDidForbidByServer = "onUserDidForbidByServer"; - public static final String onUserDidChangePassword = "onUserDidChangePassword"; - public static final String onUserDidLoginTooManyDevice = "onUserDidLoginTooManyDevice"; - public static final String onUserKickedByOtherDevice = "onUserKickedByOtherDevice"; - public static final String onUserAuthenticationFailed = "onUserAuthenticationFailed"; - public static final String onAppActiveNumberReachLimit = "onAppActiveNumberReachLimit"; + public static final String onDataSyncStart = "onDataSyncStart"; + public static final String onDataSyncFinish = "onDataSyncFinish"; + public static final String onDatabaseOpened = "onDatabaseOpened"; /// EMContactManager methods public static final String addContact = "addContact"; public static final String deleteContact = "deleteContact"; - public static final String getAllContactsFromServer = "getAllContactsFromServer"; public static final String getAllContactsFromDB = "getAllContactsFromDB"; public static final String addUserToBlockList = "addUserToBlockList"; public static final String removeUserFromBlockList = "removeUserFromBlockList"; @@ -66,12 +54,13 @@ public class ExtSdkMethodType { /// EMChatManager methods public static final String sendMessage = "sendMessage"; public static final String resendMessage = "resendMessage"; - public static final String ackMessageRead = "ackMessageRead"; - public static final String ackGroupMessageRead = "ackGroupMessageRead"; - public static final String ackConversationRead = "ackConversationRead"; + public static final String sendMessageReadReceipts = "sendMessageReadReceipts"; + public static final String clearConversationUnreadMessageCount = "clearConversationUnreadMessageCount"; + public static final String clearAllConversationUnreadMessageCount = "clearAllConversationUnreadMessageCount"; + public static final String getGroupMessageReadReceipts = "getGroupMessageReadReceipts"; + public static final String fetchGroupMessageReadReceipts = "fetchGroupMessageReadReceipts"; public static final String recallMessage = "recallMessage"; public static final String getConversation = "getConversation"; - public static final String markAllChatMsgAsRead = "markAllChatMsgAsRead"; public static final String getUnreadMessageCount = "getUnreadMessageCount"; public static final String updateChatMessage = "updateChatMessage"; @@ -81,13 +70,11 @@ public class ExtSdkMethodType { public static final String downloadThumbnail = "downloadThumbnail"; public static final String importMessages = "importMessages"; public static final String loadAllConversations = "loadAllConversations"; - public static final String getConversationsFromServer = "getConversationsFromServer"; public static final String deleteConversation = "deleteConversation"; - public static final String fetchHistoryMessages = "fetchHistoryMessages"; + public static final String deleteConversations = "deleteConversations"; public static final String fetchHistoryMessagesByOptions = "fetchHistoryMessagesByOptions"; public static final String searchChatMsgFromDB = "searchChatMsgFromDB"; public static final String getMessage = "getMessage"; - public static final String asyncFetchGroupAcks = "asyncFetchGroupAcks"; public static final String deleteRemoteConversation = "deleteRemoteConversation"; public static final String deleteMessagesBeforeTimestamp = "deleteMessagesBeforeTimestamp"; @@ -98,33 +85,22 @@ public class ExtSdkMethodType { public static final String removeReaction = "removeReaction"; public static final String fetchReactionList = "fetchReactionList"; public static final String fetchReactionDetail = "fetchReactionDetail"; - public static final String reportMessage = "reportMessage"; - public static final String fetchConversationsFromServerWithPage = "fetchConversationsFromServerWithPage"; public static final String removeMessagesFromServerWithMsgIds = "removeMessagesFromServerWithMsgIds"; public static final String removeMessagesFromServerWithTs = "removeMessagesFromServerWithTs"; - public static final String getConversationsFromServerWithCursor = "getConversationsFromServerWithCursor"; - public static final String getPinnedConversationsFromServerWithCursor = - "getPinnedConversationsFromServerWithCursor"; - public static final String getConversationsFromDBWithCursor = "getConversationsFromDBWithCursor"; public static final String pinConversation = "pinConversation"; - public static final String modifyMessage = "modifyMessage"; public static final String downloadAndParseCombineMessage = "downloadAndParseCombineMessage"; /// EMChatManager listener public static final String onMessagesReceived = "onMessagesReceived"; public static final String onCmdMessagesReceived = "onCmdMessagesReceived"; - public static final String onMessagesRead = "onMessagesRead"; - public static final String onGroupMessageRead = "onGroupMessageRead"; + public static final String onMessageReadReceipts = "onMessageReadReceipts"; public static final String onMessagesDelivered = "onMessagesDelivered"; - public static final String onMessagesRecalled = "onMessagesRecalled"; public static final String onMessagesRecalledInfo = "onMessagesRecalledInfo"; public static final String onConversationUpdate = "onConversationUpdate"; - public static final String onConversationHasRead = "onConversationHasRead"; - public static final String onReadAckForGroupMessageUpdated = "onReadAckForGroupMessageUpdated"; public static final String onMessageReactionDidChange = "messageReactionDidChange"; public static final String onMessageContentChanged = "onMessageContentChanged"; @@ -132,27 +108,19 @@ public class ExtSdkMethodType { public static final String onMessageProgressUpdate = "onMessageProgressUpdate"; public static final String onMessageError = "onMessageError"; public static final String onMessageSuccess = "onMessageSuccess"; - public static final String onMessageReadAck = "onMessageReadAck"; // deprecated 2026-08-20 use onMessagesRead instead - public static final String onMessageDeliveryAck = "onMessageDeliveryAck"; // deprecated 2026-08-20 use onMessagesDelivered instead - public static final String onMessageStatusChanged = "onMessageStatusChanged"; // deprecated 2022.05.04 /// EMConversation public static final String getUnreadMsgCount = "getUnreadMsgCount"; public static final String getMsgCount = "getMsgCount"; - public static final String markAllMessagesAsRead = "markAllMessagesAsRead"; - public static final String markMessageAsRead = "markMessageAsRead"; public static final String syncConversationExt = "syncConversationExt"; - public static final String syncConversationName = "syncConversationName"; // deprecated 2022.05.04 public static final String removeMessage = "removeMessage"; public static final String getLatestMessage = "getLatestMessage"; public static final String getLatestMessageFromOthers = "getLatestMessageFromOthers"; public static final String clearAllMessages = "clearAllMessages"; public static final String deleteMessagesWithTs = "deleteMessagesWithTs"; public static final String insertMessage = "insertMessage"; - public static final String appendMessage = "appendMessage"; public static final String updateConversationMessage = "updateConversationMessage"; - public static final String loadMsgWithId = "loadMsgWithId"; public static final String loadMsgWithStartId = "loadMsgWithStartId"; public static final String loadMsgWithKeywords = "loadMsgWithKeywords"; public static final String loadMsgWithMsgType = "loadMsgWithMsgType"; @@ -160,7 +128,6 @@ public class ExtSdkMethodType { // EMMessage method public static final String getReactionList = "getReactionList"; - public static final String groupAckCount = "groupAckCount"; // EMChatRoomManager public static final String joinChatRoom = "joinChatRoom"; @@ -168,9 +135,6 @@ public class ExtSdkMethodType { public static final String fetchPublicChatRoomsFromServer = "fetchPublicChatRoomsFromServer"; public static final String fetchChatRoomInfoFromServer = "fetchChatRoomInfoFromServer"; public static final String getChatRoom = "getChatRoom"; - public static final String getAllChatRooms = "getAllChatRooms"; - public static final String createChatRoom = "createChatRoom"; - public static final String destroyChatRoom = "destroyChatRoom"; public static final String changeChatRoomSubject = "changeChatRoomSubject"; public static final String changeChatRoomDescription = "changeChatRoomDescription"; public static final String fetchChatRoomMembers = "fetchChatRoomMembers"; @@ -196,7 +160,6 @@ public class ExtSdkMethodType { public static final String unMuteAllChatRoomMembers = "unMuteAllChatRoomMembers"; public static final String fetchChatRoomAttributes = "fetchChatRoomAttributes"; - public static final String fetchChatRoomAllAttributes = "fetchChatRoomAllAttributes"; public static final String setChatRoomAttributes = "setChatRoomAttributes"; public static final String removeChatRoomAttributes = "removeChatRoomAttributes"; @@ -206,10 +169,8 @@ public class ExtSdkMethodType { /// EMGroupManager public static final String getGroupWithId = "getGroupWithId"; public static final String getJoinedGroups = "getJoinedGroups"; - public static final String getGroupsWithoutPushNotification = "getGroupsWithoutPushNotification"; - public static final String getJoinedGroupsFromServer = "getJoinedGroupsFromServer"; - public static final String getPublicGroupsFromServer = "getPublicGroupsFromServer"; public static final String createGroup = "createGroup"; + public static final String updateGroupConfigs = "updateGroupConfigs"; public static final String getGroupSpecificationFromServer = "getGroupSpecificationFromServer"; public static final String getGroupMemberListFromServer = "getGroupMemberListFromServer"; public static final String getGroupBlockListFromServer = "getGroupBlockListFromServer"; @@ -252,24 +213,14 @@ public class ExtSdkMethodType { public static final String setMemberAttributesFromGroup = "setMemberAttributesFromGroup"; public static final String fetchMemberAttributesFromGroup = "fetchMemberAttributesFromGroup"; public static final String fetchMembersAttributesFromGroup = "fetchMembersAttributesFromGroup"; - public static final String ignoreGroupPush = "ignoreGroupPush"; // deprecated 2022.05.25 /// EMGroupManagerListener public static final String onGroupChanged = "onGroupChanged"; /// EMPushManager - public static final String getImPushConfig = "getImPushConfig"; public static final String getImPushConfigFromServer = "getImPushConfigFromServer"; public static final String updatePushNickname = "updatePushNickname"; - public static final String updateHMSPushToken = "updateHMSPushToken"; // deprecated 2026-05-21 - public static final String updateFCMPushToken = "updateFCMPushToken"; // deprecated 2026-05-21 - public static final String enableOfflinePush = "enableOfflinePush"; - public static final String disableOfflinePush = "disableOfflinePush"; - public static final String getNoPushGroups = "getNoPushGroups"; - public static final String setNoDisturbUsers = "setNoDisturbUsers"; // deprecated 2022.05.04 - public static final String getNoDisturbUsersFromServer = "getNoDisturbUsersFromServer"; // deprecated 2022.05.04 - - public static final String reportPushAction = "reportPushAction"; + public static final String setConversationSilentMode = "setConversationSilentMode"; public static final String removeConversationSilentMode = "removeConversationSilentMode"; public static final String fetchConversationSilentMode = "fetchConversationSilentMode"; @@ -282,19 +233,11 @@ public class ExtSdkMethodType { public static final String getPushTemplate = "getPushTemplate"; /// ImPushConfig - public static final String imPushNoDisturb = "imPushNoDisturb"; // deprecated 2022.05.04 public static final String updateImPushStyle = "updateImPushStyle"; - public static final String updateGroupPushService = "updateGroupPushService"; - public static final String getNoDisturbGroups = "getNoDisturbGroups"; // deprecated 2022.05.04 - public static final String updateUserPushService = "updateUserPushService"; - public static final String getNoPushUsers = "getNoPushUsers"; - public static final String updatePushConfig = "updatePushConfig"; /// EMUserInfoManager public static final String updateOwnUserInfo = "updateOwnUserInfo"; - public static final String updateOwnUserInfoWithType = "updateOwnUserInfoWithType"; public static final String fetchUserInfoById = "fetchUserInfoById"; - public static final String fetchUserInfoByIdWithType = "fetchUserInfoByIdWithType"; /// EMPresenceManager methods public static final String presenceWithDescription = "publishPresenceWithDescription"; @@ -330,8 +273,6 @@ public class ExtSdkMethodType { public static final String getAllContacts = "getAllContacts"; public static final String setContactRemark = "setContactRemark"; public static final String getContact = "getContact"; - public static final String fetchAllContacts = "fetchAllContacts"; - public static final String fetchContacts = "fetchContacts"; public static final String fetchJoinedGroupCount = "fetchJoinedGroupCount"; // 2024-04-16 4.5.0 @@ -340,7 +281,6 @@ public class ExtSdkMethodType { public static final String onMessagePinChanged = "onMessagePinChanged"; public static final String addRemoteAndLocalConversationsMark = "addRemoteAndLocalConversationsMark"; public static final String deleteRemoteAndLocalConversationsMark = "deleteRemoteAndLocalConversationsMark"; - public static final String fetchConversationsByOptions = "fetchConversationsByOptions"; public static final String deleteAllMessageAndConversation = "deleteAllMessageAndConversation"; public static final String pinMessage = "pinMessage"; public static final String unpinMessage = "unpinMessage"; diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkChatManagerWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkChatManagerWrapper.java index 10bbcd04..41faf8cc 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkChatManagerWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkChatManagerWrapper.java @@ -1,6 +1,5 @@ package com.chatsdk.dispatch; -import android.util.Log; import com.chatsdk.common.ExtSdkCallback; import com.chatsdk.common.ExtSdkMethodType; import com.hyphenate.EMCallBack; @@ -10,10 +9,9 @@ import com.hyphenate.chat.EMAudioParams; import com.hyphenate.chat.EMClient; import com.hyphenate.chat.EMConversation; -import com.hyphenate.chat.EMConversationFilter; import com.hyphenate.chat.EMCursorResult; import com.hyphenate.chat.EMFetchMessageOption; -import com.hyphenate.chat.EMGroupReadAck; +import com.hyphenate.chat.EMGroupReadReceipt; import com.hyphenate.chat.EMKeywordListMatchType; import com.hyphenate.chat.EMLanguage; import com.hyphenate.chat.EMMessage; @@ -21,16 +19,12 @@ import com.hyphenate.chat.EMMessagePinInfo; import com.hyphenate.chat.EMMessageReaction; import com.hyphenate.chat.EMMessageReactionChange; +import com.hyphenate.chat.EMMessageReadReceipt; import com.hyphenate.chat.EMMessageSearchOption; import com.hyphenate.chat.EMPageResult; import com.hyphenate.chat.EMRecallMessageInfo; import com.hyphenate.chat.EMSearchServerMessageResult; -import com.hyphenate.chat.EMStatisticsManager; -import com.hyphenate.chat.adapter.message.EMAMessage; -import com.hyphenate.exceptions.HyphenateException; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -141,44 +135,105 @@ public void onError(int code, String desc) { onSuccess(result, channelName, ExtSdkMessageHelper.toJson(tempMsg)); } - public void ackMessageRead(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = param.getString("msg_id"); - String to = param.getString("to"); - - try { - EMClient.getInstance().chatManager().ackMessageRead(to, msgId); - onSuccess(result, channelName, true); - } catch (HyphenateException e) { - onError(result, e, null); + public void sendMessageReadReceipts(JSONObject param, String channelName, ExtSdkCallback result) + throws JSONException { + JSONArray msgIdsJson = param.getJSONArray("msg_ids"); + List messages = new ArrayList<>(); + for (int i = 0; i < msgIdsJson.length(); i++) { + EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgIdsJson.getString(i)); + if (msg != null) { + messages.add(msg); + } } + EMClient.getInstance().chatManager().asyncSendMessageReadReceipts(messages, new EMCallBack() { + @Override + public void onSuccess() { + ExtSdkWrapper.onSuccess(result, channelName, null); + } + + @Override + public void onError(int code, String error) { + ExtSdkWrapper.onError(result, code, error); + } + }); } - public void ackGroupMessageRead(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = param.getString("msg_id"); - String to = param.getString("group_id"); - String content = null; - if (param.has("content")) { - content = param.getString("content"); - } - String finalContent = content; + public void clearConversationUnreadMessageCount(JSONObject param, String channelName, ExtSdkCallback result) + throws JSONException { + String convId = param.getString("convId"); + EMClient.getInstance().chatManager().asyncClearConversationUnreadMessageCount(convId, new EMCallBack() { + @Override + public void onSuccess() { + ExtSdkWrapper.onSuccess(result, channelName, null); + } - try { - EMClient.getInstance().chatManager().ackGroupMessageRead(to, msgId, finalContent); - onSuccess(result, channelName, null); - } catch (HyphenateException e) { - onError(result, e, null); - } + @Override + public void onError(int code, String error) { + ExtSdkWrapper.onError(result, code, error); + } + }); } - public void ackConversationRead(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String conversationId = param.getString("convId"); + public void clearAllConversationUnreadMessageCount(JSONObject param, String channelName, ExtSdkCallback result) + throws JSONException { + EMClient.getInstance().chatManager().asyncClearAllConversationUnreadMessageCount(new EMCallBack() { + @Override + public void onSuccess() { + ExtSdkWrapper.onSuccess(result, channelName, null); + } + + @Override + public void onError(int code, String error) { + ExtSdkWrapper.onError(result, code, error); + } + }); + } - try { - EMClient.getInstance().chatManager().ackConversationRead(conversationId); - onSuccess(result, channelName, true); - } catch (HyphenateException e) { - onError(result, e, null); + public void getGroupMessageReadReceipts(JSONObject param, String channelName, ExtSdkCallback result) + throws JSONException { + JSONArray msgIdsJson = param.getJSONArray("msg_ids"); + List messages = new ArrayList<>(); + for (int i = 0; i < msgIdsJson.length(); i++) { + EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgIdsJson.getString(i)); + if (msg != null) { + messages.add(msg); + } } + EMClient.getInstance().chatManager().asyncGetGroupMessageReadReceipts( + messages, new EMValueCallBack>() { + @Override + public void onSuccess(List value) { + List receipts = new ArrayList<>(); + for (EMMessageReadReceipt receipt : value) { + receipts.add(ExtSdkMessageReadReceiptHelper.toJson(receipt)); + } + ExtSdkWrapper.onSuccess(result, channelName, receipts); + } + + @Override + public void onError(int error, String errorMsg) { + ExtSdkWrapper.onError(result, error, errorMsg); + } + }); + } + + public void fetchGroupMessageReadReceipts(JSONObject param, String channelName, ExtSdkCallback result) + throws JSONException { + String msgId = param.getString("msg_id"); + String startAckId = param.optString("receipt_id"); + int pageSize = param.optInt("pageSize"); + EMClient.getInstance().chatManager().asyncFetchGroupMessageReadReceipts( + msgId, pageSize, startAckId, new EMValueCallBack>() { + @Override + public void onSuccess(EMCursorResult value) { + ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(value)); + } + + @Override + public void onError(int error, String errorMsg) { + ExtSdkWrapper.onError(result, error, errorMsg); + } + }); } public void recallMessage(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { @@ -221,12 +276,6 @@ public void getConversation(JSONObject param, String channelName, ExtSdkCallback onSuccess(result, channelName, ExtSdkConversationHelper.toJson(conversation)); } - public void markAllChatMsgAsRead(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - EMClient.getInstance().chatManager().markAllConversationsAsRead(); - - onSuccess(result, channelName, null); - } - public void getUnreadMessageCount(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { int count = EMClient.getInstance().chatManager().getUnreadMessageCount(); @@ -602,24 +651,6 @@ public void loadAllConversations(JSONObject param, String channelName, ExtSdkCal onSuccess(result, channelName, conversations); } - public void getConversationsFromServer(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - - try { - List list = - new ArrayList<>(EMClient.getInstance().chatManager().fetchConversationsFromServer().values()); - Collections.sort( - list, (o1, o2) -> (o2.getLastMessage().getMsgTime() - o1.getLastMessage().getMsgTime() > 0 ? 1 : -1)); - List conversations = new ArrayList<>(); - for (EMConversation conversation : list) { - conversations.add(ExtSdkConversationHelper.toJson(conversation)); - } - onSuccess(result, channelName, conversations); - } catch (HyphenateException e) { - onError(result, e, null); - } - } - public void deleteConversation(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String conId = param.getString("convId"); boolean isDelete = param.getBoolean("deleteMessages"); @@ -632,21 +663,27 @@ public void deleteConversation(JSONObject param, String channelName, ExtSdkCallb } } - public void fetchHistoryMessages(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String conId = param.getString("convId"); - EMConversation.EMConversationType type = - InternalConvertHelper.conversationTypeFromInt(param.getInt("convType")); - int pageSize = param.getInt("pageSize"); - String startMsgId = param.getString("startMsgId"); - EMConversation.EMSearchDirection direction = - ExtSdkEMSearchDirectionHelper.toDirection(param.getString("direction")); - try { - EMCursorResult cursorResult = - EMClient.getInstance().chatManager().fetchHistoryMessages(conId, type, pageSize, startMsgId, direction); - onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(cursorResult)); - } catch (HyphenateException e) { - onError(result, e, null); + public void deleteConversations(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { + JSONArray ids = param.getJSONArray("convIds"); + List conversationIds = new ArrayList<>(); + for (int i = 0; i < ids.length(); i++) { + conversationIds.add(ids.getString(i)); } + boolean deleteMessages = param.optBoolean("deleteMessages", true); + EMClient.getInstance().chatManager().asyncDeleteConversations(conversationIds, deleteMessages, new EMCallBack() { + @Override + public void onSuccess() { + ExtSdkWrapper.onSuccess(result, channelName, null); + } + + @Override + public void onError(int code, String error) { + ExtSdkWrapper.onError(result, code, error); + } + + @Override + public void onProgress(int progress, String status) {} + }); } public void fetchHistoryMessagesByOptions(JSONObject param, String channelName, ExtSdkCallback result) @@ -698,24 +735,6 @@ public void searchChatMsgFromDB(JSONObject param, String channelName, ExtSdkCall onSuccess(result, channelName, messages); } - public void asyncFetchGroupAcks(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = param.getString("msg_id"); - String ackId = param.getString("ack_id"); - int pageSize = param.getInt("pageSize"); - EMClient.getInstance().chatManager().asyncFetchGroupReadAcks( - msgId, pageSize, ackId, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult value) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(value)); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - public void deleteRemoteConversation(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String conversationId = param.getString("conversationId"); @@ -922,77 +941,6 @@ public void onError(int error, String errorMsg) { }); } - public void reportMessage(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = param.getString("msgId"); - String tag = param.getString("tag"); - String reason = param.getString("reason"); - EMClient.getInstance().chatManager().asyncReportMessage(msgId, tag, reason, new EMCallBack() { - @Override - public void onSuccess() { - ExtSdkWrapper.onSuccess(result, channelName, null); - } - - @Override - public void onError(int code, String error) { - ExtSdkWrapper.onError(result, code, error); - } - }); - } - - public void fetchConversationsFromServerWithPage(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - int pageNum = param.getInt("pageNum"); - int pageSize = param.getInt("pageSize"); - EMClient.getInstance().chatManager().asyncFetchConversationsFromServer( - pageNum, pageSize, new EMValueCallBack>() { - @Override - public void onSuccess(Map value) { - ArrayList list = new ArrayList<>(value.values()); - boolean retry = false; - List conversations = new ArrayList<>(); - do { - try { - retry = false; - Collections.sort(list, new Comparator() { - @Override - public int compare(EMConversation o1, EMConversation o2) { - if (o1 == null || o2 == null) { - return 0; - } - if (o1.getLastMessage() == null) { - return 1; - } - - if (o2.getLastMessage() == null) { - return -1; - } - - if (o1.getLastMessage().getMsgTime() == o2.getLastMessage().getMsgTime()) { - return 0; - } - - return o2.getLastMessage().getMsgTime() - o1.getLastMessage().getMsgTime() > 0 ? 1 - : -1; - } - }); - for (EMConversation conversation : list) { - conversations.add(ExtSdkConversationHelper.toJson(conversation)); - } - - } catch (IllegalArgumentException e) { - retry = true; - } - } while (retry); - ExtSdkWrapper.onSuccess(result, channelName, conversations); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - public void removeMessagesFromServerWithMsgIds(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { EMConversation conversation = this.getConversation(param); @@ -1034,63 +982,6 @@ public void onError(int code, String error) { }); } - public void getConversationsFromServerWithCursor(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - String cursor = param.optString("cursor"); - int pageSize = param.optInt("pageSize"); - EMClient.getInstance().chatManager().asyncFetchConversationsFromServer( - pageSize, cursor, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult emConversationEMCursorResult) { - ExtSdkWrapper.onSuccess(result, channelName, - ExtSdkCursorResultHelper.toJson(emConversationEMCursorResult)); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); - } - - public void getPinnedConversationsFromServerWithCursor(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - String cursor = param.optString("cursor"); - int pageSize = param.optInt("pageSize"); - EMClient.getInstance().chatManager().asyncFetchPinnedConversationsFromServer( - pageSize, cursor, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult emConversationEMCursorResult) { - ExtSdkWrapper.onSuccess(result, channelName, - ExtSdkCursorResultHelper.toJson(emConversationEMCursorResult)); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); - } - - public void getConversationsFromDBWithCursor(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - String cursor = param.optString("cursor"); - int pageSize = param.optInt("pageSize"); - EMClient.getInstance().chatManager().asyncGetConversationsFromDB( - cursor, pageSize, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult emConversationEMCursorResult) { - ExtSdkWrapper.onSuccess(result, channelName, - ExtSdkCursorResultHelper.toJson(emConversationEMCursorResult)); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); - } - public void pinConversation(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String convId = param.optString("convId"); Boolean isPinned = param.optBoolean("isPinned", false); @@ -1107,23 +998,6 @@ public void onError(int i, String s) { }); } - public void modifyMessage(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = param.optString("msgId"); - JSONObject bodyJson = param.optJSONObject("body"); - EMMessageBody body = ExtSdkMessageBodyHelper.textBodyFromJson(bodyJson != null ? bodyJson : new JSONObject()); - EMClient.getInstance().chatManager().asyncModifyMessage(msgId, body, new EMValueCallBack() { - @Override - public void onSuccess(EMMessage emMessage) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkMessageHelper.toJson(emMessage)); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); - } - public void downloadAndParseCombineMessage(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { EMMessage msg = ExtSdkMessageHelper.fromJson(param.optJSONObject("message")); @@ -1191,59 +1065,6 @@ public void onError(int code, String error) { }); } - public void fetchConversationsByOptions(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - String cursor = ExtSdkConversationFilterHelper.cursor(param); - Boolean isPinned = ExtSdkConversationFilterHelper.pinned(param); - Boolean isMark = ExtSdkConversationFilterHelper.hasMark(param); - int pageSize = ExtSdkConversationFilterHelper.pageSize(param); - if (isPinned) { - EMClient.getInstance().chatManager().asyncFetchPinnedConversationsFromServer( - pageSize, cursor, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult emCursorResult) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(emCursorResult)); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); - return; - } - - if (isMark) { - EMConversationFilter filter = ExtSdkConversationFilterHelper.fromJson(param); - EMClient.getInstance().chatManager().asyncGetConversationsFromServerWithCursor( - cursor, filter, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult value) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(value)); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - return; - } - - EMClient.getInstance().chatManager().asyncFetchConversationsFromServer( - pageSize, cursor, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult emCursorResult) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(emCursorResult)); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); - } - public void deleteAllMessageAndConversation(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { Boolean clearServerData = param.getBoolean("clearServerData"); @@ -1540,14 +1361,14 @@ public void onCmdMessageReceived(List messages) { } @Override - public void onMessageRead(List messages) { - ArrayList> msgList = new ArrayList<>(); - for (EMMessage message : messages) { - msgList.add(ExtSdkMessageHelper.toJson(message)); - ExtSdkWrapper.onReceive(ExtSdkMethodType.onMessageReadAck, ExtSdkMessageHelper.toJson(message)); + public void onMessageReadReceipts(List receipts) { + ArrayList> list = new ArrayList<>(); + for (EMMessageReadReceipt receipt : receipts) { + list.add(ExtSdkMessageReadReceiptHelper.toJson(receipt)); } - - ExtSdkWrapper.onReceive(ExtSdkMethodType.onMessagesRead, msgList); + Map data = new HashMap<>(); + data.put("receipts", list); + ExtSdkWrapper.onReceive(ExtSdkMethodType.onMessageReadReceipts, data); } @Override @@ -1555,7 +1376,6 @@ public void onMessageDelivered(List messages) { ArrayList> msgList = new ArrayList<>(); for (EMMessage message : messages) { msgList.add(ExtSdkMessageHelper.toJson(message)); - ExtSdkWrapper.onReceive(ExtSdkMethodType.onMessageDeliveryAck, ExtSdkMessageHelper.toJson(message)); } ExtSdkWrapper.onReceive(ExtSdkMethodType.onMessagesDelivered, msgList); } @@ -1574,20 +1394,6 @@ public void onMessageRecalledWithExt(List recallMessageInfo ExtSdkWrapper.onReceive(ExtSdkMethodType.onMessagesRecalledInfo, msgList); } - @Override - public void onGroupMessageRead(List var1) { - ArrayList> msgList = new ArrayList<>(); - for (EMGroupReadAck ack : var1) { - msgList.add(ExtSdkGroupAckHelper.toJson(ack)); - } - ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupMessageRead, msgList); - } - - @Override - public void onReadAckForGroupMessageUpdated() { - ExtSdkWrapper.onReceive(ExtSdkMethodType.onReadAckForGroupMessageUpdated, null); - } - @Override public void onReactionChanged(List messageReactionChangeList) { ArrayList> list = new ArrayList<>(); @@ -1629,14 +1435,6 @@ public void onConversationUpdate() { Map data = new HashMap<>(); ExtSdkWrapper.onReceive(ExtSdkMethodType.onConversationUpdate, data); } - - @Override - public void onConversationRead(String from, String to) { - Map data = new HashMap<>(); - data.put("from", from); - data.put("to", to); - ExtSdkWrapper.onReceive(ExtSdkMethodType.onConversationHasRead, data); - } }; EMClient.getInstance().chatManager().addConversationListener(this.conversationListener); } diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkChatMessageWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkChatMessageWrapper.java index 7d77042b..2f941090 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkChatMessageWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkChatMessageWrapper.java @@ -41,15 +41,6 @@ public void getReactionList(JSONObject param, String channelName, ExtSdkCallback ExtSdkWrapper.onSuccess(result, channelName, list); } - public void groupAckCount(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = param.getString("msgId"); - EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgId); - if (ExtSdkWrapper.getMessageParams(msg, channelName, result)) { - return; - } - ExtSdkWrapper.onSuccess(result, channelName, msg.groupAckCount()); - } - public void getPinInfo(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { String msgId = params.getString("msgId"); EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgId); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkChatRoomManagerWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkChatRoomManagerWrapper.java index c1b6500a..81bc767a 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkChatRoomManagerWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkChatRoomManagerWrapper.java @@ -100,54 +100,6 @@ public void getChatRoom(JSONObject param, String channelName, ExtSdkCallback res onSuccess(result, channelName, ExtSdkChatRoomHelper.toJson(room)); } - public void getAllChatRooms(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - List list = EMClient.getInstance().chatroomManager().getAllChatRooms(); - List> roomList = new ArrayList<>(); - for (EMChatRoom room : list) { - roomList.add(ExtSdkChatRoomHelper.toJson(room)); - } - onSuccess(result, channelName, roomList); - } - - public void createChatRoom(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String subject = param.getString("subject"); - String description = param.getString("desc"); - String welcomeMessage = param.getString("welcomeMsg"); - int maxUserCount = param.getInt("maxUserCount"); - JSONArray members = param.getJSONArray("members"); - List membersList = new ArrayList<>(); - for (int i = 0; i < members.length(); i++) { - membersList.add((String)members.get(i)); - } - EMClient.getInstance().chatroomManager().asyncCreateChatRoom( - subject, description, welcomeMessage, maxUserCount, membersList, new EMValueCallBack() { - @Override - public void onSuccess(EMChatRoom value) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkChatRoomHelper.toJson(value)); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - - public void destroyChatRoom(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String roomId = param.getString("roomId"); - EMClient.getInstance().chatroomManager().asyncDestroyChatRoom(roomId, new EMCallBack() { - @Override - public void onSuccess() { - ExtSdkWrapper.onSuccess(result, channelName, null); - } - - @Override - public void onError(int code, String error) { - ExtSdkWrapper.onError(result, code, error); - } - }); - } - public void changeChatRoomSubject(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String roomId = param.getString("roomId"); @@ -575,23 +527,6 @@ public void onError(int error, String errorMsg) { }); } - public void fetchChatRoomAllAttributes(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - String roomId = param.getString("roomId"); - EMClient.getInstance().chatroomManager().asyncFetchChatRoomAllAttributesFromServer( - roomId, new EMValueCallBack>() { - @Override - public void onSuccess(Map value) { - ExtSdkWrapper.onSuccess(result, channelName, value); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - public void setChatRoomAttributes(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String roomId = param.getString("roomId"); @@ -682,7 +617,7 @@ public void onError(int i, String s) { private void registerEaseListener() { if (this.roomChangeListener != null) { - EMClient.getInstance().chatroomManager().removeChatRoomListener(this.roomChangeListener); + EMClient.getInstance().chatroomManager().removeChatRoomChangeListener(this.roomChangeListener); } this.roomChangeListener = new EMChatRoomChangeListener() { @Override @@ -752,16 +687,6 @@ public void onRemovedFromChatRoom(int reason, String roomId, String roomName, St ExtSdkWrapper.onReceive(ExtSdkMethodType.chatRoomChange, data); } - @Override - public void onMuteListAdded(String chatRoomId, List mutes, long expireTime) { - Map data = new HashMap<>(); - data.put("roomId", chatRoomId); - data.put("mutes", mutes); - data.put("expireTime", String.valueOf(expireTime)); - data.put("type", "onMuteListAdded"); - ExtSdkWrapper.onReceive(ExtSdkMethodType.chatRoomChange, data); - } - @Override public void onMuteListAdded(String chatRoomId, Map muteInfo) { Map data = new HashMap<>(); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkClientWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkClientWrapper.java index 96506f59..787ddf9d 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkClientWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkClientWrapper.java @@ -43,63 +43,29 @@ public void getToken(JSONObject param, String channelName, ExtSdkCallback result onSuccess(result, channelName, EMClient.getInstance().getAccessToken()); } - public void createAccount(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String username = param.getString("username"); - String password = param.getString("password"); - try { - EMClient.getInstance().createAccount(username, password); - onSuccess(result, channelName, username); - } catch (HyphenateException e) { - onError(result, e, null); - } - } - public void login(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - boolean isPwd = param.getBoolean("isPassword"); String username = param.getString("username"); - String pwdOrToken = param.getString("pwdOrToken"); - - if (isPwd) { - EMClient.getInstance().login(username, pwdOrToken, new EMCallBack() { - @Override - public void onSuccess() { - Map param = new HashMap<>(); - param.put("username", EMClient.getInstance().getCurrentUser()); - param.put("token", EMClient.getInstance().getAccessToken()); - ExtSdkWrapper.onSuccess(result, channelName, param); - } - - @Override - public void onError(int code, String error) { - ExtSdkWrapper.onError(result, code, error); - } + String token = param.getString("pwdOrToken"); - @Override - public void onProgress(int progress, String status) { - // todo: 原来就没有写 - } - }); - } else { - EMClient.getInstance().loginWithToken(username, pwdOrToken, new EMCallBack() { - @Override - public void onSuccess() { - Map param = new HashMap<>(); - param.put("username", EMClient.getInstance().getCurrentUser()); - param.put("token", EMClient.getInstance().getAccessToken()); - ExtSdkWrapper.onSuccess(result, channelName, param); - } + EMClient.getInstance().loginWithToken(username, token, new EMCallBack() { + @Override + public void onSuccess() { + Map param = new HashMap<>(); + param.put("username", EMClient.getInstance().getCurrentUser()); + param.put("token", EMClient.getInstance().getAccessToken()); + ExtSdkWrapper.onSuccess(result, channelName, param); + } - @Override - public void onError(int code, String error) { - ExtSdkWrapper.onError(result, code, error); - } + @Override + public void onError(int code, String error) { + ExtSdkWrapper.onError(result, code, error); + } - @Override - public void onProgress(int progress, String status) { - // todo: 原来就没有写 - } - }); - } + @Override + public void onProgress(int progress, String status) { + // todo: 原来就没有写 + } + }); } public void logout(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { @@ -144,19 +110,6 @@ public void getCurrentUser(JSONObject param, String channelName, ExtSdkCallback onSuccess(result, channelName, EMClient.getInstance().getCurrentUser()); } - public void uploadLog(JSONObject param, String channelName, ExtSdkCallback result) { - EMClient.getInstance().uploadLog(new EMCallBack() { - @Override - public void onSuccess() {} - - @Override - public void onError(int code, String error) {} - - @Override - public void onProgress(int progress, String status) {} - }); - } - public void compressLogs(JSONObject param, String channelName, ExtSdkCallback result) { try { String path = EMClient.getInstance().compressLogs(); @@ -169,17 +122,11 @@ public void compressLogs(JSONObject param, String channelName, ExtSdkCallback re public void kickDevice(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String username = param.getString("username"); - String password = param.getString("password"); + String token = param.getString("token"); String resource = param.getString("resource"); - boolean isPassword = param.getBoolean("isPassword"); try { - if (isPassword) { - EMClient.getInstance().kickDevice(username, password, resource); - } else { - EMClient.getInstance().kickDeviceWithToken(username, password, resource); - } - + EMClient.getInstance().kickDeviceWithToken(username, token, resource); onSuccess(result, channelName, true); } catch (HyphenateException e) { onError(result, e, null); @@ -188,51 +135,37 @@ public void kickDevice(JSONObject param, String channelName, ExtSdkCallback resu public void kickAllDevices(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String username = param.getString("username"); - String password = param.getString("password"); - boolean isPassword = param.getBoolean("isPassword"); + String token = param.getString("token"); try { - if (isPassword) { - EMClient.getInstance().kickAllDevices(username, password); - } else { - EMClient.getInstance().kickAllDevicesWithToken(username, password); - } - + EMClient.getInstance().kickAllDevicesWithToken(username, token); onSuccess(result, channelName, true); } catch (HyphenateException e) { onError(result, e, null); } } - public void isLoggedInBefore(JSONObject param, String channelName, ExtSdkCallback result) { - onSuccess(result, channelName, EMClient.getInstance().isLoggedInBefore()); - } - public void getLoggedInDevicesFromServer(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String username = param.getString("username"); - String password = param.getString("password"); - boolean isPassword = param.getBoolean("isPassword"); + String token = param.getString("token"); - try { - List jsonList = new ArrayList<>(); - if (isPassword) { - List devices = EMClient.getInstance().getLoggedInDevicesFromServer(username, password); - for (EMDeviceInfo info : devices) { - jsonList.add(ExtSdkDeviceInfoHelper.toJson(info)); - } - } else { - List devices = - EMClient.getInstance().getLoggedInDevicesFromServerWithToken(username, password); - for (EMDeviceInfo info : devices) { - jsonList.add(ExtSdkDeviceInfoHelper.toJson(info)); + EMClient.getInstance().fetchLoggedInDevicesFromServerWithToken( + username, token, new EMValueCallBack>() { + @Override + public void onSuccess(List value) { + List jsonList = new ArrayList<>(); + for (EMDeviceInfo info : value) { + jsonList.add(ExtSdkDeviceInfoHelper.toJson(info)); + } + ExtSdkWrapper.onSuccess(result, channelName, jsonList); } - } - onSuccess(result, channelName, jsonList); - } catch (HyphenateException e) { - onError(result, e, null); - } + @Override + public void onError(int error, String errorMsg) { + ExtSdkWrapper.onError(result, error, errorMsg); + } + }); } public void init(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { @@ -260,36 +193,13 @@ public void init(JSONObject param, String channelName, ExtSdkCallback result) th }); } - public void loginWithAgoraToken(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String username = param.getString("username"); - String agoratoken = param.getString("agoratoken"); - - EMClient.getInstance().loginWithAgoraToken(username, agoratoken, new EMCallBack() { - @Override - public void onSuccess() { - Map param = new HashMap<>(); - param.put("username", EMClient.getInstance().getCurrentUser()); - param.put("token", EMClient.getInstance().getAccessToken()); - ExtSdkWrapper.onSuccess(result, channelName, param); - } - - @Override - public void onError(int code, String error) { - ExtSdkWrapper.onError(result, code, error); - } - - @Override - public void onProgress(int progress, String status) {} - }); - } - public void isConnected(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { onSuccess(result, channelName, EMClient.getInstance().isConnected()); } public void renewToken(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - String agoraToken = param.getString("agora_token"); - EMClient.getInstance().renewToken(agoraToken, new EMCallBack() { + String token = param.getString("token"); + EMClient.getInstance().renewToken(token, new EMCallBack() { @Override public void onSuccess() { ExtSdkWrapper.onSuccess(result, channelName, null); @@ -302,11 +212,10 @@ public void onError(int code, String error) { }); } - public void updatePushConfig(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - JSONObject config = param.getJSONObject("config"); - String deviceId = config.getString("deviceId"); - String deviceToken = config.getString("deviceToken"); - EMClient.getInstance().pushManager().bindDeviceToken(deviceId, deviceToken, new EMCallBack() { + public void bindDeviceToken(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { + String notifierName = param.optString("notifierName"); + String deviceToken = param.getString("deviceToken"); + EMClient.getInstance().pushManager().bindDeviceToken(notifierName, deviceToken, new EMCallBack() { @Override public void onSuccess() { ExtSdkWrapper.onSuccess(result, channelName, null); @@ -319,7 +228,6 @@ public void onError(int code, String error) { }); } - // 2026-09-24 1.21.0 // PushKit is an iOS-only capability: the Android SDK has no PushKit API. These routes stay // registered to keep the cross-platform method key contract aligned, and answer with an // explicit error instead of a silent success if they are ever invoked directly. The @@ -449,26 +357,13 @@ public void onConnected() { @Override public void onDisconnected(int errorCode) { - if (errorCode == 206) { - // move to onLogout - // onReceive(ExtSdkMethodType.onUserDidLoginFromOtherDevice, null); - } else if (errorCode == 207) { - onReceive(ExtSdkMethodType.onUserDidRemoveFromServer, null); - } else if (errorCode == 305) { - onReceive(ExtSdkMethodType.onUserDidForbidByServer, null); - } else if (errorCode == 216) { - onReceive(ExtSdkMethodType.onUserDidChangePassword, null); - } else if (errorCode == 214) { - onReceive(ExtSdkMethodType.onUserDidLoginTooManyDevice, null); - } else if (errorCode == 217) { - onReceive(ExtSdkMethodType.onUserKickedByOtherDevice, null); - } else if (errorCode == 202) { - onReceive(ExtSdkMethodType.onUserAuthenticationFailed, null); - } else if (errorCode == 8) { - onReceive(ExtSdkMethodType.onAppActiveNumberReachLimit, null); - } else { - onReceive(ExtSdkMethodType.onDisconnected, null); + if (errorCode == EMError.USER_LOGIN_ANOTHER_DEVICE) { + // 206 is emitted by onLogout(int, EMLoginExtensionInfo) below, which carries the device info. + return; } + Map data = new HashMap<>(); + data.put("errorCode", Integer.valueOf(errorCode)); + onReceive(ExtSdkMethodType.onDisconnected, data); } @Override @@ -482,21 +377,40 @@ public void onTokenWillExpire() { } @Override - public void onLogout(int errorCode, String info) { - if (errorCode == 206) { - Map attributes = new HashMap<>(); - attributes.put("deviceName", info); - onReceive(ExtSdkMethodType.onUserDidLoginFromOtherDevice, attributes); + public void onLogout(int errorCode, EMLoginExtensionInfo info) { + EMConnectionListener.super.onLogout(errorCode, info); + // onLogout fires for every forced logout, but onDisconnected(int) has already + // delivered the error code; only 206 carries extra device info worth emitting. + if (errorCode == EMError.USER_LOGIN_ANOTHER_DEVICE) { + Map data = new HashMap<>(); + data.put("errorCode", Integer.valueOf(errorCode)); + data.put("deviceName", info.getDeviceInfo()); + data.put("ext", info.getDeviceExt()); + onReceive(ExtSdkMethodType.onDisconnected, data); } } @Override - public void onLogout(int errorCode, EMLoginExtensionInfo info) { - EMConnectionListener.super.onLogout(errorCode, info); - Map attributes = new HashMap<>(); - attributes.put("deviceName", info.getDeviceInfo()); - attributes.put("ext", info.getDeviceExt()); - onReceive(ExtSdkMethodType.onUserDidLoginFromOtherDeviceWithInfo, attributes); + public void onDataSyncStart(EMOptions.EMDataSyncType type) { + Map data = new HashMap<>(); + data.put("type", type.getValue()); + onReceive(ExtSdkMethodType.onDataSyncStart, data); + } + + @Override + public void onDataSyncFinish(EMOptions.EMDataSyncType type, int errorCode) { + Map data = new HashMap<>(); + data.put("type", type.getValue()); + data.put("errorCode", errorCode); + onReceive(ExtSdkMethodType.onDataSyncFinish, data); + } + + @Override + public void onDatabaseOpened(String username) { + Map data = new HashMap<>(); + data.put("username", username); + data.put("errorCode", 0); + onReceive(ExtSdkMethodType.onDatabaseOpened, data); } @Override diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkContactManagerWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkContactManagerWrapper.java index 861a8f3e..0848a2d7 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkContactManagerWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkContactManagerWrapper.java @@ -7,7 +7,6 @@ import com.hyphenate.EMValueCallBack; import com.hyphenate.chat.EMClient; import com.hyphenate.chat.EMContact; -import com.hyphenate.chat.EMCursorResult; import com.hyphenate.exceptions.HyphenateException; import java.util.ArrayList; import java.util.HashMap; @@ -62,21 +61,6 @@ public void onError(int code, String error) { }); } - public void getAllContactsFromServer(JSONObject params, String channelName, ExtSdkCallback result) - throws JSONException { - EMClient.getInstance().contactManager().asyncGetAllContactsFromServer(new EMValueCallBack>() { - @Override - public void onSuccess(List value) { - ExtSdkWrapper.onSuccess(result, channelName, value); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - public void getAllContactsFromDB(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { try { @@ -231,44 +215,6 @@ public void getContact(JSONObject params, String channelName, ExtSdkCallback res } } - public void fetchAllContacts(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - EMClient.getInstance().contactManager().asyncFetchAllContactsFromServer(new EMValueCallBack>() { - @Override - public void onSuccess(List value) { - List contactList = new ArrayList<>(); - for (EMContact contact : value) { - contactList.add(ExtSdkContactHelper.toJson(contact)); - } - ExtSdkWrapper.onSuccess(result, channelName, contactList); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - - public void fetchContacts(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - int pageSize = params.getInt("pageSize"); - String cursor = null; - if (params.has("cursor")) { - cursor = params.getString("cursor"); - } - EMClient.getInstance().contactManager().asyncFetchAllContactsFromServer( - pageSize, cursor, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult value) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(value)); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - private void registerEaseListener() { if (this.contactListener != null) { EMClient.getInstance().contactManager().removeContactListener(this.contactListener); @@ -315,26 +261,6 @@ public void onFriendRequestDeclined(String userName) { onReceive(ExtSdkMethodType.onContactChanged, data); } - @Override - public void onContactSyncStart() { - Map data = new HashMap<>(); - data.put("type", "onContactSyncStart"); - onReceive(ExtSdkMethodType.onContactChanged, data); - } - - @Override - public void onContactSyncFinishWithError(int errorCode, String errorMessage) { - Map data = new HashMap<>(); - data.put("type", "onContactSyncFinish"); - if (errorCode != 0) { - Map error = new HashMap<>(); - error.put("code", errorCode); - error.put("description", errorMessage); - data.put("error", error); - } - onReceive(ExtSdkMethodType.onContactChanged, data); - } - @Override public void onContactInfoUpdate(EMContact contact) { Map data = new HashMap<>(); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkConversationWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkConversationWrapper.java index 478ff3db..248c1fb4 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkConversationWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkConversationWrapper.java @@ -37,20 +37,6 @@ public void getMsgCount(JSONObject params, String channelName, ExtSdkCallback re onSuccess(result, channelName, conversation.getAllMsgCount()); } - public void markAllMessagesAsRead(JSONObject params, String channelName, ExtSdkCallback result) - throws JSONException { - EMConversation conversation = this.getConversation(params); - conversation.markAllMessagesAsRead(); - onSuccess(result, channelName, null); - } - - public void markMessageAsRead(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - EMConversation conversation = this.getConversation(params); - String msg_id = params.getString("msg_id"); - conversation.markMessageAsRead(msg_id); - onSuccess(result, channelName, null); - } - public void syncConversationExt(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { EMConversation conversation = this.getConversation(params); JSONObject ext = params.getJSONObject("ext"); @@ -112,17 +98,6 @@ public void insertMessage(JSONObject params, String channelName, ExtSdkCallback onSuccess(result, channelName, null); } - public void appendMessage(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - // EMConversation conversation = this.getConversation(params); - JSONObject msg = params.getJSONObject("msg"); - EMMessage message = ExtSdkMessageHelper.fromJson(msg); - if (message != null) { - final EMConversation conversation = this.getConversationFromMessage(message); - conversation.appendMessage(message); - } - onSuccess(result, channelName, null); - } - public void updateConversationMessage(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { EMConversation conversation = this.getConversation(params); @@ -137,15 +112,6 @@ public void updateConversationMessage(JSONObject params, String channelName, Ext onSuccess(result, channelName, null); } - public void loadMsgWithId(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - String msgId = params.getString("msg_id"); - EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgId); - if (ExtSdkWrapper.getMessageParams(msg, channelName, result)) { - return; - } - onSuccess(result, channelName, ExtSdkMessageHelper.toJson(msg)); - } - public void loadMsgWithStartId(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { EMConversation conversation = this.getConversation(params); String startId = params.getString("startId"); @@ -170,38 +136,26 @@ public void loadMsgWithKeywords(JSONObject params, String channelName, ExtSdkCal int scopeJson = params.optInt("searchScope", EMConversation.EMMessageSearchScope.ALL.ordinal()); EMConversation.EMMessageSearchScope scope = InternalConvertHelper.searchScopeFromInt(scopeJson); JSONArray sendersJson = params.optJSONArray("senders"); - String sender; - if (sendersJson == null) { - sender = params.optString("sender"); - List msgList = - conversation.searchMsgFromDB(keywords, timestamp, count, sender, direction, scope); - List messages = new ArrayList<>(); - for (EMMessage msg : msgList) { - messages.add(ExtSdkMessageHelper.toJson(msg)); - } - onSuccess(result, channelName, messages); - } else { - List senders = new ArrayList<>(); - for (int i = 0; i < sendersJson.length(); i++) { - senders.add(sendersJson.getString(i)); - } - conversation.asyncSearchMsgFromDB(keywords, timestamp, count, senders, direction, scope, - new EMValueCallBack>() { - @Override - public void onSuccess(List emMessages) { - List messages = new ArrayList<>(); - for (EMMessage msg : emMessages) { - messages.add(ExtSdkMessageHelper.toJson(msg)); - } - ExtSdkWrapper.onSuccess(result, channelName, messages); - } - - @Override - public void onError(int i, String s) { - ExtSdkWrapper.onError(result, i, s); - } - }); + List senders = new ArrayList<>(); + for (int i = 0; sendersJson != null && i < sendersJson.length(); i++) { + senders.add(sendersJson.getString(i)); } + conversation.asyncSearchMsgFromDB(keywords, timestamp, count, senders, direction, scope, + new EMValueCallBack>() { + @Override + public void onSuccess(List emMessages) { + List messages = new ArrayList<>(); + for (EMMessage msg : emMessages) { + messages.add(ExtSdkMessageHelper.toJson(msg)); + } + ExtSdkWrapper.onSuccess(result, channelName, messages); + } + + @Override + public void onError(int i, String s) { + ExtSdkWrapper.onError(result, i, s); + } + }); } public void loadMsgWithMsgType(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java b/modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java index 52659e1a..40853af7 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkDispatch.java @@ -64,9 +64,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.init: ExtSdkClientWrapper.getInstance().init(jsonParams, methodType, callback); break; - case ExtSdkMethodType.createAccount: - ExtSdkClientWrapper.getInstance().createAccount(jsonParams, methodType, callback); - break; case ExtSdkMethodType.login: ExtSdkClientWrapper.getInstance().login(jsonParams, methodType, callback); break; @@ -79,15 +76,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.changeAppId: ExtSdkClientWrapper.getInstance().changeAppId(jsonParams, methodType, callback); break; - case ExtSdkMethodType.isLoggedInBefore: - ExtSdkClientWrapper.getInstance().isLoggedInBefore(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.updateCurrentUserNick: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.uploadLog: - ExtSdkClientWrapper.getInstance().uploadLog(jsonParams, methodType, callback); - break; case ExtSdkMethodType.compressLogs: ExtSdkClientWrapper.getInstance().compressLogs(jsonParams, methodType, callback); break; @@ -106,19 +94,15 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getToken: ExtSdkClientWrapper.getInstance().getToken(jsonParams, methodType, callback); break; - case ExtSdkMethodType.loginWithAgoraToken: - ExtSdkClientWrapper.getInstance().loginWithAgoraToken(jsonParams, methodType, callback); - break; case ExtSdkMethodType.isConnected: ExtSdkClientWrapper.getInstance().isConnected(jsonParams, methodType, callback); break; case ExtSdkMethodType.renewToken: ExtSdkClientWrapper.getInstance().renewToken(jsonParams, methodType, callback); break; - case ExtSdkMethodType.updatePushConfig: - ExtSdkClientWrapper.getInstance().updatePushConfig(jsonParams, methodType, callback); + case ExtSdkMethodType.bindDeviceToken: + ExtSdkClientWrapper.getInstance().bindDeviceToken(jsonParams, methodType, callback); break; - // 2026-09-24 1.21.0 // PushKit is an iOS-only capability: the Android SDK has no PushKit API. These routes // exist to keep the cross-platform method key contract aligned, and the TypeScript API // is guarded by Platform.OS so they are not reached in practice. @@ -147,25 +131,13 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.onTokenWillExpire: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onUserDidLoginFromOtherDevice: + case ExtSdkMethodType.onDataSyncStart: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onUserDidRemoveFromServer: + case ExtSdkMethodType.onDataSyncFinish: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onUserDidForbidByServer: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onUserDidChangePassword: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onUserDidLoginTooManyDevice: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onUserKickedByOtherDevice: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onUserAuthenticationFailed: + case ExtSdkMethodType.onDatabaseOpened: callback.fail(1, "no implement: " + methodType); break; @@ -176,9 +148,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.deleteContact: ExtSdkContactManagerWrapper.getInstance().deleteContact(jsonParams, methodType, callback); break; - case ExtSdkMethodType.getAllContactsFromServer: - ExtSdkContactManagerWrapper.getInstance().getAllContactsFromServer(jsonParams, methodType, callback); - break; case ExtSdkMethodType.getAllContactsFromDB: ExtSdkContactManagerWrapper.getInstance().getAllContactsFromDB(jsonParams, methodType, callback); break; @@ -215,14 +184,20 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.resendMessage: ExtSdkChatManagerWrapper.getInstance().resendMessage(jsonParams, methodType, callback); break; - case ExtSdkMethodType.ackMessageRead: - ExtSdkChatManagerWrapper.getInstance().ackMessageRead(jsonParams, methodType, callback); + case ExtSdkMethodType.sendMessageReadReceipts: + ExtSdkChatManagerWrapper.getInstance().sendMessageReadReceipts(jsonParams, methodType, callback); + break; + case ExtSdkMethodType.clearConversationUnreadMessageCount: + ExtSdkChatManagerWrapper.getInstance().clearConversationUnreadMessageCount(jsonParams, methodType, callback); break; - case ExtSdkMethodType.ackGroupMessageRead: - ExtSdkChatManagerWrapper.getInstance().ackGroupMessageRead(jsonParams, methodType, callback); + case ExtSdkMethodType.clearAllConversationUnreadMessageCount: + ExtSdkChatManagerWrapper.getInstance().clearAllConversationUnreadMessageCount(jsonParams, methodType, callback); break; - case ExtSdkMethodType.ackConversationRead: - ExtSdkChatManagerWrapper.getInstance().ackConversationRead(jsonParams, methodType, callback); + case ExtSdkMethodType.getGroupMessageReadReceipts: + ExtSdkChatManagerWrapper.getInstance().getGroupMessageReadReceipts(jsonParams, methodType, callback); + break; + case ExtSdkMethodType.fetchGroupMessageReadReceipts: + ExtSdkChatManagerWrapper.getInstance().fetchGroupMessageReadReceipts(jsonParams, methodType, callback); break; case ExtSdkMethodType.recallMessage: ExtSdkChatManagerWrapper.getInstance().recallMessage(jsonParams, methodType, callback); @@ -230,9 +205,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getConversation: ExtSdkChatManagerWrapper.getInstance().getConversation(jsonParams, methodType, callback); break; - case ExtSdkMethodType.markAllChatMsgAsRead: - ExtSdkChatManagerWrapper.getInstance().markAllChatMsgAsRead(jsonParams, methodType, callback); - break; case ExtSdkMethodType.getUnreadMessageCount: ExtSdkChatManagerWrapper.getInstance().getUnreadMessageCount(jsonParams, methodType, callback); break; @@ -257,14 +229,11 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.loadAllConversations: ExtSdkChatManagerWrapper.getInstance().loadAllConversations(jsonParams, methodType, callback); break; - case ExtSdkMethodType.getConversationsFromServer: - ExtSdkChatManagerWrapper.getInstance().getConversationsFromServer(jsonParams, methodType, callback); - break; case ExtSdkMethodType.deleteConversation: ExtSdkChatManagerWrapper.getInstance().deleteConversation(jsonParams, methodType, callback); break; - case ExtSdkMethodType.fetchHistoryMessages: - ExtSdkChatManagerWrapper.getInstance().fetchHistoryMessages(jsonParams, methodType, callback); + case ExtSdkMethodType.deleteConversations: + ExtSdkChatManagerWrapper.getInstance().deleteConversations(jsonParams, methodType, callback); break; case ExtSdkMethodType.fetchHistoryMessagesByOptions: ExtSdkChatManagerWrapper.getInstance().fetchHistoryMessagesByOptions(jsonParams, methodType, callback); @@ -275,9 +244,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getMessage: ExtSdkChatManagerWrapper.getInstance().getMessage(jsonParams, methodType, callback); break; - case ExtSdkMethodType.asyncFetchGroupAcks: - ExtSdkChatManagerWrapper.getInstance().asyncFetchGroupAcks(jsonParams, methodType, callback); - break; case ExtSdkMethodType.deleteRemoteConversation: ExtSdkChatManagerWrapper.getInstance().deleteRemoteConversation(jsonParams, methodType, callback); break; @@ -292,25 +258,16 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.onCmdMessagesReceived: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onMessagesRead: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onGroupMessageRead: + case ExtSdkMethodType.onMessageReadReceipts: callback.fail(1, "no implement: " + methodType); break; case ExtSdkMethodType.onMessagesDelivered: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onMessagesRecalled: - callback.fail(1, "no implement: " + methodType); - break; case ExtSdkMethodType.onConversationUpdate: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onConversationHasRead: - callback.fail(1, "no implement: " + methodType); - break; /// EMMessage listener case ExtSdkMethodType.onMessageProgressUpdate: @@ -322,15 +279,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.onMessageSuccess: callback.fail(1, "no implement: " + methodType); break; - case ExtSdkMethodType.onMessageReadAck: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onMessageDeliveryAck: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.onMessageStatusChanged: - callback.fail(1, "no implement: " + methodType); - break; /// EMConversation case ExtSdkMethodType.getUnreadMsgCount: @@ -339,18 +287,9 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getMsgCount: ExtSdkConversationWrapper.getInstance().getMsgCount(jsonParams, methodType, callback); break; - case ExtSdkMethodType.markAllMessagesAsRead: - ExtSdkConversationWrapper.getInstance().markAllMessagesAsRead(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.markMessageAsRead: - ExtSdkConversationWrapper.getInstance().markMessageAsRead(jsonParams, methodType, callback); - break; case ExtSdkMethodType.syncConversationExt: ExtSdkConversationWrapper.getInstance().syncConversationExt(jsonParams, methodType, callback); break; - case ExtSdkMethodType.syncConversationName: - callback.fail(1, "no implement: " + methodType); - break; case ExtSdkMethodType.removeMessage: ExtSdkConversationWrapper.getInstance().removeMessage(jsonParams, methodType, callback); break; @@ -369,17 +308,10 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.insertMessage: ExtSdkConversationWrapper.getInstance().insertMessage(jsonParams, methodType, callback); break; - case ExtSdkMethodType.appendMessage: - ExtSdkConversationWrapper.getInstance().appendMessage(jsonParams, methodType, callback); - break; case ExtSdkMethodType.updateConversationMessage: ExtSdkConversationWrapper.getInstance().updateConversationMessage(jsonParams, methodType, callback); break; - // 根据消息id获取消息 - case ExtSdkMethodType.loadMsgWithId: - ExtSdkConversationWrapper.getInstance().loadMsgWithId(jsonParams, methodType, callback); - break; // 根据起始消息id获取消息 case ExtSdkMethodType.loadMsgWithStartId: ExtSdkConversationWrapper.getInstance().loadMsgWithStartId(jsonParams, methodType, callback); @@ -413,15 +345,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getChatRoom: ExtSdkChatRoomManagerWrapper.getInstance().getChatRoom(jsonParams, methodType, callback); break; - case ExtSdkMethodType.getAllChatRooms: - ExtSdkChatRoomManagerWrapper.getInstance().getAllChatRooms(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.createChatRoom: - ExtSdkChatRoomManagerWrapper.getInstance().createChatRoom(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.destroyChatRoom: - ExtSdkChatRoomManagerWrapper.getInstance().destroyChatRoom(jsonParams, methodType, callback); - break; case ExtSdkMethodType.changeChatRoomSubject: ExtSdkChatRoomManagerWrapper.getInstance().changeChatRoomSubject(jsonParams, methodType, callback); break; @@ -500,18 +423,12 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getJoinedGroups: ExtSdkGroupManagerWrapper.getInstance().getJoinedGroups(jsonParams, methodType, callback); break; - case ExtSdkMethodType.getGroupsWithoutPushNotification: - ExtSdkGroupManagerWrapper.getInstance().getGroupsWithoutPushNotification(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getJoinedGroupsFromServer: - ExtSdkGroupManagerWrapper.getInstance().getJoinedGroupsFromServer(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getPublicGroupsFromServer: - ExtSdkGroupManagerWrapper.getInstance().getPublicGroupsFromServer(jsonParams, methodType, callback); - break; case ExtSdkMethodType.createGroup: ExtSdkGroupManagerWrapper.getInstance().createGroup(jsonParams, methodType, callback); break; + case ExtSdkMethodType.updateGroupConfigs: + ExtSdkGroupManagerWrapper.getInstance().updateGroupConfigs(jsonParams, methodType, callback); + break; case ExtSdkMethodType.getGroupSpecificationFromServer: ExtSdkGroupManagerWrapper.getInstance().getGroupSpecificationFromServer(jsonParams, methodType, callback); break; @@ -638,9 +555,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.fetchMembersAttributesFromGroup: ExtSdkGroupManagerWrapper.getInstance().fetchMembersAttributes(jsonParams, methodType, callback); break; - case ExtSdkMethodType.ignoreGroupPush: - callback.fail(1, "no implement: " + methodType); - break; /// EMGroupManagerListener case ExtSdkMethodType.onGroupChanged: @@ -648,71 +562,25 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non break; /// EMPushManager - case ExtSdkMethodType.getImPushConfig: - ExtSdkPushManagerWrapper.getInstance().getImPushConfig(jsonParams, methodType, callback); - break; case ExtSdkMethodType.getImPushConfigFromServer: ExtSdkPushManagerWrapper.getInstance().getImPushConfigFromServer(jsonParams, methodType, callback); break; case ExtSdkMethodType.updatePushNickname: ExtSdkPushManagerWrapper.getInstance().updatePushNickname(jsonParams, methodType, callback); break; - case ExtSdkMethodType.updateHMSPushToken: - ExtSdkPushManagerWrapper.getInstance().updateHMSPushToken(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.updateFCMPushToken: - ExtSdkPushManagerWrapper.getInstance().updateFCMPushToken(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.enableOfflinePush: - ExtSdkPushManagerWrapper.getInstance().enableOfflinePush(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.disableOfflinePush: - ExtSdkPushManagerWrapper.getInstance().disableOfflinePush(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getNoPushGroups: - ExtSdkPushManagerWrapper.getInstance().getNoPushGroups(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.setNoDisturbUsers: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.getNoDisturbUsersFromServer: - callback.fail(1, "no implement: " + methodType); - break; /// ImPushConfig - case ExtSdkMethodType.imPushNoDisturb: - callback.fail(1, "no implement: " + methodType); - break; case ExtSdkMethodType.updateImPushStyle: ExtSdkPushManagerWrapper.getInstance().updateImPushStyle(jsonParams, methodType, callback); break; - case ExtSdkMethodType.updateGroupPushService: - ExtSdkPushManagerWrapper.getInstance().updateGroupPushService(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getNoDisturbGroups: - callback.fail(1, "no implement: " + methodType); - break; - case ExtSdkMethodType.updateUserPushService: - ExtSdkPushManagerWrapper.getInstance().updateUserPushService(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getNoPushUsers: - ExtSdkPushManagerWrapper.getInstance().getNoPushUsers(jsonParams, methodType, callback); - break; /// EMUserInfoManager case ExtSdkMethodType.updateOwnUserInfo: ExtSdkUserInfoManagerWrapper.getInstance().updateOwnUserInfo(jsonParams, methodType, callback); break; - case ExtSdkMethodType.updateOwnUserInfoWithType: - ExtSdkUserInfoManagerWrapper.getInstance().updateOwnUserInfoWithType(jsonParams, methodType, callback); - break; case ExtSdkMethodType.fetchUserInfoById: ExtSdkUserInfoManagerWrapper.getInstance().fetchUserInfoByUserId(jsonParams, methodType, callback); break; - case ExtSdkMethodType.fetchUserInfoByIdWithType: - ExtSdkUserInfoManagerWrapper.getInstance().fetchUserInfoByIdWithType(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.translateMessage: ExtSdkChatManagerWrapper.getInstance().translateMessage(jsonParams, methodType, callback); break; @@ -750,13 +618,7 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.fetchReactionDetail: ExtSdkChatManagerWrapper.getInstance().fetchReactionDetail(jsonParams, methodType, callback); break; - case ExtSdkMethodType.reportMessage: - ExtSdkChatManagerWrapper.getInstance().reportMessage(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.fetchConversationsFromServerWithPage: - ExtSdkChatManagerWrapper.getInstance().fetchConversationsFromServerWithPage(jsonParams, methodType, callback); - break; case ExtSdkMethodType.removeMessagesFromServerWithMsgIds: ExtSdkChatManagerWrapper.getInstance().removeMessagesFromServerWithMsgIds(jsonParams, methodType, callback); break; @@ -764,9 +626,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non ExtSdkChatManagerWrapper.getInstance().removeMessagesFromServerWithTs(jsonParams, methodType, callback); break; - case ExtSdkMethodType.onReadAckForGroupMessageUpdated: - callback.fail(1, "no implement: " + methodType); - break; case ExtSdkMethodType.onMessageReactionDidChange: callback.fail(1, "no implement: " + methodType); break; @@ -774,9 +633,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getReactionList: ExtSdkChatMessageWrapper.getInstance().getReactionList(jsonParams, methodType, callback); break; - case ExtSdkMethodType.groupAckCount: - ExtSdkChatMessageWrapper.getInstance().groupAckCount(jsonParams, methodType, callback); - break; case ExtSdkMethodType.fetchChatThreadDetail: ExtSdkChatThreadManagerWrapper.getInstance().fetchChatThreadDetail(jsonParams, methodType, callback); @@ -821,9 +677,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non ExtSdkChatThreadManagerWrapper.getInstance().getThreadConversation(jsonParams, methodType, callback); break; - case ExtSdkMethodType.reportPushAction: - ExtSdkPushManagerWrapper.getInstance().reportPushAction(jsonParams, methodType, callback); - break; case ExtSdkMethodType.setConversationSilentMode: ExtSdkPushManagerWrapper.getInstance().setConversationSilentMode(jsonParams, methodType, callback); break; @@ -852,30 +705,15 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.fetchChatRoomAttributes: ExtSdkChatRoomManagerWrapper.getInstance().fetchChatRoomAttributes(jsonParams, methodType, callback); break; - case ExtSdkMethodType.fetchChatRoomAllAttributes: - ExtSdkChatRoomManagerWrapper.getInstance().fetchChatRoomAllAttributes(jsonParams, methodType, callback); - break; case ExtSdkMethodType.setChatRoomAttributes: ExtSdkChatRoomManagerWrapper.getInstance().setChatRoomAttributes(jsonParams, methodType, callback); break; case ExtSdkMethodType.removeChatRoomAttributes: ExtSdkChatRoomManagerWrapper.getInstance().removeChatRoomAttributes(jsonParams, methodType, callback); break; - case ExtSdkMethodType.getConversationsFromServerWithCursor: - ExtSdkChatManagerWrapper.getInstance().getConversationsFromServerWithCursor(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getPinnedConversationsFromServerWithCursor: - ExtSdkChatManagerWrapper.getInstance().getPinnedConversationsFromServerWithCursor(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.getConversationsFromDBWithCursor: - ExtSdkChatManagerWrapper.getInstance().getConversationsFromDBWithCursor(jsonParams, methodType, callback); - break; case ExtSdkMethodType.pinConversation: ExtSdkChatManagerWrapper.getInstance().pinConversation(jsonParams, methodType, callback); break; - case ExtSdkMethodType.modifyMessage: - ExtSdkChatManagerWrapper.getInstance().modifyMessage(jsonParams, methodType, callback); - break; case ExtSdkMethodType.downloadAndParseCombineMessage: ExtSdkChatManagerWrapper.getInstance().downloadAndParseCombineMessage(jsonParams, methodType, callback); break; @@ -898,12 +736,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.getContact: ExtSdkContactManagerWrapper.getInstance().getContact(jsonParams, methodType, callback); break; - case ExtSdkMethodType.fetchAllContacts: - ExtSdkContactManagerWrapper.getInstance().fetchAllContacts(jsonParams, methodType, callback); - break; - case ExtSdkMethodType.fetchContacts: - ExtSdkContactManagerWrapper.getInstance().fetchContacts(jsonParams, methodType, callback); - break; case ExtSdkMethodType.getPinInfo: ExtSdkChatMessageWrapper.getInstance().getPinInfo(jsonParams, methodType, callback); @@ -917,9 +749,6 @@ public void callSdkApi(@NonNull String methodType, @Nullable Object params, @Non case ExtSdkMethodType.deleteRemoteAndLocalConversationsMark: ExtSdkChatManagerWrapper.getInstance().deleteRemoteAndLocalConversationsMark(jsonParams, methodType, callback); break; - case ExtSdkMethodType.fetchConversationsByOptions: - ExtSdkChatManagerWrapper.getInstance().fetchConversationsByOptions(jsonParams, methodType, callback); - break; case ExtSdkMethodType.deleteAllMessageAndConversation: ExtSdkChatManagerWrapper.getInstance().deleteAllMessageAndConversation(jsonParams, methodType, callback); break; diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkGroupManagerWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkGroupManagerWrapper.java index 1d795c7f..30218e29 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkGroupManagerWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkGroupManagerWrapper.java @@ -8,12 +8,13 @@ import com.hyphenate.chat.EMClient; import com.hyphenate.chat.EMCursorResult; import com.hyphenate.chat.EMGroup; -import com.hyphenate.chat.EMGroupInfo; +import com.hyphenate.chat.EMGroupConfigs; +import com.hyphenate.chat.EMGroupManager; import com.hyphenate.chat.EMGroupMemberInfo; -import com.hyphenate.chat.EMGroupOptions; import com.hyphenate.chat.EMMucSharedFile; import com.hyphenate.exceptions.HyphenateException; import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -39,7 +40,6 @@ public void getGroupWithId(JSONObject param, String channelName, ExtSdkCallback } public void getJoinedGroups(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { - EMClient.getInstance().groupManager().loadAllGroups(); List groups = EMClient.getInstance().groupManager().getAllGroups(); List groupList = new ArrayList<>(); for (EMGroup group : groups) { @@ -48,73 +48,6 @@ public void getJoinedGroups(JSONObject param, String channelName, ExtSdkCallback onSuccess(result, channelName, groupList); } - public void getGroupsWithoutPushNotification(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - onSuccess(result, channelName, null); - } - - public void getJoinedGroupsFromServer(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - - int pageSize = 0; - if (param.has("pageSize")) { - pageSize = param.getInt("pageSize"); - } - int pageNum = 0; - if (param.has("pageNum")) { - pageNum = param.getInt("pageNum"); - } - boolean needRole = false; - if (param.has("needRole")) { - needRole = param.getBoolean("needRole"); - } - boolean needMemberCount = false; - if (param.has("needMemberCount")) { - needMemberCount = param.getBoolean("needMemberCount"); - } - - EMClient.getInstance().groupManager().asyncGetJoinedGroupsFromServer( - pageNum, pageSize, needMemberCount, needRole, new EMValueCallBack>() { - @Override - public void onSuccess(List value) { - List groupList = new ArrayList<>(); - for (EMGroup group : value) { - groupList.add(ExtSdkGroupHelper.toJson(group)); - } - ExtSdkWrapper.onSuccess(result, channelName, groupList); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - - public void getPublicGroupsFromServer(JSONObject param, String channelName, ExtSdkCallback result) - throws JSONException { - int pageSize = 0; - if (param.has("pageSize")) { - pageSize = param.getInt("pageSize"); - } - String cursor = null; - if (param.has("cursor")) { - cursor = param.getString("cursor"); - } - EMClient.getInstance().groupManager().asyncGetPublicGroupsFromServer( - pageSize, cursor, new EMValueCallBack>() { - @Override - public void onSuccess(EMCursorResult value) { - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkCursorResultHelper.toJson(value)); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - public void createGroup(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { String groupName = null; @@ -146,9 +79,29 @@ public void createGroup(JSONObject param, String channelName, ExtSdkCallback res inviteReason = param.getString("inviteReason"); } - EMGroupOptions options = ExtSdkGroupOptionsHelper.fromJson(param.getJSONObject("options")); + EMGroupConfigs configs = ExtSdkGroupConfigsHelper.fromJson(param.getJSONObject("configs")); EMClient.getInstance().groupManager().asyncCreateGroup( - groupName, groupAvatar, desc, members, inviteReason, options, new EMValueCallBack() { + groupName, groupAvatar, desc, members, inviteReason, configs, new EMValueCallBack() { + @Override + public void onSuccess(EMGroup value) { + ExtSdkWrapper.onSuccess(result, channelName, ExtSdkGroupHelper.toJson(value)); + } + + @Override + public void onError(int error, String errorMsg) { + ExtSdkWrapper.onError(result, error, errorMsg); + } + }); + } + + public void updateGroupConfigs(JSONObject param, String channelName, ExtSdkCallback result) throws JSONException { + String groupId = param.getString("group_id"); + int types = param.getInt("types"); + EMGroupConfigs configs = ExtSdkGroupConfigsHelper.fromJson(param.getJSONObject("configs")); + EnumSet configsTypes = + InternalConvertHelper.groupConfigsTypeFromInt(types); + EMClient.getInstance().groupManager().asyncUpdateGroupConfigs( + groupId, configsTypes, configs, new EMValueCallBack() { @Override public void onSuccess(EMGroup value) { ExtSdkWrapper.onSuccess(result, channelName, ExtSdkGroupHelper.toJson(value)); @@ -1295,17 +1248,6 @@ public void onRequestToJoinAccepted(String groupId, String groupName, String acc ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupChanged, data); } - @Override - public void onRequestToJoinDeclined(String groupId, String groupName, String decliner, String reason) { - // Map data = new HashMap<>(); - // data.put("type", "onRequestToJoinDeclined"); - // data.put("groupId", groupId); - // data.put("groupName", groupName); - // data.put("decliner", decliner); - // data.put("reason", reason); - // ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupChanged, data); - } - @Override public void onRequestToJoinDeclined(String groupId, String groupName, String decliner, String reason, String applicant) { @@ -1414,15 +1356,6 @@ public void onOwnerChanged(String groupId, String newOwner, String oldOwner) { ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupChanged, data); } - @Override - public void onMemberJoined(String groupId, String member) { - Map data = new HashMap<>(); - data.put("type", "onMemberJoined"); - data.put("groupId", groupId); - data.put("member", member); - ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupChanged, data); - } - @Override public void onMembersJoined(String groupId, List members) { Map data = new HashMap<>(); @@ -1432,15 +1365,6 @@ public void onMembersJoined(String groupId, List members) { ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupChanged, data); } - @Override - public void onMemberExited(String groupId, String member) { - Map data = new HashMap<>(); - data.put("type", "onMemberExited"); - data.put("groupId", groupId); - data.put("member", member); - ExtSdkWrapper.onReceive(ExtSdkMethodType.onGroupChanged, data); - } - @Override public void onMembersExited(String groupId, List members) { Map data = new HashMap<>(); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkHelper.java b/modules/java/com/chatsdk/dispatch/ExtSdkHelper.java index a89c2e52..dac1006d 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkHelper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkHelper.java @@ -9,18 +9,17 @@ import com.hyphenate.chat.EMCombineMessageBody; import com.hyphenate.chat.EMContact; import com.hyphenate.chat.EMConversation; -import com.hyphenate.chat.EMConversationFilter; import com.hyphenate.chat.EMCursorResult; import com.hyphenate.chat.EMCustomMessageBody; import com.hyphenate.chat.EMDeviceInfo; import com.hyphenate.chat.EMFetchMessageOption; import com.hyphenate.chat.EMFileMessageBody; import com.hyphenate.chat.EMGroup; +import com.hyphenate.chat.EMGroupConfigs; import com.hyphenate.chat.EMGroupInfo; import com.hyphenate.chat.EMGroupManager; import com.hyphenate.chat.EMGroupMemberInfo; -import com.hyphenate.chat.EMGroupOptions; -import com.hyphenate.chat.EMGroupReadAck; +import com.hyphenate.chat.EMGroupReadReceipt; import com.hyphenate.chat.EMImageMessageBody; import com.hyphenate.chat.EMLanguage; import com.hyphenate.chat.EMLocationMessageBody; @@ -31,6 +30,7 @@ import com.hyphenate.chat.EMMessageReaction; import com.hyphenate.chat.EMMessageReactionChange; import com.hyphenate.chat.EMMessageReactionOperation; +import com.hyphenate.chat.EMMessageReadReceipt; import com.hyphenate.chat.EMMucSharedFile; import com.hyphenate.chat.EMNormalFileMessageBody; import com.hyphenate.chat.EMOptions; @@ -50,8 +50,8 @@ import com.hyphenate.chat.EMVideoMessageBody; import com.hyphenate.chat.EMVoiceMessageBody; import com.hyphenate.exceptions.HyphenateException; -import com.hyphenate.push.EMPushConfig; import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -164,34 +164,37 @@ static int messageStatusToInt(EMMessage.Status status) { return 0; } - static EMGroupManager.EMGroupStyle groupStyleFromInt(int style) { - switch (style) { - case 0: - return EMGroupManager.EMGroupStyle.EMGroupStylePrivateOnlyOwnerInvite; + static EMOptions.AreaCode areaCodeFromInt(int areaCode) { + switch (areaCode) { case 1: - return EMGroupManager.EMGroupStyle.EMGroupStylePrivateMemberCanInvite; + return EMOptions.AreaCode.CN; case 2: - return EMGroupManager.EMGroupStyle.EMGroupStylePublicJoinNeedApproval; - case 3: - return EMGroupManager.EMGroupStyle.EMGroupStylePublicOpenJoin; + return EMOptions.AreaCode.NA; + case 4: + return EMOptions.AreaCode.EU; + case 8: + return EMOptions.AreaCode.AS; + case 16: + return EMOptions.AreaCode.JP; + case 32: + return EMOptions.AreaCode.IN; + default: + return EMOptions.AreaCode.GLOB; } - - return EMGroupManager.EMGroupStyle.EMGroupStylePrivateOnlyOwnerInvite; } - static int groupStyleToInt(EMGroupManager.EMGroupStyle style) { - switch (style) { - case EMGroupStylePrivateOnlyOwnerInvite: - return 0; - case EMGroupStylePrivateMemberCanInvite: - return 1; - case EMGroupStylePublicJoinNeedApproval: - return 2; - case EMGroupStylePublicOpenJoin: - return 3; - } - - return 0; + // Cross-platform contract mask: AllowInvites=1, MaxUsers=2, InviteNeedConfirm=4, + // JoinApprovalRequired=8, IsPublic=16, Ext=32. The Android enum bit order differs, + // so map bit by bit instead of using EMGroupConfigsType.toNativeMask. + static EnumSet groupConfigsTypeFromInt(int mask) { + EnumSet types = EnumSet.noneOf(EMGroupManager.EMGroupConfigsType.class); + if ((mask & 1) != 0) { types.add(EMGroupManager.EMGroupConfigsType.ALLOW_INVITES); } + if ((mask & 2) != 0) { types.add(EMGroupManager.EMGroupConfigsType.MAX_USERS); } + if ((mask & 4) != 0) { types.add(EMGroupManager.EMGroupConfigsType.INVITE_NEED_CONFIRM); } + if ((mask & 8) != 0) { types.add(EMGroupManager.EMGroupConfigsType.JOIN_APPROVAL_REQUIRED); } + if ((mask & 16) != 0) { types.add(EMGroupManager.EMGroupConfigsType.IS_PUBLIC); } + if ((mask & 32) != 0) { types.add(EMGroupManager.EMGroupConfigsType.EXT); } + return types; } static EMFileMessageBody.EMDownloadStatus downloadStatusFromInt(int downloadStatus) { @@ -289,8 +292,6 @@ static EMOptions fromJson(JSONObject json, Context context) throws JSONException EMOptions options = new EMOptions(); if (!json.optString("appKey").isEmpty()) { options.setAppKey(json.getString("appKey")); } if (!json.optString("appId").isEmpty()) { options.setAppId(json.getString("appId")); } - if (json.has("autoLogin")) { options.setAutoLogin(json.getBoolean("autoLogin")); } - if (json.has("requireAck")) { options.setRequireAck(json.getBoolean("requireAck")); } if (json.has("requireDeliveryAck")) { options.setRequireDeliveryAck(json.getBoolean("requireDeliveryAck")); } if (json.has("sortMessageByServerTime")) { options.setSortMessageByServerTime(json.getBoolean("sortMessageByServerTime")); } if (json.has("acceptInvitationAlways")) { options.setAcceptInvitationAlways(json.getBoolean("acceptInvitationAlways")); } @@ -308,34 +309,14 @@ static EMOptions fromJson(JSONObject json, Context context) throws JSONException if (!json.optString("restServer").isEmpty()) { options.setRestServer(json.getString("restServer")); } if (!json.optString("dnsUrl").isEmpty()) { options.setDnsUrl(json.getString("dnsUrl")); } } - if (json.has("areaCode")) { options.setAreaCode(json.getInt("areaCode")); } + if (json.has("areaCode")) { options.setAreaCode(InternalConvertHelper.areaCodeFromInt(json.getInt("areaCode"))); } + if (json.has("dataSyncType")) { + options.setDataSyncType(EMOptions.EMDataSyncType.fromNativeMask(json.getInt("dataSyncType"))); + } if (json.has("enableEmptyConversation")) { options.setLoadEmptyConversations(json.getBoolean("enableEmptyConversation")); } if (!json.optString("customDeviceName").isEmpty()) { options.setCustomDeviceName(json.getString("customDeviceName")); } if (json.has("customOSType")) { options.setCustomOSPlatform(json.getInt("customOSType")); } - if (json.has("pushConfig")) { - JSONObject pushConfig = json.getJSONObject("pushConfig"); - if (!pushConfig.optString("manufacturer").isEmpty()) { - EMPushConfig.Builder builder = new EMPushConfig.Builder(context); - String manufacturer = pushConfig.getString("manufacturer"); - if (manufacturer.equalsIgnoreCase("huawei")) { - builder.enableHWPush(); - options.setPushConfig(builder.build()); - } else { - String deviceId = pushConfig.optString("deviceId"); - if (!deviceId.isEmpty()) { - if (manufacturer.equalsIgnoreCase("google")) { builder.enableFCM(deviceId); } - else if (manufacturer.equalsIgnoreCase("meizu")) { builder.enableFCM(deviceId); } - else if (manufacturer.equalsIgnoreCase("xiaomi")) { builder.enableFCM(deviceId); } - else if (manufacturer.equalsIgnoreCase("oppo")) { builder.enableOppoPush(deviceId, ""); } - else if (manufacturer.equalsIgnoreCase("vivo")) { builder.enableFCM(deviceId); } - else { builder.enableFCM(deviceId); } - options.setPushConfig(builder.build()); - } - } - } - } - // 2024-04-16 4.5.0 if (json.has("enableTLS")) { options.setEnableTLSConnection(json.getBoolean("enableTLS")); } if (json.has("useReplacedMessageContents")) { options.setUseReplacedMessageContents(json.getBoolean("useReplacedMessageContents")); } @@ -356,7 +337,6 @@ static EMOptions fromJson(JSONObject json, Context context) throws JSONException // 2026-08-05 4.22.0 if (json.has("enableUserInfo")) { options.setEnableUserInfo(json.getBoolean("enableUserInfo")); } - if (json.has("enableAutoSyncContacts")) { options.setEnableAutoSyncContacts(json.getBoolean("enableAutoSyncContacts")); } // 2026-08-25 4.24.1 if (json.has("ntpServers")) { @@ -370,10 +350,6 @@ static EMOptions fromJson(JSONObject json, Context context) throws JSONException } } - // 2026-09-10 4.25.0 - if (json.has("enableChatroomConversation")) { options.setEnableChatroomConversation(json.getBoolean("enableChatroomConversation")); } - if (json.has("autoLoadConversations")) { options.setAutoLoadAllConversations(json.getBoolean("autoLoadConversations")); } - return options; } @@ -383,9 +359,7 @@ static Map toJson(EMOptions options) { } Map data = new HashMap<>(); data.put("appKey", options.getAppKey()); - data.put("autoLogin", options.getAutoLogin()); // data.put("debugModel", options.) - data.put("requireAck", options.getRequireAck()); data.put("requireDeliveryAck", options.getRequireDeliveryAck()); data.put("sortMessageByServerTime", options.isSortMessageByServerTime()); data.put("acceptInvitationAlways", options.getAcceptInvitationAlways()); @@ -403,6 +377,7 @@ static Map toJson(EMOptions options) { data.put("restServer", options.getRestServer()); data.put("dnsUrl", options.getDnsUrl()); data.put("areaCode", options.getAreaCode()); + data.put("dataSyncType", EMOptions.EMDataSyncType.toNativeMask(options.getDataSyncType())); data.put("enableEmptyConversation", options.isLoadEmptyConversations()); data.put("customDeviceName", options.getCustomDeviceName()); data.put("customOSType", options.getCustomOSPlatform()); @@ -417,10 +392,7 @@ static Map toJson(EMOptions options) { data.put("webSocketPort", options.getWebSocketPort()); data.put("dohVendor", options.getDohVendor()); data.put("enableUserInfo", options.isEnableUserInfo()); - data.put("enableAutoSyncContacts", options.isEnableAutoSyncContacts()); data.put("ntpServers", options.getNtpServers()); - data.put("enableChatroomConversation", options.isEnableChatroomConversation()); - data.put("autoLoadConversations", options.isAutoLoadAllConversations()); return data; } } @@ -444,16 +416,17 @@ static Map toJson(EMGroup group) { data.put("muteList", group.getMuteList()); data.put("messageBlocked", group.isMsgBlocked()); data.put("isAllMemberMuted", group.isAllMemberMuted()); + data.put("isDisabled", group.isDisabled()); data.put("permissionType", InternalConvertHelper.intTypeFromGroupPermissionType(group.getGroupPermissionType())); - Map option = new HashMap<>(); - option.put("maxCount", group.getMaxUserCount()); - option.put("inviteNeedConfirm", group.isMemberAllowToInvite()); - option.put("ext", group.getExtension()); - option.put("isDisabled", group.isDisabled()); - option.put("isMemberOnly", group.isMemberOnly()); - data.put("options", option); + Map configs = new HashMap<>(); + configs.put("maxCount", group.getMaxUserCount()); + configs.put("ext", group.getExtension()); + configs.put("isPublic", group.isPublic()); + configs.put("joinApprovalRequired", group.isJoinApprovalRequired()); + configs.put("allowInvites", group.isMemberAllowToInvite()); + data.put("configs", configs); return data; } @@ -466,7 +439,7 @@ static Map toJson(EMGroupMemberInfo memberInfo) { } Map data = new HashMap<>(); - data.put("memberId", memberInfo.getMemberId()); + data.put("memberId", memberInfo.getUserId()); data.put("joinedTimestamp", memberInfo.getJoinTime()); data.put("role", InternalConvertHelper.intTypeFromGroupPermissionType(memberInfo.getRole())); if (memberInfo.getNamecard() != null) { @@ -504,23 +477,27 @@ static Map toJson(EMMucSharedFile file) { } } -class ExtSdkGroupOptionsHelper { +class ExtSdkGroupConfigsHelper { - static EMGroupOptions fromJson(JSONObject json) throws JSONException { - EMGroupOptions options = new EMGroupOptions(); - if (json.has("maxCount")) { options.maxUsers = json.getInt("maxCount"); } - if (json.has("inviteNeedConfirm")) { options.inviteNeedConfirm = json.getBoolean("inviteNeedConfirm"); } - if (!json.optString("ext").isEmpty()) { options.extField = json.getString("ext"); } - if (json.has("style")) { options.style = InternalConvertHelper.groupStyleFromInt(json.getInt("style")); } - return options; + static EMGroupConfigs fromJson(JSONObject json) throws JSONException { + EMGroupConfigs configs = new EMGroupConfigs(); + if (json.has("maxCount")) { configs.maxUsers = json.getInt("maxCount"); } + if (json.has("inviteNeedConfirm")) { configs.inviteNeedConfirm = json.getBoolean("inviteNeedConfirm"); } + if (!json.optString("ext").isEmpty()) { configs.extField = json.getString("ext"); } + if (json.has("isPublic")) { configs.isPublic = json.getBoolean("isPublic"); } + if (json.has("joinApprovalRequired")) { configs.joinApprovalRequired = json.getBoolean("joinApprovalRequired"); } + if (json.has("allowInvites")) { configs.allowInvites = json.getBoolean("allowInvites"); } + return configs; } - static Map toJson(EMGroupOptions options) { + static Map toJson(EMGroupConfigs configs) { Map data = new HashMap<>(); - data.put("maxCount", options.maxUsers); - data.put("inviteNeedConfirm", options.inviteNeedConfirm); - data.put("ext", options.extField); - data.put("style", InternalConvertHelper.groupStyleToInt(options.style)); + data.put("maxCount", configs.maxUsers); + data.put("inviteNeedConfirm", configs.inviteNeedConfirm); + data.put("ext", configs.extField); + data.put("isPublic", configs.isPublic); + data.put("joinApprovalRequired", configs.joinApprovalRequired); + data.put("allowInvites", configs.allowInvites); return data; } } @@ -701,14 +678,9 @@ static EMMessage fromJson(JSONObject json) throws JSONException { if (!json.optString("to").isEmpty()) { message.setTo(json.getString("to")); } if (!json.optString("from").isEmpty()) { message.setFrom(json.getString("from")); } - if (json.has("hasReadAck")) { message.setAcked(json.getBoolean("hasReadAck")); } - if (json.has("status") && InternalConvertHelper.messageStatusFromInt(json.getInt("status")) == EMMessage.Status.SUCCESS) { - if (json.has("hasRead")) { message.setUnread(!json.getBoolean("hasRead")); } - } // sdk auto invoke // message.setDelivered(json.getBoolean("hasDeliverAck")); - if (json.has("needGroupAck")) { message.setIsNeedGroupAck(json.getBoolean("needGroupAck")); } - if (json.has("groupAckCount")) { message.setGroupAckCount(json.getInt("groupAckCount")); } + if (json.has("isNeedReadReceipt")) { message.setIsNeedReadReceipt(json.getBoolean("isNeedReadReceipt")); } if (json.has("localTime")) { message.setLocalTime(json.getLong("localTime")); } if (json.has("serverTime")) { message.setMsgTime(json.getLong("serverTime")); } if (json.has("status")) { message.setStatus(InternalConvertHelper.messageStatusFromInt(json.getInt("status"))); } @@ -773,7 +745,7 @@ static Map toJson(EMMessage message) { } data.put("from", message.getFrom()); data.put("to", message.getTo()); - data.put("hasReadAck", message.isAcked()); + data.put("isPeerRead", message.isPeerRead()); data.put("hasDeliverAck", message.isDelivered()); data.put("localTime", message.localTime()); data.put("serverTime", message.getMsgTime()); @@ -782,9 +754,9 @@ static Map toJson(EMMessage message) { data.put("direction", message.direct() == EMMessage.Direct.SEND ? "send" : "rec"); data.put("conversationId", message.conversationId()); data.put("msgId", message.getMsgId()); - data.put("hasRead", !message.isUnread()); - data.put("needGroupAck", message.isNeedGroupAck()); - data.put("groupAckCount", message.groupAckCount()); + data.put("isRead", message.isRead()); + data.put("isNeedReadReceipt", message.isNeedReadReceipt()); + data.put("groupReadReceiptCount", message.readReceiptCount()); data.put("isChatThread", message.isChatThreadMessage()); data.put("isOnline", message.isOnlineState()); data.put("deliverOnlineOnly", message.isDeliverOnlineOnly()); @@ -826,16 +798,30 @@ static Map toJson(EMSenderInfo senderInfo) { } class ExtSdkGroupAckHelper { - static Map toJson(EMGroupReadAck ack) { + static Map toJson(EMGroupReadReceipt ack) { + if (ack == null) { + return null; + } Map data = new HashMap<>(); data.put("msg_id", ack.getMsgId()); data.put("ack_id", ack.getAckId()); - data.put("from", ack.getFrom()); + data.put("from", ExtSdkGroupMemberInfoHelper.toJson(ack.getFrom())); data.put("count", ack.getCount()); data.put("timestamp", ack.getTimestamp()); - if (ack.getContent() != null) { - data.put("content", ack.getContent()); + return data; + } +} + +class ExtSdkMessageReadReceiptHelper { + static Map toJson(EMMessageReadReceipt receipt) { + if (receipt == null) { + return null; } + Map data = new HashMap<>(); + data.put("msg_id", receipt.getMessageId()); + data.put("conv_id", receipt.getConversationId()); + data.put("isPeerReceipt", receipt.isPeerReceipt()); + data.put("readCount", receipt.getReadCount()); return data; } } @@ -1062,7 +1048,6 @@ static EMImageMessageBody imageBodyFromJson(JSONObject json) throws JSONExceptio if (!json.optString("secret").isEmpty()) { body.setSecret(json.getString("secret")); } if (!json.optString("thumbnailLocalPath").isEmpty()) { body.setThumbnailLocalPath(json.getString("thumbnailLocalPath")); } if (!json.optString("thumbnailRemotePath").isEmpty()) { body.setThumbnailUrl(json.getString("thumbnailRemotePath")); } - if (!json.optString("thumbnailSecret").isEmpty()) { body.setThumbnailSecret(json.getString("thumbnailSecret")); } if (json.has("fileSize")) { body.setFileLength(json.getInt("fileSize")); } if (json.has("width") && json.has("height")) { body.setThumbnailSize(json.getInt("width"), json.getInt("height")); } if (json.has("sendOriginalImage")) { body.setSendOriginalImage(json.getBoolean("sendOriginalImage")); } @@ -1081,10 +1066,9 @@ static Map imageBodyToJson(EMImageMessageBody body) { data.put("fileStatus", InternalConvertHelper.downloadStatusToInt(body.downloadStatus())); data.put("thumbnailLocalPath", body.thumbnailLocalPath()); data.put("thumbnailRemotePath", body.getThumbnailUrl()); - data.put("thumbnailSecret", body.getThumbnailSecret()); data.put("height", body.getHeight()); data.put("width", body.getWidth()); - data.put("sendOriginalImage", body.isSendOriginalImage()); + data.put("sendOriginalImage", body.isOriginalImage()); data.put("fileSize", body.getFileSize()); data.put("bigImageLocalPath", body.getBigImageLocalUrl()); data.put("bigImageRemotePath", body.getBigImageRemoteUrl()); @@ -1168,6 +1152,10 @@ static Map toJson(EMConversation conversation) { Map data = new HashMap<>(); data.put("convId", conversation.conversationId()); data.put("convType", InternalConvertHelper.conversationTypeToInt(conversation.getType())); + if (conversation.getConversationName() != null) + data.put("name", conversation.getConversationName()); + if (conversation.getConversationAvatar() != null) + data.put("avatar", conversation.getConversationAvatar()); data.put("isChatThread", conversation.isChatThread()); data.put("isPinned", conversation.isPinned()); data.put("pinnedTime", conversation.getPinnedTime()); @@ -1267,8 +1255,8 @@ static Map toJson(EMCursorResult result) { jsonList.add(ExtSdkChatRoomHelper.toJson((EMChatRoom)obj)); } - if (obj instanceof EMGroupReadAck) { - jsonList.add(ExtSdkGroupAckHelper.toJson((EMGroupReadAck)obj)); + if (obj instanceof EMGroupReadReceipt) { + jsonList.add(ExtSdkGroupAckHelper.toJson((EMGroupReadReceipt)obj)); } if (obj instanceof String) { @@ -1658,7 +1646,6 @@ static EMFetchMessageOption fromJson(JSONObject json) throws JSONException { if (json.has("needSave")) { options.setIsSave(json.getBoolean("needSave")); } if (json.has("startTs")) { options.setStartTime(json.getLong("startTs")); } if (json.has("endTs")) { options.setEndTime(json.getLong("endTs")); } - if (!json.optString("from").isEmpty()) { options.setFrom(json.getString("from")); } if (json.has("senders") && json.getJSONArray("senders").length() > 0) { List fromIds = new ArrayList<>(); JSONArray senders = json.getJSONArray("senders"); @@ -1707,41 +1694,6 @@ static Map toJson(EMMessagePinInfo info) { } } -class ExtSdkConversationFilterHelper { - static EMConversationFilter fromJson(JSONObject json) throws JSONException { - EMConversation.EMMarkType markType = EMConversation.EMMarkType.values()[json.getInt("mark")]; - int pageSize = json.has("pageSize") ? json.getInt("pageSize") : 20; - EMConversationFilter filter = new EMConversationFilter(markType, pageSize); - return filter; - } - - static String cursor(JSONObject json) throws JSONException { - if (json.has("cursor")) { - return json.getString("cursor"); - } else { - return null; - } - } - - static Boolean pinned(JSONObject json) throws JSONException { - if (json.has("pinned")) { - return json.getBoolean("pinned"); - } else { - return false; - } - } - - static Boolean hasMark(JSONObject json) throws JSONException { return json.has("mark"); } - - static int pageSize(JSONObject json) throws JSONException { - if (json.has("pageSize")) { - return json.getInt("pageSize"); - } else { - return 0; - } - } -} - class ExtSdkRecalledMessageInfoHelper { static Map toJson(EMRecallMessageInfo info) { Map data = new HashMap<>(); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkPushManagerWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkPushManagerWrapper.java index 565dac57..7b777e87 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkPushManagerWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkPushManagerWrapper.java @@ -26,11 +26,6 @@ public static class SingleHolder { public static ExtSdkPushManagerWrapper getInstance() { return ExtSdkPushManagerWrapper.SingleHolder.instance; } - public void getImPushConfig(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - EMPushConfigs configs = EMClient.getInstance().pushManager().getPushConfigs(); - onSuccess(result, channelName, ExtSdkPushConfigsHelper.toJson(configs)); - } - public void getImPushConfigFromServer(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { EMClient.getInstance().pushManager().asyncGetPushConfigsFromServer(new EMValueCallBack() { @@ -61,22 +56,6 @@ public void onError(int code, String error) { }); } - public void enableOfflinePush(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - onSuccess(result, channelName, null); - } - - public void disableOfflinePush(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - onSuccess(result, channelName, null); - } - - public void getNoPushGroups(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - onSuccess(result, channelName, null); - } - - public void getNoPushUsers(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - onSuccess(result, channelName, null); - } - public void updateImPushStyle(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { EMPushManager.DisplayStyle style = params.getInt("pushStyle") == 0 ? EMPushManager.DisplayStyle.SimpleBanner : EMPushManager.DisplayStyle.MessageSummary; @@ -93,32 +72,6 @@ public void onError(int code, String error) { }); } - public void updateGroupPushService(JSONObject params, String channelName, ExtSdkCallback result) - throws JSONException { - onSuccess(result, channelName, null); - } - - public void updateUserPushService(JSONObject params, String channelName, ExtSdkCallback result) - throws JSONException { - onSuccess(result, channelName, null); - } - - public void updateHMSPushToken(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - String token = params.getString("token"); - EMClient.getInstance().sendHMSPushTokenToServer(token); - onSuccess(result, channelName, token); - } - - public void updateFCMPushToken(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - String token = params.getString("token"); - EMClient.getInstance().sendFCMTokenToServer(token); - onSuccess(result, channelName, token); - } - - public void reportPushAction(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { - // TODO: - } - public void setConversationSilentMode(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { String conversationId = params.getString("conversationId"); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkUserInfoManagerWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkUserInfoManagerWrapper.java index 4795e5be..24df6e4e 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkUserInfoManagerWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkUserInfoManagerWrapper.java @@ -129,37 +129,6 @@ public void onError(int error, String errorMsg) { }); } - public void updateOwnUserInfoWithType(JSONObject params, String channelName, ExtSdkCallback result) - throws JSONException { - int userInfoTypeInt = params.getInt("userInfoType"); - String userInfoTypeValue = params.getString("userInfoValue"); - EMUserInfo.EMUserInfoType userInfoType = getUserInfoTypeFromInt(userInfoTypeInt); - EMClient.getInstance().userInfoManager().updateOwnInfoByAttribute( - userInfoType, userInfoTypeValue, new EMValueCallBack() { - @Override - public void onSuccess(String value) { - if (value != null && value.length() > 0) { - JSONObject obj; - try { - obj = new JSONObject(value); - String userId = EMClient.getInstance().getCurrentUser(); - obj.put("userId", userId); - EMUserInfo userInfo = ExtSdkUserInfoHelper.fromJson(obj); - ExtSdkWrapper.onSuccess(result, channelName, ExtSdkUserInfoHelper.toJson(userInfo)); - - } catch (JSONException e) { - ExtSdkWrapper.onError(result, e, null); - } - } - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - public void fetchUserInfoByUserId(JSONObject params, String channelName, ExtSdkCallback result) throws JSONException { JSONArray userIdArray = params.getJSONArray("userIds"); @@ -183,37 +152,6 @@ public void onError(int error, String errorMsg) { }); } - public void fetchUserInfoByIdWithType(JSONObject params, String channelName, ExtSdkCallback result) - throws JSONException { - JSONArray userIdArray = params.getJSONArray("userIds"); - JSONArray userInfoTypeArray = params.getJSONArray("userInfoTypes"); - - String[] userIds = new String[userIdArray.length()]; - for (int i = 0; i < userIdArray.length(); i++) { - userIds[i] = (String)userIdArray.get(i); - } - - EMUserInfo.EMUserInfoType[] infoTypes = new EMUserInfo.EMUserInfoType[userInfoTypeArray.length()]; - for (int i = 0; i < userInfoTypeArray.length(); i++) { - EMUserInfo.EMUserInfoType infoType = getUserInfoTypeFromInt((int)userInfoTypeArray.get(i)); - infoTypes[i] = infoType; - } - - EMClient.getInstance().userInfoManager().fetchUserInfoByAttribute( - userIds, infoTypes, new EMValueCallBack>() { - @Override - public void onSuccess(Map value) { - final Map rMap = generateMapFromMap(value); - ExtSdkWrapper.onSuccess(result, channelName, rMap); - } - - @Override - public void onError(int error, String errorMsg) { - ExtSdkWrapper.onError(result, error, errorMsg); - } - }); - } - Map generateMapFromMap(Map aMap) { Map resultMap = new HashMap<>(); @@ -225,49 +163,6 @@ Map generateMapFromMap(Map aMap) { return resultMap; } - private EMUserInfo.EMUserInfoType getUserInfoTypeFromInt(int value) { - EMUserInfo.EMUserInfoType infoType; - - switch (value) { - case 0: { - infoType = EMUserInfo.EMUserInfoType.NICKNAME; - } break; - - case 1: { - infoType = EMUserInfo.EMUserInfoType.AVATAR_URL; - } break; - - case 2: { - infoType = EMUserInfo.EMUserInfoType.EMAIL; - } break; - - case 3: { - infoType = EMUserInfo.EMUserInfoType.PHONE; - } break; - - case 4: { - infoType = EMUserInfo.EMUserInfoType.GENDER; - } break; - - case 5: { - infoType = EMUserInfo.EMUserInfoType.SIGN; - } break; - - case 6: { - infoType = EMUserInfo.EMUserInfoType.BIRTH; - } break; - - case 100: { - infoType = EMUserInfo.EMUserInfoType.EXT; - } break; - - default: - throw new IllegalStateException("Unexpected value: " + value); - } - - return infoType; - } - private void registerEaseListener() { if (this.userInfoManagerListener != null) { EMClient.getInstance().userInfoManager().removeUserInfoManagerListener(this.userInfoManagerListener); diff --git a/modules/java/com/chatsdk/dispatch/ExtSdkWrapper.java b/modules/java/com/chatsdk/dispatch/ExtSdkWrapper.java index 19394e88..9bb4d4f8 100644 --- a/modules/java/com/chatsdk/dispatch/ExtSdkWrapper.java +++ b/modules/java/com/chatsdk/dispatch/ExtSdkWrapper.java @@ -78,7 +78,7 @@ protected void mergeMessageBody(EMMessageBody msgBody, EMMessage dbMsg) throws J dbimage.setSecret(image.getSecret()); dbimage.setFileLength(image.getFileSize()); dbimage.setDownloadStatus(image.downloadStatus()); - dbimage.setSendOriginalImage(image.isSendOriginalImage()); + dbimage.setSendOriginalImage(image.isOriginalImage()); dbimage.setThumbnailLocalPath(image.thumbnailLocalPath()); dbimage.setThumbnailDownloadStatus(image.thumbnailDownloadStatus()); dbimage.setThumbnailUrl(image.getThumbnailUrl()); @@ -135,11 +135,9 @@ protected void mergeMessage(EMMessage msg, EMMessage dbMsg) throws JSONException dbMsg.setMsgTime(msg.getMsgTime()); dbMsg.setLocalTime(msg.localTime()); dbMsg.setStatus(msg.status()); - dbMsg.setAcked(msg.isAcked()); dbMsg.setIsChatThreadMessage(msg.isChatThreadMessage()); - dbMsg.setIsNeedGroupAck(msg.isNeedGroupAck()); + dbMsg.setIsNeedReadReceipt(msg.isNeedReadReceipt()); dbMsg.setDeliverAcked(msg.isDelivered()); - dbMsg.setUnread(msg.isUnread()); dbMsg.setListened(msg.isListened()); dbMsg.setReceiverList(msg.receiverList()); // dbMsg.setPriority(msg.setPriority()); diff --git a/modules/objc/common/ExtSdkMethodTypeObjc.h b/modules/objc/common/ExtSdkMethodTypeObjc.h index c6a26c85..29ec274a 100644 --- a/modules/objc/common/ExtSdkMethodTypeObjc.h +++ b/modules/objc/common/ExtSdkMethodTypeObjc.h @@ -2,24 +2,19 @@ #pragma mark - EMClientWrapper static NSString *_Nonnull const ExtSdkMethodKeyInit = @"init"; -static NSString *_Nonnull const ExtSdkMethodKeyCreateAccount = @"createAccount"; static NSString *_Nonnull const ExtSdkMethodKeyLogin = @"login"; static NSString *_Nonnull const ExtSdkMethodKeyLogout = @"logout"; static NSString *_Nonnull const ExtSdkMethodKeyChangeAppKey = @"changeAppKey"; static NSString *_Nonnull const ExtSdkMethodKeyChangeAppId = @"changeAppId"; -static NSString *_Nonnull const ExtSdkMethodKeyIsLoggedInBefore = @"isLoggedInBefore"; -static NSString *_Nonnull const ExtSdkMethodKeyUploadLog = @"uploadLog"; static NSString *_Nonnull const ExtSdkMethodKeyCompressLogs = @"compressLogs"; static NSString *_Nonnull const ExtSdkMethodKeyKickDevice = @"kickDevice"; static NSString *_Nonnull const ExtSdkMethodKeyKickAllDevices = @"kickAllDevices"; -static NSString *_Nonnull const ExtSdkMethodKeyCurrentUser = @"currentUser"; // deprecated 2022.04.06 static NSString *_Nonnull const ExtSdkMethodKeyGetLoggedInDevicesFromServer = @"getLoggedInDevicesFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyGetToken = @"getToken"; -static NSString *_Nonnull const ExtSdkMethodKeyLoginWithAgoraToken = @"loginWithAgoraToken"; static NSString *_Nonnull const ExtSdkMethodKeyGetCurrentUser = @"getCurrentUser"; static NSString *_Nonnull const ExtSdkMethodKeyIsConnected = @"isConnected"; static NSString *_Nonnull const ExtSdkMethodKeyRenewToken = @"renewToken"; -// 2026-09-24 1.21.0 +static NSString *_Nonnull const ExtSdkMethodKeyBindDeviceToken = @"bindDeviceToken"; static NSString *_Nonnull const ExtSdkMethodKeyBindPushKitToken = @"bindPushKitToken"; static NSString *_Nonnull const ExtSdkMethodKeyUnbindPushKitToken = @"unbindPushKitToken"; @@ -32,6 +27,9 @@ static NSString *_Nonnull const ExtSdkMethodKeyOnTokenWillExpire = @"onTokenWill static NSString *_Nonnull const ExtSdkMethodKeyOnTokenDidExpire = @"onTokenDidExpire"; static NSString *_Nonnull const ExtSdkMethodKeyOnOfflineMessageSyncStart = @"onOfflineMessageSyncStart"; static NSString *_Nonnull const ExtSdkMethodKeyOnOfflineMessageSyncFinish = @"onOfflineMessageSyncFinish"; +static NSString *_Nonnull const ExtSdkMethodKeyOnDataSyncStart = @"onDataSyncStart"; +static NSString *_Nonnull const ExtSdkMethodKeyOnDataSyncFinish = @"onDataSyncFinish"; +static NSString *_Nonnull const ExtSdkMethodKeyOnDatabaseOpened = @"onDatabaseOpened"; static NSString *_Nonnull const ExtSdkMethodKeyOnMultiDeviceEventContact = @"onMultiDeviceEventContact"; static NSString *_Nonnull const ExtSdkMethodKeyOnMultiDeviceEventGroup = @"onMultiDeviceEventGroup"; @@ -39,21 +37,9 @@ static NSString *_Nonnull const ExtSdkMethodKeyOnMultiDeviceEventThread = @"onMu static NSString *_Nonnull const ExtSdkMethodKeyOnMultiDeviceEventRemoveMessage = @"onMultiDeviceEventRemoveMessage"; static NSString *_Nonnull const ExtSdkMethodKeyOnMultiDeviceEventConversation = @"onMultiDeviceEventConversation"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserDidLoginFromOtherDevice = @"onUserDidLoginFromOtherDevice"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceWithInfo = - @"onUserDidLoginFromOtherDeviceWithInfo"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserDidRemoveFromServer = @"onUserDidRemoveFromServer"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserDidForbidByServer = @"onUserDidForbidByServer"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserDidChangePassword = @"onUserDidChangePassword"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserDidLoginTooManyDevice = @"onUserDidLoginTooManyDevice"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserKickedByOtherDevice = @"onUserKickedByOtherDevice"; -static NSString *_Nonnull const ExtSdkMethodKeyOnUserAuthenticationFailed = @"onUserAuthenticationFailed"; -static NSString *_Nonnull const ExtSdkMethodKeyOnAppActiveNumberReachLimit = @"onAppActiveNumberReachLimit"; - #pragma mark - EMContactManagerWrapper static NSString *_Nonnull const ExtSdkMethodKeyAddContact = @"addContact"; static NSString *_Nonnull const ExtSdkMethodKeyDeleteContact = @"deleteContact"; -static NSString *_Nonnull const ExtSdkMethodKeyGetAllContactsFromServer = @"getAllContactsFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyGetAllContactsFromDB = @"getAllContactsFromDB"; static NSString *_Nonnull const ExtSdkMethodKeyAddUserToBlockList = @"addUserToBlockList"; static NSString *_Nonnull const ExtSdkMethodKeyRemoveUserFromBlockList = @"removeUserFromBlockList"; @@ -69,12 +55,8 @@ static NSString *_Nonnull const ExtSdkMethodKeyOnContactChanged = @"onContactCha #pragma mark - EMChatManagerWrapper static NSString *_Nonnull const ExtSdkMethodKeySendMessage = @"sendMessage"; static NSString *_Nonnull const ExtSdkMethodKeyResendMessage = @"resendMessage"; -static NSString *_Nonnull const ExtSdkMethodKeyAckMessageRead = @"ackMessageRead"; -static NSString *_Nonnull const ExtSdkMethodKeyAckGroupMessageRead = @"ackGroupMessageRead"; -static NSString *_Nonnull const ExtSdkMethodKeyAckConversationRead = @"ackConversationRead"; static NSString *_Nonnull const ExtSdkMethodKeyRecallMessage = @"recallMessage"; static NSString *_Nonnull const ExtSdkMethodKeyGetConversation = @"getConversation"; -static NSString *_Nonnull const ExtSdkMethodKeyMarkAllChatMsgAsRead = @"markAllChatMsgAsRead"; static NSString *_Nonnull const ExtSdkMethodKeyGetUnreadMessageCount = @"getUnreadMessageCount"; static NSString *_Nonnull const ExtSdkMethodKeyUpdateChatMessage = @"updateChatMessage"; static NSString *_Nonnull const ExtSdkMethodKeyDownloadAttachment = @"downloadAttachment"; @@ -83,17 +65,14 @@ static NSString *_Nonnull const ExtSdkMethodKeyDownloadAttachmentInCombine = @"d static NSString *_Nonnull const ExtSdkMethodKeyDownloadThumbnailInCombine = @"downloadThumbnailInCombine"; static NSString *_Nonnull const ExtSdkMethodKeyImportMessages = @"importMessages"; static NSString *_Nonnull const ExtSdkMethodKeyLoadAllConversations = @"loadAllConversations"; -static NSString *_Nonnull const ExtSdkMethodKeyGetConversationsFromServer = @"getConversationsFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyDeleteConversation = @"deleteConversation"; +static NSString *_Nonnull const ExtSdkMethodKeyDeleteConversations = @"deleteConversations"; // static NSString * _Nonnull const ExtSdkMethodKeySetVoiceMessageListened = @"setVoiceMessageListened"; // deprecated 2022.05.04 // static NSString * _Nonnull const ExtSdkMethodKeyUpdateParticipant = @"updateParticipant"; // deprecated 2022.05.04 -static NSString *_Nonnull const ExtSdkMethodKeyUpdateConversationsName = @"updateConversationsName"; // deprecated 2022.05.04 -static NSString *_Nonnull const ExtSdkMethodKeyFetchHistoryMessages = @"fetchHistoryMessages"; static NSString *_Nonnull const ExtSdkMethodKeyfetchHistoryMessagesByOptions = @"fetchHistoryMessagesByOptions"; static NSString *_Nonnull const ExtSdkMethodKeySearchChatMsgFromDB = @"searchChatMsgFromDB"; static NSString *_Nonnull const ExtSdkMethodKeyGetMessage = @"getMessage"; -static NSString *_Nonnull const ExtSdkMethodKeyAsyncFetchGroupAcks = @"asyncFetchGroupAcks"; static NSString *_Nonnull const ExtSdkMethodKeydeleteRemoteConversation = @"deleteRemoteConversation"; static NSString *_Nonnull const ExtSdkMethodKeyDeleteMessagesBeforeTimestamp = @"deleteMessagesBeforeTimestamp"; @@ -104,21 +83,12 @@ static NSString *_Nonnull const ExtSdkMethodKeyChatAddReaction = @"addReaction"; static NSString *_Nonnull const ExtSdkMethodKeyChatRemoveReaction = @"removeReaction"; static NSString *_Nonnull const ExtSdkMethodKeyChatFetchReactionList = @"fetchReactionList"; static NSString *_Nonnull const ExtSdkMethodKeyChatFetchReactionDetail = @"fetchReactionDetail"; -static NSString *_Nonnull const ExtSdkMethodKeyChatReportMessage = @"reportMessage"; -static NSString *_Nonnull const ExtSdkMethodKeyFetchConversationsFromServerWithPage = - @"fetchConversationsFromServerWithPage"; static NSString *_Nonnull const ExtSdkMethodKeyRemoveMessagesFromServerWithMsgIds = @"removeMessagesFromServerWithMsgIds"; static NSString *_Nonnull const ExtSdkMethodKeyRemoveMessagesFromServerWithTs = @"removeMessagesFromServerWithTs"; -static NSString *_Nonnull const ExtSdkMethodKeyGetConversationsFromServerWithCursor = - @"getConversationsFromServerWithCursor"; -static NSString *_Nonnull const ExtSdkMethodKeyGetPinnedConversationsFromServerWithCursor = - @"getPinnedConversationsFromServerWithCursor"; -static NSString *_Nonnull const ExtSdkMethodKeyGetConversationsFromDBWithCursor = @"getConversationsFromDBWithCursor"; static NSString *_Nonnull const ExtSdkMethodKeyPinConversation = @"pinConversation"; -static NSString *_Nonnull const ExtSdkMethodKeyModifyMessage = @"modifyMessage"; static NSString *_Nonnull const ExtSdkMethodKeyDownloadAndParseCombineMessage = @"downloadAndParseCombineMessage"; static NSString *_Nonnull const ExtSdkMethodKeyVoiceMessageToText = @"voiceMessageToText"; @@ -129,16 +99,12 @@ static NSString *_Nonnull const ExtSdkMethodKeyDownloadBigImage = @"downloadBigI static NSString *_Nonnull const ExtSdkMethodKeyOnMessagesReceived = @"onMessagesReceived"; static NSString *_Nonnull const ExtSdkMethodKeyOnStreamMessagesReceived = @"onStreamMessagesReceived"; static NSString *_Nonnull const ExtSdkMethodKeyOnCmdMessagesReceived = @"onCmdMessagesReceived"; -static NSString *_Nonnull const ExtSdkMethodKeyOnMessagesRead = @"onMessagesRead"; -static NSString *_Nonnull const ExtSdkMethodKeyOnGroupMessageRead = @"onGroupMessageRead"; static NSString *_Nonnull const ExtSdkMethodKeyOnMessagesDelivered = @"onMessagesDelivered"; -static NSString *_Nonnull const ExtSdkMethodKeyOnMessagesRecalled = @"onMessagesRecalled"; static NSString *_Nonnull const ExtSdkMethodKeyOnMessagesRecalledInfo = @"onMessagesRecalledInfo"; +static NSString *_Nonnull const ExtSdkMethodKeyOnMessageReadReceipts = @"onMessageReadReceipts"; static NSString *_Nonnull const ExtSdkMethodKeyOnConversationUpdate = @"onConversationUpdate"; -static NSString *_Nonnull const ExtSdkMethodKeyOnConversationHasRead = @"onConversationHasRead"; -static NSString *_Nonnull const ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdated = @"onReadAckForGroupMessageUpdated"; static NSString *_Nonnull const ExtSdkMethodKeyChatOnMessageReactionDidChange = @"messageReactionDidChange"; static NSString *_Nonnull const ExtSdkMethodKeyOnMessageContentChanged = @"onMessageContentChanged"; @@ -146,29 +112,20 @@ static NSString *_Nonnull const ExtSdkMethodKeyOnMessageContentChanged = @"onMes static NSString *_Nonnull const ExtSdkMethodKeyOnMessageProgressUpdate = @"onMessageProgressUpdate"; static NSString *_Nonnull const ExtSdkMethodKeyOnMessageSuccess = @"onMessageSuccess"; static NSString *_Nonnull const ExtSdkMethodKeyOnMessageError = @"onMessageError"; -static NSString *_Nonnull const ExtSdkMethodKeyOnMessageReadAck = @"onMessageReadAck"; // deprecated 2026-08-20 use onMessagesRead instead -static NSString *_Nonnull const ExtSdkMethodKeyOnMessageDeliveryAck = @"onMessageDeliveryAck"; // deprecated 2026-08-20 use onMessagesDelivered instead -static NSString *_Nonnull const ExtSdkMethodKeyOnMessageStatusChanged = - @"onMessageStatusChanged"; // deprecated 2022.05.04 #pragma mark - EMConversationWrapper static NSString *_Nonnull const ExtSdkMethodKeyGetUnreadMsgCount = @"getUnreadMsgCount"; static NSString *_Nonnull const ExtSdkMethodKeyGetMsgCount = @"getMsgCount"; -static NSString *_Nonnull const ExtSdkMethodKeyMarkAllMsgsAsRead = @"markAllMessagesAsRead"; -static NSString *_Nonnull const ExtSdkMethodKeyMarkMsgAsRead = @"markMessageAsRead"; static NSString *_Nonnull const ExtSdkMethodKeySyncConversationExt = @"syncConversationExt"; -static NSString *_Nonnull const ExtSdkMethodKeySyncConversationName = @"syncConversationName"; // deprecated 2022.05.04 static NSString *_Nonnull const ExtSdkMethodKeyRemoveMsg = @"removeMessage"; static NSString *_Nonnull const ExtSdkMethodKeyGetLatestMsg = @"getLatestMessage"; static NSString *_Nonnull const ExtSdkMethodKeyGetLatestMsgFromOthers = @"getLatestMessageFromOthers"; static NSString *_Nonnull const ExtSdkMethodKeyClearAllMsg = @"clearAllMessages"; static NSString *_Nonnull const ExtSdkMethodKeydeleteMessagesWithTs = @"deleteMessagesWithTs"; static NSString *_Nonnull const ExtSdkMethodKeyInsertMsg = @"insertMessage"; -static NSString *_Nonnull const ExtSdkMethodKeyAppendMsg = @"appendMessage"; static NSString *_Nonnull const ExtSdkMethodKeyUpdateConversationMsg = @"updateConversationMessage"; -static NSString *_Nonnull const ExtSdkMethodKeyLoadMsgWithId = @"loadMsgWithId"; static NSString *_Nonnull const ExtSdkMethodKeyLoadMsgWithStartId = @"loadMsgWithStartId"; static NSString *_Nonnull const ExtSdkMethodKeyLoadMsgWithKeywords = @"loadMsgWithKeywords"; static NSString *_Nonnull const ExtSdkMethodKeyLoadMsgWithMsgType = @"loadMsgWithMsgType"; @@ -176,7 +133,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyLoadMsgWithTime = @"loadMsgWithTi #pragma mark - EMChatMessageWrapper static NSString *_Nonnull const ExtSdkMethodKeyChatGetReactionList = @"getReactionList"; -static NSString *_Nonnull const ExtSdkMethodKeyChatGroupAckCount = @"groupAckCount"; #pragma mark - EMChatroomManagerWrapper @@ -185,9 +141,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyLeaveChatRoom = @"leaveChatRoom"; static NSString *_Nonnull const ExtSdkMethodKeyGetChatroomsFromServer = @"fetchPublicChatRoomsFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyFetchChatRoomFromServer = @"fetchChatRoomInfoFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyGetChatRoom = @"getChatRoom"; -static NSString *_Nonnull const ExtSdkMethodKeyGetAllChatRooms = @"getAllChatRooms"; -static NSString *_Nonnull const ExtSdkMethodKeyCreateChatRoom = @"createChatRoom"; -static NSString *_Nonnull const ExtSdkMethodKeyDestroyChatRoom = @"destroyChatRoom"; static NSString *_Nonnull const ExtSdkMethodKeyChatRoomUpdateSubject = @"changeChatRoomSubject"; static NSString *_Nonnull const ExtSdkMethodKeyChatRoomUpdateDescription = @"changeChatRoomDescription"; static NSString *_Nonnull const ExtSdkMethodKeyGetChatroomMemberListFromServer = @"fetchChatRoomMembers"; @@ -217,7 +170,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyMuteAllChatRoomMembers = @"muteAl static NSString *_Nonnull const ExtSdkMethodKeyUnMuteAllChatRoomMembers = @"unMuteAllChatRoomMembers"; static NSString *_Nonnull const ExtSdkMethodKeyfetchChatRoomAttributes = @"fetchChatRoomAttributes"; -static NSString *_Nonnull const ExtSdkMethodKeyfetchChatRoomAllAttributes = @"fetchChatRoomAllAttributes"; static NSString *_Nonnull const ExtSdkMethodKeysetChatRoomAttributes = @"setChatRoomAttributes"; static NSString *_Nonnull const ExtSdkMethodKeyremoveChatRoomAttributes = @"removeChatRoomAttributes"; @@ -227,9 +179,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyChatroomChanged = @"onChatRoomCha static NSString *_Nonnull const ExtSdkMethodKeyGetGroupWithId = @"getGroupWithId"; static NSString *_Nonnull const ExtSdkMethodKeyGetJoinedGroups = @"getJoinedGroups"; -static NSString *_Nonnull const ExtSdkMethodKeyGetGroupsWithoutPushNotification = @"getGroupsWithoutPushNotification"; -static NSString *_Nonnull const ExtSdkMethodKeyGetJoinedGroupsFromServer = @"getJoinedGroupsFromServer"; -static NSString *_Nonnull const ExtSdkMethodKeyGetPublicGroupsFromServer = @"getPublicGroupsFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyCreateGroup = @"createGroup"; static NSString *_Nonnull const ExtSdkMethodKeyGetGroupSpecificationFromServer = @"getGroupSpecificationFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyGetGroupMemberListFromServer = @"getGroupMemberListFromServer"; @@ -273,7 +222,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyDeclineInvitationFromGroup = @"de static NSString *_Nonnull const ExtSdkMethodKeysetMemberAttributesFromGroup = @"setMemberAttributesFromGroup"; static NSString *_Nonnull const ExtSdkMethodKeyfetchMemberAttributesFromGroup = @"fetchMemberAttributesFromGroup"; static NSString *_Nonnull const ExtSdkMethodKeyfetchMembersAttributesFromGroup = @"fetchMembersAttributesFromGroup"; -static NSString *_Nonnull const ExtSdkMethodKeyIgnoreGroupPush = @"ignoreGroupPush"; // deprecated 2022.05.25 static NSString *_Nonnull const ExtSdkMethodKeyUpdateGroupNamecard = @"updateGroupNamecard"; static NSString *_Nonnull const ExtSdkMethodKeyGetGroupNamecard = @"getGroupNamecard"; @@ -281,27 +229,11 @@ static NSString *_Nonnull const ExtSdkMethodKeyGetGroupNamecard = @"getGroupName static NSString *_Nonnull const ExtSdkMethodKeyOnGroupChanged = @"onGroupChanged"; #pragma mark - EMPushManagerWrapper -static NSString *_Nonnull const ExtSdkMethodKeyGetImPushConfig = @"getImPushConfig"; static NSString *_Nonnull const ExtSdkMethodKeyGetImPushConfigFromServer = @"getImPushConfigFromServer"; static NSString *_Nonnull const ExtSdkMethodKeyUpdatePushNickname = @"updatePushNickname"; -static NSString *_Nonnull const ExtSdkMethodKeyImPushNoDisturb = @"imPushNoDisturb"; // deprecated 2022.05.04 static NSString *_Nonnull const ExtSdkMethodKeyUpdateImPushStyle = @"updateImPushStyle"; -static NSString *_Nonnull const ExtSdkMethodKeyUpdateGroupPushService = @"updateGroupPushService"; -static NSString *_Nonnull const ExtSdkMethodKeyGetNoDisturbGroups = @"getNoDisturbGroups"; // deprecated 2022.05.04 -static NSString *_Nonnull const ExtSdkMethodKeyBindDeviceToken = @"updateAPNsPushToken"; // deprecated 2026-05-21 -static NSString *_Nonnull const ExtSdkMethodKeyEnablePush = @"enableOfflinePush"; -static NSString *_Nonnull const ExtSdkMethodKeyDisablePush = @"disableOfflinePush"; -static NSString *_Nonnull const ExtSdkMethodKeyGetNoPushGroups = @"getNoPushGroups"; -static NSString *_Nonnull const ExtSdkMethodKeySetNoDisturbUsers = @"setNoDisturbUsers"; // deprecated 2022.05.04 -static NSString *_Nonnull const ExtSdkMethodKeyGetNoDisturbUsersFromServer = - @"getNoDisturbUsersFromServer"; // deprecated 2022.05.04 -static NSString *_Nonnull const ExtSdkMethodKeyUpdateUserPushService = @"updateUserPushService"; -static NSString *_Nonnull const ExtSdkMethodKeyGetNoPushUsers = @"getNoPushUsers"; - -static NSString *_Nonnull const ExtSdkMethodKeyUpdatePushConfig = @"updatePushConfig"; - -static NSString *_Nonnull const ExtSdkMethodKeyReportPushAction = @"reportPushAction"; + static NSString *_Nonnull const ExtSdkMethodKeySetConversationSilentMode = @"setConversationSilentMode"; static NSString *_Nonnull const ExtSdkMethodKeyRemoveConversationSilentMode = @"removeConversationSilentMode"; static NSString *_Nonnull const ExtSdkMethodKeyFetchConversationSilentMode = @"fetchConversationSilentMode"; @@ -315,9 +247,7 @@ static NSString *_Nonnull const ExtSdkMethodKeyGetPushTemplate = @"getPushTempla #pragma mark - EMUserInfoManagerWrapper static NSString *_Nonnull const ExtSdkMethodKeyUpdateOwnUserInfo = @"updateOwnUserInfo"; -static NSString *_Nonnull const ExtSdkMethodKeyUpdateOwnUserInfoWithType = @"updateOwnUserInfoWithType"; static NSString *_Nonnull const ExtSdkMethodKeyFetchUserInfoById = @"fetchUserInfoById"; -static NSString *_Nonnull const ExtSdkMethodKeyFetchUserInfoByIdWithType = @"fetchUserInfoByIdWithType"; static NSString *_Nonnull const ExtSdkMethodKeyGetLocalUserInfoByIds = @"getLocalUserInfoByIds"; static NSString *_Nonnull const ExtSdkMethodKeySubscribeUsersInfo = @"subscribeUsersInfo"; static NSString *_Nonnull const ExtSdkMethodKeyUnsubscribeUsersInfo = @"unsubscribeUsersInfo"; @@ -337,7 +267,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyFetchPresenceStatus = @"fetchPres static NSString *_Nonnull const ExtSdkMethodKeyOnPresenceStatusChanged = @"onPresenceStatusChanged"; #pragma mark - EMChatThreadManager methods -static NSString *_Nonnull const ExtSdkMethodKeyChatFetchChatThread = @"fetchChatThread"; // deprecated 2022.05.04 static NSString *_Nonnull const ExtSdkMethodKeyChatFetchChatThreadDetail = @"fetchChatThreadDetail"; static NSString *_Nonnull const ExtSdkMethodKeyChatFetchJoinedChatThreads = @"fetchJoinedChatThreads"; static NSString *_Nonnull const ExtSdkMethodKeyChatFetchChatThreadsWithParentId = @"fetchChatThreadsWithParentId"; @@ -363,8 +292,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyChatOnChatThreadUserRemoved = @"o static NSString *_Nonnull const ExtSdkMethodKeygetAllContacts = @"getAllContacts"; static NSString *_Nonnull const ExtSdkMethodKeysetContactRemark = @"setContactRemark"; static NSString *_Nonnull const ExtSdkMethodKeygetContact = @"getContact"; -static NSString *_Nonnull const ExtSdkMethodKeyfetchAllContacts = @"fetchAllContacts"; -static NSString *_Nonnull const ExtSdkMethodKeyfetchContacts = @"fetchContacts"; static NSString *_Nonnull const ExtSdkMethodKeyfetchJoinedGroupCount = @"fetchJoinedGroupCount"; // TODO: EMChatThreadManagerListener @@ -377,7 +304,6 @@ static NSString *_Nonnull const ExtSdkMethodKeyaddRemoteAndLocalConversationsMar @"addRemoteAndLocalConversationsMark"; static NSString *_Nonnull const ExtSdkMethodKeydeleteRemoteAndLocalConversationsMark = @"deleteRemoteAndLocalConversationsMark"; -static NSString *_Nonnull const ExtSdkMethodKeyfetchConversationsByOptions = @"fetchConversationsByOptions"; static NSString *_Nonnull const ExtSdkMethodKeydeleteAllMessageAndConversation = @"deleteAllMessageAndConversation"; static NSString *_Nonnull const ExtSdkMethodKeypinMessage = @"pinMessage"; static NSString *_Nonnull const ExtSdkMethodKeyunpinMessage = @"unpinMessage"; @@ -407,6 +333,15 @@ static NSString *_Nonnull const ExtSdkMethodKeygetUserIdsWithRTCUids = @"getUser // 2026-08-25 4.24.1 static NSString *_Nonnull const ExtSdkMethodKeySearchMessagesFromServer = @"searchMessagesFromServer"; +// 2026-09-15 5.0.0 +static NSString *_Nonnull const ExtSdkMethodKeysendMessageReadReceipts = @"sendMessageReadReceipts"; +static NSString *_Nonnull const ExtSdkMethodKeyclearConversationUnreadMessageCount = + @"clearConversationUnreadMessageCount"; +static NSString *_Nonnull const ExtSdkMethodKeyclearAllConversationUnreadMessageCount = + @"clearAllConversationUnreadMessageCount"; +static NSString *_Nonnull const ExtSdkMethodKeygetGroupMessageReadReceipts = @"getGroupMessageReadReceipts"; +static NSString *_Nonnull const ExtSdkMethodKeyfetchGroupMessageReadReceipts = @"fetchGroupMessageReadReceipts"; +static NSString *_Nonnull const ExtSdkMethodKeyupdateGroupConfigs = @"updateGroupConfigs"; // ############################################################################ // value start @@ -414,23 +349,21 @@ static NSString *_Nonnull const ExtSdkMethodKeySearchMessagesFromServer = @"sear #pragma mark - EMClientWrapper value static const int ExtSdkMethodKeyInitValue = 100; -static const int ExtSdkMethodKeyCreateAccountValue = 101; static const int ExtSdkMethodKeyLoginValue = 102; static const int ExtSdkMethodKeyLogoutValue = 103; static const int ExtSdkMethodKeyChangeAppKeyValue = 104; static const int ExtSdkMethodKeyChangeAppIdValue = 117; -static const int ExtSdkMethodKeyIsLoggedInBeforeValue = 105; -static const int ExtSdkMethodKeyUploadLogValue = 106; static const int ExtSdkMethodKeyCompressLogsValue = 107; static const int ExtSdkMethodKeyKickDeviceValue = 108; static const int ExtSdkMethodKeyKickAllDevicesValue = 109; -static const int ExtSdkMethodKeyCurrentUserValue = 110; // deprecated 2022.04.06 static const int ExtSdkMethodKeyGetLoggedInDevicesFromServerValue = 111; static const int ExtSdkMethodKeyGetTokenValue = 112; -static const int ExtSdkMethodKeyLoginWithAgoraTokenValue = 113; static const int ExtSdkMethodKeyGetCurrentUserValue = 114; static const int ExtSdkMethodKeyIsConnectedValue = 115; static const int ExtSdkMethodKeyRenewTokenValue = 116; +static const int ExtSdkMethodKeyBindDeviceTokenValue = 118; +static const int ExtSdkMethodKeyBindPushKitTokenValue = 119; +static const int ExtSdkMethodKeyUnbindPushKitTokenValue = 120; #pragma mark - EMClientDelegate value static const int ExtSdkMethodKeyOnConnectedValue = 200; @@ -439,15 +372,6 @@ static const int ExtSdkMethodKeyOnMultiDeviceEventValue = 202; static const int ExtSdkMethodKeySendDataToFlutterValue = 203; static const int ExtSdkMethodKeyOnTokenWillExpireValue = 204; static const int ExtSdkMethodKeyOnTokenDidExpireValue = 205; -static const int ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceValue = 206; -static const int ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceWithInfoValue = 219; -static const int ExtSdkMethodKeyOnUserDidRemoveFromServerValue = 207; -static const int ExtSdkMethodKeyOnUserDidForbidByServerValue = 208; -static const int ExtSdkMethodKeyOnUserDidChangePasswordValue = 209; -static const int ExtSdkMethodKeyOnUserDidLoginTooManyDeviceValue = 210; -static const int ExtSdkMethodKeyOnUserKickedByOtherDeviceValue = 211; -static const int ExtSdkMethodKeyOnUserAuthenticationFailedValue = 212; -static const int ExtSdkMethodKeyOnAppActiveNumberReachLimitValue = 213; static const int ExtSdkMethodKeyOnMultiDeviceEventContactValue = 214; static const int ExtSdkMethodKeyOnMultiDeviceEventGroupValue = 215; static const int ExtSdkMethodKeyOnMultiDeviceEventThreadValue = 216; @@ -455,11 +379,13 @@ static const int ExtSdkMethodKeyOnMultiDeviceEventRemoveMessageValue = 217; static const int ExtSdkMethodKeyOnMultiDeviceEventConversationValue = 218; static const int ExtSdkMethodKeyOnOfflineMessageSyncStartValue = 221; static const int ExtSdkMethodKeyOnOfflineMessageSyncFinishValue = 220; +static const int ExtSdkMethodKeyOnDataSyncStartValue = 2032; +static const int ExtSdkMethodKeyOnDataSyncFinishValue = 2033; +static const int ExtSdkMethodKeyOnDatabaseOpenedValue = 2034; #pragma mark - EMContactManagerWrapper value static const int ExtSdkMethodKeyAddContactValue = 300; static const int ExtSdkMethodKeyDeleteContactValue = 301; -static const int ExtSdkMethodKeyGetAllContactsFromServerValue = 302; static const int ExtSdkMethodKeyGetAllContactsFromDBValue = 303; static const int ExtSdkMethodKeyAddUserToBlockListValue = 304; static const int ExtSdkMethodKeyRemoveUserFromBlockListValue = 305; @@ -475,12 +401,8 @@ static const int ExtSdkMethodKeyOnContactChangedValue = 400; #pragma mark - EMChatManagerWrapper value static const int ExtSdkMethodKeySendMessageValue = 500; static const int ExtSdkMethodKeyResendMessageValue = 501; -static const int ExtSdkMethodKeyAckMessageReadValue = 502; -static const int ExtSdkMethodKeyAckGroupMessageReadValue = 503; -static const int ExtSdkMethodKeyAckConversationReadValue = 504; static const int ExtSdkMethodKeyRecallMessageValue = 505; static const int ExtSdkMethodKeyGetConversationValue = 506; -static const int ExtSdkMethodKeyMarkAllChatMsgAsReadValue = 507; static const int ExtSdkMethodKeyGetUnreadMessageCountValue = 508; static const int ExtSdkMethodKeyUpdateChatMessageValue = 509; static const int ExtSdkMethodKeyDownloadAttachmentValue = 510; @@ -489,16 +411,13 @@ static const int ExtSdkMethodKeyDownloadAttachmentInCombineValue = 541; static const int ExtSdkMethodKeyDownloadThumbnailInCombineValue = 542; static const int ExtSdkMethodKeyImportMessagesValue = 512; static const int ExtSdkMethodKeyLoadAllConversationsValue = 513; -static const int ExtSdkMethodKeyGetConversationsFromServerValue = 514; static const int ExtSdkMethodKeyDeleteConversationValue = 515; +static const int ExtSdkMethodKeyDeleteConversationsValue = 546; // static const int ExtSdkMethodKeySetVoiceMessageListenedValue = 516; // deprecated 2022.05.04 // static const int ExtSdkMethodKeyUpdateParticipantValue = 517; // deprecated 2022.05.04 -static const int ExtSdkMethodKeyUpdateConversationsNameValue = 518; // deprecated 2022.05.04 -static const int ExtSdkMethodKeyFetchHistoryMessagesValue = 519; static const int ExtSdkMethodKeySearchChatMsgFromDBValue = 520; static const int ExtSdkMethodKeyGetMessageValue = 521; -static const int ExtSdkMethodKeyAsyncFetchGroupAcksValue = 522; static const int ExtSdkMethodKeydeleteRemoteConversationValue = 523; static const int ExtSdkMethodKeyDeleteMessagesBeforeTimestampValue = 531; @@ -509,17 +428,11 @@ static const int ExtSdkMethodKeyChatAddReactionValue = 526; static const int ExtSdkMethodKeyChatRemoveReactionValue = 527; static const int ExtSdkMethodKeyChatFetchReactionListValue = 528; static const int ExtSdkMethodKeyChatFetchReactionDetailValue = 529; -static const int ExtSdkMethodKeyChatReportMessageValue = 530; -static const int ExtSdkMethodKeyFetchConversationsFromServerWithPageValue = 532; static const int ExtSdkMethodKeyRemoveMessagesFromServerWithMsgIdsValue = 533; static const int ExtSdkMethodKeyRemoveMessagesFromServerWithTsValue = 534; static const int ExtSdkMethodKeyfetchHistoryMessagesByOptionsValue = 535; -static const int ExtSdkMethodKeyGetConversationsFromServerWithCursorValue = 536; -static const int ExtSdkMethodKeyGetPinnedConversationsFromServerWithCursorValue = 537; -static const int ExtSdkMethodKeyGetConversationsFromDBWithCursorValue = 546; static const int ExtSdkMethodKeyPinConversationValue = 538; -static const int ExtSdkMethodKeyModifyMessageValue = 539; static const int ExtSdkMethodKeyDownloadAndParseCombineMessageValue = 540; static const int ExtSdkMethodKeyVoiceMessageToTextValue = 543; static const int ExtSdkMethodKeyVoiceFileToTextValue = 544; @@ -528,16 +441,12 @@ static const int ExtSdkMethodKeyDownloadBigImageValue = 545; #pragma mark - EMChatManagerDelegate value static const int ExtSdkMethodKeyOnMessagesReceivedValue = 600; static const int ExtSdkMethodKeyOnCmdMessagesReceivedValue = 601; -static const int ExtSdkMethodKeyOnMessagesReadValue = 602; -static const int ExtSdkMethodKeyOnGroupMessageReadValue = 603; static const int ExtSdkMethodKeyOnMessagesDeliveredValue = 604; -static const int ExtSdkMethodKeyOnMessagesRecalledValue = 605; static const int ExtSdkMethodKeyOnMessagesRecalledInfoValue = 611; +static const int ExtSdkMethodKeyOnMessageReadReceiptsValue = 2035; static const int ExtSdkMethodKeyOnConversationUpdateValue = 606; -static const int ExtSdkMethodKeyOnConversationHasReadValue = 607; -static const int ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdatedValue = 608; static const int ExtSdkMethodKeyChatOnMessageReactionDidChangeValue = 609; static const int ExtSdkMethodKeyOnMessageContentChangedValue = 610; @@ -545,27 +454,19 @@ static const int ExtSdkMethodKeyOnMessageContentChangedValue = 610; static const int ExtSdkMethodKeyOnMessageProgressUpdateValue = 1200; static const int ExtSdkMethodKeyOnMessageSuccessValue = 1201; static const int ExtSdkMethodKeyOnMessageErrorValue = 1202; -static const int ExtSdkMethodKeyOnMessageReadAckValue = 1203; -static const int ExtSdkMethodKeyOnMessageDeliveryAckValue = 1204; -static const int ExtSdkMethodKeyOnMessageStatusChangedValue = 1205; #pragma mark - EMConversationWrapper value static const int ExtSdkMethodKeyGetUnreadMsgCountValue = 700; -static const int ExtSdkMethodKeyMarkAllMsgsAsReadValue = 701; -static const int ExtSdkMethodKeyMarkMsgAsReadValue = 702; static const int ExtSdkMethodKeySyncConversationExtValue = 703; -static const int ExtSdkMethodKeySyncConversationNameValue = 704; static const int ExtSdkMethodKeyRemoveMsgValue = 705; static const int ExtSdkMethodKeyGetLatestMsgValue = 706; static const int ExtSdkMethodKeyGetLatestMsgFromOthersValue = 707; static const int ExtSdkMethodKeyClearAllMsgValue = 708; static const int ExtSdkMethodKeyInsertMsgValue = 709; -static const int ExtSdkMethodKeyAppendMsgValue = 710; static const int ExtSdkMethodKeyUpdateConversationMsgValue = 711; static const int ExtSdkMethodKeyGetMsgCountValue = 720; -static const int ExtSdkMethodKeyLoadMsgWithIdValue = 712; static const int ExtSdkMethodKeyLoadMsgWithStartIdValue = 713; static const int ExtSdkMethodKeyLoadMsgWithKeywordsValue = 714; static const int ExtSdkMethodKeyLoadMsgWithMsgTypeValue = 715; @@ -573,7 +474,6 @@ static const int ExtSdkMethodKeyLoadMsgWithTimeValue = 716; #pragma mark - EMChatMessageWrapper value static const int ExtSdkMethodKeyChatGetReactionListValue = 717; -static const int ExtSdkMethodKeyChatGroupAckCountValue = 718; static const int ExtSdkMethodKeydeleteMessagesWithTsValue = 719; #pragma mark - EMChatroomManagerWrapper value @@ -583,9 +483,6 @@ static const int ExtSdkMethodKeyLeaveChatRoomValue = 801; static const int ExtSdkMethodKeyGetChatroomsFromServerValue = 802; static const int ExtSdkMethodKeyFetchChatRoomFromServerValue = 803; static const int ExtSdkMethodKeyGetChatRoomValue = 804; -static const int ExtSdkMethodKeyGetAllChatRoomsValue = 805; -static const int ExtSdkMethodKeyCreateChatRoomValue = 806; -static const int ExtSdkMethodKeyDestroyChatRoomValue = 807; static const int ExtSdkMethodKeyChatRoomUpdateSubjectValue = 808; static const int ExtSdkMethodKeyChatRoomUpdateDescriptionValue = 809; static const int ExtSdkMethodKeyGetChatroomMemberListFromServerValue = 810; @@ -611,7 +508,6 @@ static const int ExtSdkMethodKeyMuteAllChatRoomMembersValue = 827; static const int ExtSdkMethodKeyUnMuteAllChatRoomMembersValue = 828; static const int ExtSdkMethodKeyfetchChatRoomAttributesValue = 830; -static const int ExtSdkMethodKeyfetchChatRoomAllAttributesValue = 831; static const int ExtSdkMethodKeysetChatRoomAttributesValue = 832; static const int ExtSdkMethodKeyremoveChatRoomAttributesValue = 833; @@ -621,9 +517,6 @@ static const int ExtSdkMethodKeyChatroomChangedValue = 829; static const int ExtSdkMethodKeyGetGroupWithIdValue = 900; static const int ExtSdkMethodKeyGetJoinedGroupsValue = 901; -static const int ExtSdkMethodKeyGetGroupsWithoutPushNotificationValue = 902; -static const int ExtSdkMethodKeyGetJoinedGroupsFromServerValue = 903; -static const int ExtSdkMethodKeyGetPublicGroupsFromServerValue = 904; static const int ExtSdkMethodKeyCreateGroupValue = 905; static const int ExtSdkMethodKeyGetGroupSpecificationFromServerValue = 906; static const int ExtSdkMethodKeyGetGroupMemberListFromServerValue = 907; @@ -664,7 +557,6 @@ static const int ExtSdkMethodKeyAcceptJoinApplicationValue = 941; static const int ExtSdkMethodKeyDeclineJoinApplicationValue = 942; static const int ExtSdkMethodKeyAcceptInvitationFromGroupValue = 943; static const int ExtSdkMethodKeyDeclineInvitationFromGroupValue = 944; -static const int ExtSdkMethodKeyIgnoreGroupPushValue = 945; // deprecated 2022.05.25 static const int ExtSdkMethodKeyOnGroupChangedValue = 946; static const int ExtSdkMethodKeysetMemberAttributesFromGroupValue = 947; @@ -674,25 +566,11 @@ static const int ExtSdkMethodKeyUpdateGroupNamecardValue = 950; static const int ExtSdkMethodKeyGetGroupNamecardValue = 951; #pragma mark - EMPushManagerWrapper value -static const int ExtSdkMethodKeyGetImPushConfigValue = 1000; static const int ExtSdkMethodKeyGetImPushConfigFromServerValue = 1001; static const int ExtSdkMethodKeyUpdatePushNicknameValue = 1002; -static const int ExtSdkMethodKeyImPushNoDisturbValue = 1003; static const int ExtSdkMethodKeyUpdateImPushStyleValue = 1004; -static const int ExtSdkMethodKeyUpdateGroupPushServiceValue = 1005; -static const int ExtSdkMethodKeyGetNoDisturbGroupsValue = 1006; -static const int ExtSdkMethodKeyBindDeviceTokenValue = 1007; // deprecated 2026-05-21 -static const int ExtSdkMethodKeyEnablePushValue = 1008; -static const int ExtSdkMethodKeyDisablePushValue = 1009; -static const int ExtSdkMethodKeyGetNoPushGroupsValue = 1010; -static const int ExtSdkMethodKeySetNoDisturbUsersValue = 1011; -static const int ExtSdkMethodKeyGetNoDisturbUsersFromServerValue = 1012; -static const int ExtSdkMethodKeyUpdateUserPushServiceValue = 1013; -static const int ExtSdkMethodKeyGetNoPushUsersValue = 1014; -static const int ExtSdkMethodKeyUpdatePushConfigValue = 1015; - -static const int ExtSdkMethodKeyReportPushActionValue = 1016; + static const int ExtSdkMethodKeySetConversationSilentModeValue = 1017; static const int ExtSdkMethodKeyRemoveConversationSilentModeValue = 1018; static const int ExtSdkMethodKeyFetchConversationSilentModeValue = 1019; @@ -704,15 +582,9 @@ static const int ExtSdkMethodKeyFetchPreferredNotificationLanguageValue = 1024; static const int ExtSdkMethodKeySetPushTemplateValue = 1025; static const int ExtSdkMethodKeyGetPushTemplateValue = 1026; -// 2026-09-24 1.21.0 -static const int ExtSdkMethodKeyBindPushKitTokenValue = 1027; -static const int ExtSdkMethodKeyUnbindPushKitTokenValue = 1028; - #pragma mark - EMUserInfoManagerWrapper value static const int ExtSdkMethodKeyUpdateOwnUserInfoValue = 1100; -static const int ExtSdkMethodKeyUpdateOwnUserInfoWithTypeValue = 1101; static const int ExtSdkMethodKeyFetchUserInfoByIdValue = 1102; -static const int ExtSdkMethodKeyFetchUserInfoByIdWithTypeValue = 1103; static const int ExtSdkMethodKeyGetLocalUserInfoByIdsValue = 1104; static const int ExtSdkMethodKeySubscribeUsersInfoValue = 1105; static const int ExtSdkMethodKeyUnsubscribeUsersInfoValue = 1106; @@ -732,7 +604,6 @@ static const int ExtSdkMethodKeyFetchPresenceStatusValue = 1404; static const int ExtSdkMethodKeyOnPresenceStatusChangedValue = 1405; #pragma mark - EMChatThreadManager value -static const int ExtSdkMethodKeyChatFetchChatThreadValue = 1500; // deprecated 2022.05.04 static const int ExtSdkMethodKeyChatFetchChatThreadDetailValue = 1501; static const int ExtSdkMethodKeyChatFetchJoinedChatThreadsValue = 1502; static const int ExtSdkMethodKeyChatFetchChatThreadsWithParentIdValue = 1503; @@ -761,8 +632,6 @@ static const int ExtSdkMethodKeyChatOnChatThreadUserRemovedValue = 1517; static const int ExtSdkMethodKeygetAllContactsValue = 2000; static const int ExtSdkMethodKeysetContactRemarkValue = 2001; static const int ExtSdkMethodKeygetContactValue = 2002; -static const int ExtSdkMethodKeyfetchAllContactsValue = 2003; -static const int ExtSdkMethodKeyfetchContactsValue = 2004; static const int ExtSdkMethodKeyfetchJoinedGroupCountValue = 2005; // 2024-04-16 4.5.0 @@ -771,7 +640,6 @@ static const int ExtSdkMethodKeypinnedMessagesValue = 2007; static const int ExtSdkMethodKeyonMessagePinChangedValue = 2008; static const int ExtSdkMethodKeyaddRemoteAndLocalConversationsMarkValue = 2009; static const int ExtSdkMethodKeydeleteRemoteAndLocalConversationsMarkValue = 2010; -static const int ExtSdkMethodKeyfetchConversationsByOptionsValue = 2011; static const int ExtSdkMethodKeydeleteAllMessageAndConversationValue = 2012; static const int ExtSdkMethodKeypinMessageValue = 2013; static const int ExtSdkMethodKeyunpinMessageValue = 2014; @@ -803,6 +671,14 @@ static const int ExtSdkMethodKeygetUserIdsWithRTCUidsValue = 2028; // 2026-08-25 4.24.1 static const int ExtSdkMethodKeySearchMessagesFromServerValue = 2029; +// 2026-09-15 5.0.0 +static const int ExtSdkMethodKeysendMessageReadReceiptsValue = 2030; +static const int ExtSdkMethodKeyclearConversationUnreadMessageCountValue = 2031; +static const int ExtSdkMethodKeyclearAllConversationUnreadMessageCountValue = 2036; +static const int ExtSdkMethodKeygetGroupMessageReadReceiptsValue = 2037; +static const int ExtSdkMethodKeyfetchGroupMessageReadReceiptsValue = 2038; +static const int ExtSdkMethodKeyupdateGroupConfigsValue = 2039; + // 2026-04-10 static const int ExtSdkMethodKeyOnStreamMessagesReceivedValue = 611; diff --git a/modules/objc/common/ExtSdkMethodTypeObjc.m b/modules/objc/common/ExtSdkMethodTypeObjc.m index a21a0550..f0b9911f 100644 --- a/modules/objc/common/ExtSdkMethodTypeObjc.m +++ b/modules/objc/common/ExtSdkMethodTypeObjc.m @@ -17,24 +17,19 @@ + (int)getEnumValue:(nonnull NSString *)key { /// EMClientWrapper ExtSdkMethodKeyInit : @(ExtSdkMethodKeyInitValue), - ExtSdkMethodKeyCreateAccount : @(ExtSdkMethodKeyCreateAccountValue), ExtSdkMethodKeyLogin : @(ExtSdkMethodKeyLoginValue), ExtSdkMethodKeyLogout : @(ExtSdkMethodKeyLogoutValue), ExtSdkMethodKeyChangeAppKey : @(ExtSdkMethodKeyChangeAppKeyValue), ExtSdkMethodKeyChangeAppId : @(ExtSdkMethodKeyChangeAppIdValue), - ExtSdkMethodKeyIsLoggedInBefore : @(ExtSdkMethodKeyIsLoggedInBeforeValue), - ExtSdkMethodKeyUploadLog : @(ExtSdkMethodKeyUploadLogValue), ExtSdkMethodKeyCompressLogs : @(ExtSdkMethodKeyCompressLogsValue), ExtSdkMethodKeyKickDevice : @(ExtSdkMethodKeyKickDeviceValue), ExtSdkMethodKeyKickAllDevices : @(ExtSdkMethodKeyKickAllDevicesValue), - ExtSdkMethodKeyCurrentUser : @(ExtSdkMethodKeyCurrentUserValue), // deprecated ExtSdkMethodKeyGetLoggedInDevicesFromServer : @(ExtSdkMethodKeyGetLoggedInDevicesFromServerValue), ExtSdkMethodKeyGetToken : @(ExtSdkMethodKeyGetTokenValue), - ExtSdkMethodKeyLoginWithAgoraToken : @(ExtSdkMethodKeyLoginWithAgoraTokenValue), ExtSdkMethodKeyGetCurrentUser : @(ExtSdkMethodKeyGetCurrentUserValue), ExtSdkMethodKeyIsConnected : @(ExtSdkMethodKeyIsConnectedValue), ExtSdkMethodKeyRenewToken : @(ExtSdkMethodKeyRenewTokenValue), - // 2026-09-24 1.21.0 + ExtSdkMethodKeyBindDeviceToken : @(ExtSdkMethodKeyBindDeviceTokenValue), ExtSdkMethodKeyBindPushKitToken : @(ExtSdkMethodKeyBindPushKitTokenValue), ExtSdkMethodKeyUnbindPushKitToken : @(ExtSdkMethodKeyUnbindPushKitTokenValue), @@ -45,20 +40,10 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeySendDataToFlutter : @(ExtSdkMethodKeySendDataToFlutterValue), ExtSdkMethodKeyOnTokenWillExpire : @(ExtSdkMethodKeyOnTokenWillExpireValue), ExtSdkMethodKeyOnTokenDidExpire : @(ExtSdkMethodKeyOnTokenDidExpireValue), - ExtSdkMethodKeyOnUserDidLoginFromOtherDevice : @(ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceValue), - ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceWithInfo : - @(ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceWithInfoValue), - ExtSdkMethodKeyOnUserDidRemoveFromServer : @(ExtSdkMethodKeyOnUserDidRemoveFromServerValue), - ExtSdkMethodKeyOnUserDidForbidByServer : @(ExtSdkMethodKeyOnUserDidForbidByServerValue), - ExtSdkMethodKeyOnUserDidChangePassword : @(ExtSdkMethodKeyOnUserDidChangePasswordValue), - ExtSdkMethodKeyOnUserDidLoginTooManyDevice : @(ExtSdkMethodKeyOnUserDidLoginTooManyDeviceValue), - ExtSdkMethodKeyOnUserKickedByOtherDevice : @(ExtSdkMethodKeyOnUserKickedByOtherDeviceValue), - ExtSdkMethodKeyOnUserAuthenticationFailed : @(ExtSdkMethodKeyOnUserAuthenticationFailedValue), /// EMContactManagerWrapper ExtSdkMethodKeyAddContact : @(ExtSdkMethodKeyAddContactValue), ExtSdkMethodKeyDeleteContact : @(ExtSdkMethodKeyDeleteContactValue), - ExtSdkMethodKeyGetAllContactsFromServer : @(ExtSdkMethodKeyGetAllContactsFromServerValue), ExtSdkMethodKeyGetAllContactsFromDB : @(ExtSdkMethodKeyGetAllContactsFromDBValue), ExtSdkMethodKeyAddUserToBlockList : @(ExtSdkMethodKeyAddUserToBlockListValue), ExtSdkMethodKeyRemoveUserFromBlockList : @(ExtSdkMethodKeyRemoveUserFromBlockListValue), @@ -74,12 +59,8 @@ + (int)getEnumValue:(nonnull NSString *)key { /// EMChatManagerWrapper ExtSdkMethodKeySendMessage : @(ExtSdkMethodKeySendMessageValue), ExtSdkMethodKeyResendMessage : @(ExtSdkMethodKeyResendMessageValue), - ExtSdkMethodKeyAckMessageRead : @(ExtSdkMethodKeyAckMessageReadValue), - ExtSdkMethodKeyAckGroupMessageRead : @(ExtSdkMethodKeyAckGroupMessageReadValue), - ExtSdkMethodKeyAckConversationRead : @(ExtSdkMethodKeyAckConversationReadValue), ExtSdkMethodKeyRecallMessage : @(ExtSdkMethodKeyRecallMessageValue), ExtSdkMethodKeyGetConversation : @(ExtSdkMethodKeyGetConversationValue), - ExtSdkMethodKeyMarkAllChatMsgAsRead : @(ExtSdkMethodKeyMarkAllChatMsgAsReadValue), ExtSdkMethodKeyGetUnreadMessageCount : @(ExtSdkMethodKeyGetUnreadMessageCountValue), ExtSdkMethodKeyUpdateChatMessage : @(ExtSdkMethodKeyUpdateChatMessageValue), ExtSdkMethodKeyDownloadAttachment : @(ExtSdkMethodKeyDownloadAttachmentValue), @@ -88,16 +69,13 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyDownloadThumbnailInCombine : @(ExtSdkMethodKeyDownloadThumbnailInCombineValue), ExtSdkMethodKeyImportMessages : @(ExtSdkMethodKeyImportMessagesValue), ExtSdkMethodKeyLoadAllConversations : @(ExtSdkMethodKeyLoadAllConversationsValue), - ExtSdkMethodKeyGetConversationsFromServer : @(ExtSdkMethodKeyGetConversationsFromServerValue), ExtSdkMethodKeyDeleteConversation : @(ExtSdkMethodKeyDeleteConversationValue), + ExtSdkMethodKeyDeleteConversations : @(ExtSdkMethodKeyDeleteConversationsValue), // ExtSdkMethodKeySetVoiceMessageListened: @(ExtSdkMethodKeySetVoiceMessageListenedValue), // deprecated // ExtSdkMethodKeyUpdateParticipant: @(ExtSdkMethodKeyUpdateParticipantValue), // deprecated - ExtSdkMethodKeyUpdateConversationsName : @(ExtSdkMethodKeyUpdateConversationsNameValue), // deprecated - ExtSdkMethodKeyFetchHistoryMessages : @(ExtSdkMethodKeyFetchHistoryMessagesValue), ExtSdkMethodKeySearchChatMsgFromDB : @(ExtSdkMethodKeySearchChatMsgFromDBValue), ExtSdkMethodKeyGetMessage : @(ExtSdkMethodKeyGetMessageValue), - ExtSdkMethodKeyAsyncFetchGroupAcks : @(ExtSdkMethodKeyAsyncFetchGroupAcksValue), ExtSdkMethodKeydeleteRemoteConversation : @(ExtSdkMethodKeydeleteRemoteConversationValue), ExtSdkMethodKeyDeleteMessagesBeforeTimestamp : @(ExtSdkMethodKeyDeleteMessagesBeforeTimestampValue), @@ -108,59 +86,41 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyChatRemoveReaction : @(ExtSdkMethodKeyChatRemoveReactionValue), ExtSdkMethodKeyChatFetchReactionList : @(ExtSdkMethodKeyChatFetchReactionListValue), ExtSdkMethodKeyChatFetchReactionDetail : @(ExtSdkMethodKeyChatFetchReactionDetailValue), - ExtSdkMethodKeyChatReportMessage : @(ExtSdkMethodKeyChatReportMessageValue), - ExtSdkMethodKeyFetchConversationsFromServerWithPage : - @(ExtSdkMethodKeyFetchConversationsFromServerWithPageValue), ExtSdkMethodKeyRemoveMessagesFromServerWithMsgIds : @(ExtSdkMethodKeyRemoveMessagesFromServerWithMsgIdsValue), ExtSdkMethodKeyRemoveMessagesFromServerWithTs : @(ExtSdkMethodKeyRemoveMessagesFromServerWithTsValue), /// EMChatManagerDelegate ExtSdkMethodKeyOnMessagesReceived : @(ExtSdkMethodKeyOnMessagesReceivedValue), ExtSdkMethodKeyOnCmdMessagesReceived : @(ExtSdkMethodKeyOnCmdMessagesReceivedValue), - ExtSdkMethodKeyOnMessagesRead : @(ExtSdkMethodKeyOnMessagesReadValue), - ExtSdkMethodKeyOnGroupMessageRead : @(ExtSdkMethodKeyOnGroupMessageReadValue), ExtSdkMethodKeyOnMessagesDelivered : @(ExtSdkMethodKeyOnMessagesDeliveredValue), - ExtSdkMethodKeyOnMessagesRecalled : @(ExtSdkMethodKeyOnMessagesRecalledValue), ExtSdkMethodKeyOnConversationUpdate : @(ExtSdkMethodKeyOnConversationUpdateValue), - ExtSdkMethodKeyOnConversationHasRead : @(ExtSdkMethodKeyOnConversationHasReadValue), - ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdated : - @(ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdatedValue), ExtSdkMethodKeyChatOnMessageReactionDidChange : @(ExtSdkMethodKeyChatOnMessageReactionDidChangeValue), /// EMMessageListener ExtSdkMethodKeyOnMessageProgressUpdate : @(ExtSdkMethodKeyOnMessageProgressUpdateValue), ExtSdkMethodKeyOnMessageSuccess : @(ExtSdkMethodKeyOnMessageSuccessValue), ExtSdkMethodKeyOnMessageError : @(ExtSdkMethodKeyOnMessageErrorValue), - ExtSdkMethodKeyOnMessageReadAck : @(ExtSdkMethodKeyOnMessageReadAckValue), - ExtSdkMethodKeyOnMessageDeliveryAck : @(ExtSdkMethodKeyOnMessageDeliveryAckValue), - ExtSdkMethodKeyOnMessageStatusChanged : @(ExtSdkMethodKeyOnMessageStatusChangedValue), /// EMConversationWrapper ExtSdkMethodKeyGetUnreadMsgCount : @(ExtSdkMethodKeyGetUnreadMsgCountValue), - ExtSdkMethodKeyMarkAllMsgsAsRead : @(ExtSdkMethodKeyMarkAllMsgsAsReadValue), - ExtSdkMethodKeyMarkMsgAsRead : @(ExtSdkMethodKeyMarkMsgAsReadValue), ExtSdkMethodKeySyncConversationExt : @(ExtSdkMethodKeySyncConversationExtValue), - ExtSdkMethodKeySyncConversationName : @(ExtSdkMethodKeySyncConversationNameValue), ExtSdkMethodKeyRemoveMsg : @(ExtSdkMethodKeyRemoveMsgValue), ExtSdkMethodKeyGetLatestMsg : @(ExtSdkMethodKeyGetLatestMsgValue), ExtSdkMethodKeyGetLatestMsgFromOthers : @(ExtSdkMethodKeyGetLatestMsgFromOthersValue), ExtSdkMethodKeyClearAllMsg : @(ExtSdkMethodKeyClearAllMsgValue), ExtSdkMethodKeyInsertMsg : @(ExtSdkMethodKeyInsertMsgValue), - ExtSdkMethodKeyAppendMsg : @(ExtSdkMethodKeyAppendMsgValue), ExtSdkMethodKeyUpdateConversationMsg : @(ExtSdkMethodKeyUpdateConversationMsgValue), - ExtSdkMethodKeyLoadMsgWithId : @(ExtSdkMethodKeyLoadMsgWithIdValue), ExtSdkMethodKeyLoadMsgWithStartId : @(ExtSdkMethodKeyLoadMsgWithStartIdValue), ExtSdkMethodKeyLoadMsgWithKeywords : @(ExtSdkMethodKeyLoadMsgWithKeywordsValue), ExtSdkMethodKeyLoadMsgWithMsgType : @(ExtSdkMethodKeyLoadMsgWithMsgTypeValue), ExtSdkMethodKeyLoadMsgWithTime : @(ExtSdkMethodKeyLoadMsgWithTimeValue), ExtSdkMethodKeyChatGetReactionList : @(ExtSdkMethodKeyChatGetReactionListValue), - ExtSdkMethodKeyChatGroupAckCount : @(ExtSdkMethodKeyChatGroupAckCountValue), /// EMChatroomManagerWrapper @@ -169,9 +129,6 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyGetChatroomsFromServer : @(ExtSdkMethodKeyGetChatroomsFromServerValue), ExtSdkMethodKeyFetchChatRoomFromServer : @(ExtSdkMethodKeyFetchChatRoomFromServerValue), ExtSdkMethodKeyGetChatRoom : @(ExtSdkMethodKeyGetChatRoomValue), - ExtSdkMethodKeyGetAllChatRooms : @(ExtSdkMethodKeyGetAllChatRoomsValue), - ExtSdkMethodKeyCreateChatRoom : @(ExtSdkMethodKeyCreateChatRoomValue), - ExtSdkMethodKeyDestroyChatRoom : @(ExtSdkMethodKeyDestroyChatRoomValue), ExtSdkMethodKeyChatRoomUpdateSubject : @(ExtSdkMethodKeyChatRoomUpdateSubjectValue), ExtSdkMethodKeyChatRoomUpdateDescription : @(ExtSdkMethodKeyChatRoomUpdateDescriptionValue), ExtSdkMethodKeyGetChatroomMemberListFromServer : @(ExtSdkMethodKeyGetChatroomMemberListFromServerValue), @@ -198,7 +155,6 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyUnMuteAllChatRoomMembers : @(ExtSdkMethodKeyUnMuteAllChatRoomMembersValue), ExtSdkMethodKeyfetchChatRoomAttributes : @(ExtSdkMethodKeyfetchChatRoomAttributesValue), - ExtSdkMethodKeyfetchChatRoomAllAttributes : @(ExtSdkMethodKeyfetchChatRoomAllAttributesValue), ExtSdkMethodKeysetChatRoomAttributes : @(ExtSdkMethodKeysetChatRoomAttributesValue), ExtSdkMethodKeyremoveChatRoomAttributes : @(ExtSdkMethodKeyremoveChatRoomAttributesValue), @@ -208,9 +164,6 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyGetGroupWithId : @(ExtSdkMethodKeyGetGroupWithIdValue), ExtSdkMethodKeyGetJoinedGroups : @(ExtSdkMethodKeyGetJoinedGroupsValue), - ExtSdkMethodKeyGetGroupsWithoutPushNotification : @(ExtSdkMethodKeyGetGroupsWithoutPushNotificationValue), - ExtSdkMethodKeyGetJoinedGroupsFromServer : @(ExtSdkMethodKeyGetJoinedGroupsFromServerValue), - ExtSdkMethodKeyGetPublicGroupsFromServer : @(ExtSdkMethodKeyGetPublicGroupsFromServerValue), ExtSdkMethodKeyCreateGroup : @(ExtSdkMethodKeyCreateGroupValue), ExtSdkMethodKeyGetGroupSpecificationFromServer : @(ExtSdkMethodKeyGetGroupSpecificationFromServerValue), ExtSdkMethodKeyGetGroupMemberListFromServer : @(ExtSdkMethodKeyGetGroupMemberListFromServerValue), @@ -251,32 +204,17 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyDeclineJoinApplication : @(ExtSdkMethodKeyDeclineJoinApplicationValue), ExtSdkMethodKeyAcceptInvitationFromGroup : @(ExtSdkMethodKeyAcceptInvitationFromGroupValue), ExtSdkMethodKeyDeclineInvitationFromGroup : @(ExtSdkMethodKeyDeclineInvitationFromGroupValue), - ExtSdkMethodKeyIgnoreGroupPush : @(ExtSdkMethodKeyIgnoreGroupPushValue), // deprecated ExtSdkMethodKeyUpdateGroupNamecard : @(ExtSdkMethodKeyUpdateGroupNamecardValue), ExtSdkMethodKeyGetGroupNamecard : @(ExtSdkMethodKeyGetGroupNamecardValue), ExtSdkMethodKeyOnGroupChanged : @(ExtSdkMethodKeyOnGroupChangedValue), /// EMPushManagerWrapper - ExtSdkMethodKeyGetImPushConfig : @(ExtSdkMethodKeyGetImPushConfigValue), ExtSdkMethodKeyGetImPushConfigFromServer : @(ExtSdkMethodKeyGetImPushConfigFromServerValue), ExtSdkMethodKeyUpdatePushNickname : @(ExtSdkMethodKeyUpdatePushNicknameValue), - ExtSdkMethodKeyImPushNoDisturb : @(ExtSdkMethodKeyImPushNoDisturbValue), ExtSdkMethodKeyUpdateImPushStyle : @(ExtSdkMethodKeyUpdateImPushStyleValue), - ExtSdkMethodKeyUpdateGroupPushService : @(ExtSdkMethodKeyUpdateGroupPushServiceValue), - ExtSdkMethodKeyGetNoDisturbGroups : @(ExtSdkMethodKeyGetNoDisturbGroupsValue), - ExtSdkMethodKeyBindDeviceToken : @(ExtSdkMethodKeyBindDeviceTokenValue), // deprecated - ExtSdkMethodKeyEnablePush : @(ExtSdkMethodKeyEnablePushValue), - ExtSdkMethodKeyDisablePush : @(ExtSdkMethodKeyDisablePushValue), - ExtSdkMethodKeyGetNoPushGroups : @(ExtSdkMethodKeyGetNoPushGroupsValue), - ExtSdkMethodKeySetNoDisturbUsers : @(ExtSdkMethodKeySetNoDisturbUsersValue), - ExtSdkMethodKeyGetNoDisturbUsersFromServer : @(ExtSdkMethodKeyGetNoDisturbUsersFromServerValue), - ExtSdkMethodKeyUpdateUserPushService : @(ExtSdkMethodKeyUpdateUserPushServiceValue), - ExtSdkMethodKeyGetNoPushUsers : @(ExtSdkMethodKeyGetNoPushUsersValue), - ExtSdkMethodKeyUpdatePushConfig : @(ExtSdkMethodKeyUpdatePushConfigValue), - - ExtSdkMethodKeyReportPushAction : @(ExtSdkMethodKeyReportPushActionValue), + ExtSdkMethodKeySetConversationSilentMode : @(ExtSdkMethodKeySetConversationSilentModeValue), ExtSdkMethodKeyRemoveConversationSilentMode : @(ExtSdkMethodKeyRemoveConversationSilentModeValue), ExtSdkMethodKeyFetchConversationSilentMode : @(ExtSdkMethodKeyFetchConversationSilentModeValue), @@ -288,9 +226,7 @@ + (int)getEnumValue:(nonnull NSString *)key { /// EMUserInfoManagerWrapper ExtSdkMethodKeyUpdateOwnUserInfo : @(ExtSdkMethodKeyUpdateOwnUserInfoValue), - ExtSdkMethodKeyUpdateOwnUserInfoWithType : @(ExtSdkMethodKeyUpdateOwnUserInfoWithTypeValue), ExtSdkMethodKeyFetchUserInfoById : @(ExtSdkMethodKeyFetchUserInfoByIdValue), - ExtSdkMethodKeyFetchUserInfoByIdWithType : @(ExtSdkMethodKeyFetchUserInfoByIdWithTypeValue), ExtSdkMethodKeyGetLocalUserInfoByIds : @(ExtSdkMethodKeyGetLocalUserInfoByIdsValue), ExtSdkMethodKeySubscribeUsersInfo : @(ExtSdkMethodKeySubscribeUsersInfoValue), ExtSdkMethodKeyUnsubscribeUsersInfo : @(ExtSdkMethodKeyUnsubscribeUsersInfoValue), @@ -307,7 +243,6 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyOnPresenceStatusChanged : @(ExtSdkMethodKeyOnPresenceStatusChangedValue), - ExtSdkMethodKeyChatFetchChatThread : @(ExtSdkMethodKeyChatFetchChatThreadValue), // deprecated ExtSdkMethodKeyChatFetchChatThreadDetail : @(ExtSdkMethodKeyChatFetchChatThreadDetailValue), ExtSdkMethodKeyChatFetchJoinedChatThreads : @(ExtSdkMethodKeyChatFetchJoinedChatThreadsValue), ExtSdkMethodKeyChatFetchChatThreadsWithParentId : @(ExtSdkMethodKeyChatFetchChatThreadsWithParentIdValue), @@ -336,15 +271,8 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeysetMemberAttributesFromGroup : @(ExtSdkMethodKeysetMemberAttributesFromGroupValue), ExtSdkMethodKeyfetchMemberAttributesFromGroup : @(ExtSdkMethodKeyfetchMemberAttributesFromGroupValue), ExtSdkMethodKeyfetchMembersAttributesFromGroup : @(ExtSdkMethodKeyfetchMembersAttributesFromGroupValue), - ExtSdkMethodKeyOnAppActiveNumberReachLimit : @(ExtSdkMethodKeyOnAppActiveNumberReachLimitValue), - ExtSdkMethodKeyGetConversationsFromServerWithCursor : - @(ExtSdkMethodKeyGetConversationsFromServerWithCursorValue), - ExtSdkMethodKeyGetPinnedConversationsFromServerWithCursor : - @(ExtSdkMethodKeyGetPinnedConversationsFromServerWithCursorValue), - ExtSdkMethodKeyGetConversationsFromDBWithCursor : @(ExtSdkMethodKeyGetConversationsFromDBWithCursorValue), ExtSdkMethodKeyPinConversation : @(ExtSdkMethodKeyPinConversationValue), - ExtSdkMethodKeyModifyMessage : @(ExtSdkMethodKeyModifyMessageValue), ExtSdkMethodKeyDownloadAndParseCombineMessage : @(ExtSdkMethodKeyDownloadAndParseCombineMessageValue), ExtSdkMethodKeyVoiceMessageToText : @(ExtSdkMethodKeyVoiceMessageToTextValue), ExtSdkMethodKeyVoiceFileToText : @(ExtSdkMethodKeyVoiceFileToTextValue), @@ -366,8 +294,6 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeygetAllContacts : @(ExtSdkMethodKeygetAllContactsValue), ExtSdkMethodKeysetContactRemark : @(ExtSdkMethodKeysetContactRemarkValue), ExtSdkMethodKeygetContact : @(ExtSdkMethodKeygetContactValue), - ExtSdkMethodKeyfetchAllContacts : @(ExtSdkMethodKeyfetchAllContactsValue), - ExtSdkMethodKeyfetchContacts : @(ExtSdkMethodKeyfetchContactsValue), ExtSdkMethodKeyfetchJoinedGroupCount : @(ExtSdkMethodKeyfetchJoinedGroupCountValue), ExtSdkMethodKeygetPinInfo : @(ExtSdkMethodKeygetPinInfoValue), @@ -376,7 +302,6 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeyaddRemoteAndLocalConversationsMark : @(ExtSdkMethodKeyaddRemoteAndLocalConversationsMarkValue), ExtSdkMethodKeydeleteRemoteAndLocalConversationsMark : @(ExtSdkMethodKeydeleteRemoteAndLocalConversationsMarkValue), - ExtSdkMethodKeyfetchConversationsByOptions : @(ExtSdkMethodKeyfetchConversationsByOptionsValue), ExtSdkMethodKeydeleteAllMessageAndConversation : @(ExtSdkMethodKeydeleteAllMessageAndConversationValue), ExtSdkMethodKeyfetchPinnedMessages : @(ExtSdkMethodKeyfetchPinnedMessagesValue), ExtSdkMethodKeypinMessage : @(ExtSdkMethodKeypinMessageValue), @@ -405,6 +330,20 @@ + (int)getEnumValue:(nonnull NSString *)key { ExtSdkMethodKeySearchMessagesFromServer : @(ExtSdkMethodKeySearchMessagesFromServerValue), ExtSdkMethodKeyOnStreamMessagesReceived : @(ExtSdkMethodKeyOnStreamMessagesReceivedValue), + + /// 2026-09-15 5.0.0 + ExtSdkMethodKeysendMessageReadReceipts : @(ExtSdkMethodKeysendMessageReadReceiptsValue), + ExtSdkMethodKeyclearConversationUnreadMessageCount : + @(ExtSdkMethodKeyclearConversationUnreadMessageCountValue), + ExtSdkMethodKeyclearAllConversationUnreadMessageCount : + @(ExtSdkMethodKeyclearAllConversationUnreadMessageCountValue), + ExtSdkMethodKeygetGroupMessageReadReceipts : @(ExtSdkMethodKeygetGroupMessageReadReceiptsValue), + ExtSdkMethodKeyfetchGroupMessageReadReceipts : @(ExtSdkMethodKeyfetchGroupMessageReadReceiptsValue), + ExtSdkMethodKeyupdateGroupConfigs : @(ExtSdkMethodKeyupdateGroupConfigsValue), + ExtSdkMethodKeyOnDataSyncStart : @(ExtSdkMethodKeyOnDataSyncStartValue), + ExtSdkMethodKeyOnDataSyncFinish : @(ExtSdkMethodKeyOnDataSyncFinishValue), + ExtSdkMethodKeyOnDatabaseOpened : @(ExtSdkMethodKeyOnDatabaseOpenedValue), + ExtSdkMethodKeyOnMessageReadReceipts : @(ExtSdkMethodKeyOnMessageReadReceiptsValue), }; }); if (nil != key) { diff --git a/modules/objc/dispatch/ExtSdkChatManagerWrapper.h b/modules/objc/dispatch/ExtSdkChatManagerWrapper.h index d6fc1b7b..67a56c49 100644 --- a/modules/objc/dispatch/ExtSdkChatManagerWrapper.h +++ b/modules/objc/dispatch/ExtSdkChatManagerWrapper.h @@ -23,17 +23,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)ackMessageRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)ackGroupMessageRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; -- (void)ackConversationRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)recallMessage:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -46,10 +35,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)markAllMessagesAsRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getUnreadMessageCount:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -91,21 +76,13 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)getConversationsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)deleteConversation:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)fetchHistoryMessages:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)fetchGroupReadAck:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; +- (void)deleteConversations:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; - (void)searchChatMsgFromDB:(NSDictionary *)param withMethodType:(NSString *)aChannelName @@ -139,18 +116,10 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)reportMessage:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)deleteMessagesBeforeTimestamp:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)fetchConversationsFromServerWithPage:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)removeMessagesFromServerWithMsgIds:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -163,26 +132,10 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)getConversationsFromServerWithCursor:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)getPinnedConversationsFromServerWithCursor:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)getConversationsFromDBWithCursor:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)pinConversation:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)modifyMessage:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)downloadAndParseCombineMessage:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -195,10 +148,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)fetchConversationsByOptions:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)deleteAllMessageAndConversation:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -243,6 +192,26 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; +- (void)sendMessageReadReceipts:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + +- (void)clearConversationUnreadMessageCount:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + +- (void)clearAllConversationUnreadMessageCount:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + +- (void)getGroupMessageReadReceipts:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + +- (void)fetchGroupMessageReadReceipts:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + @end NS_ASSUME_NONNULL_END diff --git a/modules/objc/dispatch/ExtSdkChatManagerWrapper.m b/modules/objc/dispatch/ExtSdkChatManagerWrapper.m index 551d3466..9dba6bd7 100644 --- a/modules/objc/dispatch/ExtSdkChatManagerWrapper.m +++ b/modules/objc/dispatch/ExtSdkChatManagerWrapper.m @@ -10,7 +10,7 @@ #import "ExtSdkToJson.h" -@interface ExtSdkChatManagerWrapper () +@interface ExtSdkChatManagerWrapper () @end @@ -28,6 +28,8 @@ + (nonnull instancetype)getInstance { - (void)initSdk { [EMClient.sharedClient.chatManager removeDelegate:self]; [EMClient.sharedClient.chatManager addDelegate:self delegateQueue:nil]; + [EMClient.sharedClient.chatManager removeConversationDelegate:self]; + [EMClient.sharedClient.chatManager addConversationDelegate:self delegateQueue:nil]; } #pragma mark - Actions @@ -85,7 +87,7 @@ - (void)resendMessage:(NSDictionary *)param return; } - [EMClient.sharedClient.chatManager resendMessage:msg + [EMClient.sharedClient.chatManager sendMessage:msg progress:^(int progress) { [weakSelf onReceive:aChannelName withParams:@{ @@ -116,54 +118,6 @@ - (void)resendMessage:(NSDictionary *)param [weakSelf onResult:result withMethodType:aChannelName withError:nil withParams:[msg toJsonObject]]; } -- (void)ackMessageRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *msgId = param[@"msg_id"]; - NSString *to = param[@"to"]; - [EMClient.sharedClient.chatManager sendMessageReadAck:msgId - toUser:to - completion:^(EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:@(!aError)]; - }]; -} - -- (void)ackGroupMessageRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *msgId = param[@"msg_id"]; - NSString *groupId = param[@"group_id"]; - NSString *content = param[@"content"]; - [EMClient.sharedClient.chatManager sendGroupMessageReadAck:msgId - toGroup:groupId - content:content - completion:^(EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:@(!aError)]; - }]; -} - -- (void)ackConversationRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *conversationId = param[@"convId"]; - [EMClient.sharedClient.chatManager ackConversationRead:conversationId - completion:^(EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:@(!aError)]; - }]; -} - - (void)recallMessage:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -204,15 +158,6 @@ - (void)getConversationApi:(NSDictionary *)param [weakSelf onResult:result withMethodType:aChannelName withError:nil withParams:[conv toJsonObject]]; } -- (void)markAllMessagesAsRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - EMError *error = [EMClient.sharedClient.chatManager markAllConversationsAsRead]; - - [weakSelf onResult:result withMethodType:aChannelName withError:error withParams:nil]; -} - - (void)getMessageCount:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -226,13 +171,9 @@ - (void)getUnreadMessageCount:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { __weak typeof(self) weakSelf = self; - NSArray *conList = [EMClient.sharedClient.chatManager getAllConversations]; - int unreadCount = 0; + NSInteger unreadCount = 0; EMError *error = nil; - for (EMConversation *con in conList) { - unreadCount += con.unreadMessagesCount; - } - + unreadCount = [EMClient.sharedClient.chatManager getUnreadMessageCount]; [weakSelf onResult:result withMethodType:aChannelName withError:error withParams:@(unreadCount)]; } @@ -582,29 +523,6 @@ - (void)loadAllConversations:(NSDictionary *)param [self onResult:result withMethodType:aChannelName withError:nil withParams:conList]; } -- (void)getConversationsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - // !!! It has been marked as invalid in the typescript language. - [EMClient.sharedClient.chatManager getConversationsFromServer:^(NSArray *aCoversations, EMError *aError) { - NSArray *sortedList = - [aCoversations sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { - if (((EMConversation *)obj1).latestMessage.timestamp > ((EMConversation *)obj2).latestMessage.timestamp) { - return NSOrderedAscending; - } else { - return NSOrderedDescending; - } - }]; - NSMutableArray *conList = [NSMutableArray array]; - for (EMConversation *conversation in sortedList) { - [conList addObject:[conversation toJsonObject]]; - } - - [weakSelf onResult:result withMethodType:aChannelName withError:nil withParams:conList]; - }]; -} - - (void)deleteConversation:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -621,52 +539,132 @@ - (void)deleteConversation:(NSDictionary *)param }]; } -- (void)fetchHistoryMessages:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { +- (void)deleteConversations:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { __weak typeof(self) weakSelf = self; - NSString *conversationId = param[@"convId"]; - EMConversationType type = (EMConversationType)[param[@"convType"] intValue]; - int pageSize = [param[@"pageSize"] intValue]; - NSString *startMsgId = param[@"startMsgId"]; - int direction = [param[@"direction"] intValue]; - // !!! It has been marked as invalid in the typescript language. + NSArray *convIds = param[@"convIds"]; + BOOL isDeleteMsgs = [param[@"deleteMessages"] boolValue]; + NSMutableArray *conversations = [NSMutableArray array]; + for (NSString *convId in convIds) { + EMConversation *conversation = + [EMClient.sharedClient.chatManager getConversationWithConvId:convId]; + if (conversation) { + [conversations addObject:conversation]; + } + } + if (convIds.count > 0 && conversations.count == 0) { + // Conversation IDs that do not exist are skipped, aligning with Android. + [weakSelf onResult:result + withMethodType:aChannelName + withError:nil + withParams:@(YES)]; + return; + } [EMClient.sharedClient.chatManager - asyncFetchHistoryMessagesFromServer:conversationId - conversationType:type - startMessageId:startMsgId - fetchDirection:(direction == 0 ? EMMessageFetchHistoryDirectionUp - : EMMessageFetchHistoryDirectionDown) - pageSize:pageSize - completion:^(EMCursorResult *_Nullable aResult, - EMError *_Nullable aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aResult toJsonObject]]; - }]; + deleteConversations:conversations + isDeleteMessages:isDeleteMsgs + completion:^(EMError *aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:@(!aError)]; + }]; } -- (void)fetchGroupReadAck:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { +- (void)fetchGroupMessageReadReceipts:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { NSString *msgId = param[@"msg_id"]; int pageSize = [param[@"pageSize"] intValue]; - NSString *ackId = param[@"ack_id"]; + NSString *receiptId = param[@"receipt_id"]; NSString *groupId = param[@"group_id"]; __weak typeof(self) weakSelf = self; [EMClient.sharedClient.chatManager - asyncFetchGroupMessageAcksFromServer:msgId - groupId:groupId - startGroupAckId:ackId - pageSize:pageSize - completion:^(EMCursorResult *aResult, EMError *aError, int totalCount) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aResult toJsonObject]]; - }]; + asyncFetchGroupMessageReadUsersFromServer:msgId + groupId:groupId + readReceiptId:receiptId + pageSize:pageSize + completion:^(EMCursorResult *aResult, EMError *aError, int totalCount) { + NSMutableDictionary *data = + [[aResult toJsonObject] mutableCopy] ?: [NSMutableDictionary dictionary]; + data[@"totalCount"] = @(totalCount); + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:data]; + }]; +} + +- (void)sendMessageReadReceipts:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + NSArray *msgIds = param[@"msg_ids"]; + NSMutableArray *messages = [NSMutableArray array]; + for (NSString *msgId in msgIds) { + EMChatMessage *msg = [EMClient.sharedClient.chatManager getMessageWithMessageId:msgId]; + if (msg) { + [messages addObject:msg]; + } + } + [EMClient.sharedClient.chatManager sendMessageReadReceipts:messages + completion:^(EMError *aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:nil]; + }]; +} + +- (void)clearConversationUnreadMessageCount:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + NSString *conversationId = param[@"convId"]; + [EMClient.sharedClient.chatManager clearConversationUnreadMessageCount:conversationId + completion:^(EMError *aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:nil]; + }]; +} + +- (void)clearAllConversationUnreadMessageCount:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + [EMClient.sharedClient.chatManager clearAllConversationUnreadMessageCount:^(EMError *aError) { + [weakSelf onResult:result withMethodType:aChannelName withError:aError withParams:nil]; + }]; +} + +- (void)getGroupMessageReadReceipts:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + NSArray *msgIds = param[@"msg_ids"]; + NSMutableArray *messages = [NSMutableArray array]; + for (NSString *msgId in msgIds) { + EMChatMessage *msg = [EMClient.sharedClient.chatManager getMessageWithMessageId:msgId]; + if (msg) { + [messages addObject:msg]; + } + } + [EMClient.sharedClient.chatManager + getGroupMessageReadReceipts:messages + completion:^(NSArray *_Nullable aReceipts, EMError *_Nullable aError) { + NSMutableArray *list = [NSMutableArray array]; + for (EMMessageReadReceipt *receipt in aReceipts) { + [list addObject:[receipt toJsonObject]]; + } + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:aError ? nil : list]; + }]; } - (void)searchChatMsgFromDB:(NSDictionary *)param @@ -847,24 +845,6 @@ - (void)fetchReactionDetail:(NSDictionary *)param }]; } -- (void)reportMessage:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - NSString *msgId = param[@"msgId"]; - NSString *tag = param[@"tag"]; - NSString *reason = param[@"reason"]; - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient.chatManager reportMessageWithId:msgId - tag:tag - reason:reason - completion:^(EMError *error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:nil]; - }]; -} - - (void)deleteMessagesBeforeTimestamp:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -879,39 +859,6 @@ - (void)deleteMessagesBeforeTimestamp:(NSDictionary *)param }]; } -- (void)fetchConversationsFromServerWithPage:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - int pageSize = [param[@"pageSize"] intValue]; - int pageNum = [param[@"pageNum"] intValue]; - __weak typeof(self) weakSelf = self; - // !!! It has been marked as invalid in the typescript language. - [EMClient.sharedClient.chatManager - getConversationsFromServerByPage:pageNum - pageSize:pageSize - completion:^(NSArray *_Nullable aConversations, - EMError *_Nullable aError) { - NSArray *sortedList = [aConversations - sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { - if (((EMConversation *)obj1).latestMessage.timestamp > - ((EMConversation *)obj2).latestMessage.timestamp) { - return NSOrderedAscending; - } else { - return NSOrderedDescending; - } - }]; - NSMutableArray *conList = [NSMutableArray array]; - for (EMConversation *conversation in sortedList) { - [conList addObject:[conversation toJsonObject]]; - } - - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:conList]; - }]; -} - - (void)removeMessagesFromServerWithMsgIds:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -970,60 +917,6 @@ - (void)fetchHistoryMessagesByOptions:(NSDictionary *)param }]; } -- (void)getConversationsFromServerWithCursor:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *cursor = param[@"cursor"]; - int pageSize = [param[@"pageSize"] intValue]; - [EMClient.sharedClient.chatManager - getConversationsFromServerWithCursor:cursor - pageSize:pageSize - completion:^(EMCursorResult *_Nullable ret, - EMError *_Nullable error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:[ret toJsonObject]]; - }]; -} - -- (void)getPinnedConversationsFromServerWithCursor:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *cursor = param[@"cursor"]; - int pageSize = [param[@"pageSize"] intValue]; - [EMClient.sharedClient.chatManager - getPinnedConversationsFromServerWithCursor:cursor - pageSize:pageSize - completion:^(EMCursorResult *_Nullable ret, - EMError *_Nullable error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:[ret toJsonObject]]; - }]; -} - -- (void)getConversationsFromDBWithCursor:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *cursor = param[@"cursor"]; - NSInteger pageSize = [param[@"pageSize"] integerValue]; - [EMClient.sharedClient.chatManager - getConversationsFromDBWithCursor:cursor - pageSize:pageSize - completion:^(EMCursorResult *_Nullable ret, - EMError *_Nullable error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:[ret toJsonObject]]; - }]; -} - - (void)pinConversation:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -1040,22 +933,6 @@ - (void)pinConversation:(NSDictionary *)param }]; } -- (void)modifyMessage:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *msgId = param[@"msgId"]; - EMMessageBody *body = [EMTextMessageBody fromJsonObject:param[@"body"]]; - [EMClient.sharedClient.chatManager modifyMessage:msgId - body:body - completion:^(EMError *_Nullable error, EMChatMessage *_Nullable message) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:error != nil ? nil : [message toJsonObject]]; - }]; -} - - (void)downloadAndParseCombineMessage:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -1107,55 +984,6 @@ - (void)deleteRemoteAndLocalConversationsMark:(NSDictionary *)param }]; } -- (void)fetchConversationsByOptions:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *cursor = [EMConversationFilter getCursor:param]; - BOOL isPinned = [EMConversationFilter getPinned:param]; - BOOL isMark = [EMConversationFilter hasMark:param]; - NSInteger pageSize = [EMConversationFilter pageSize:param]; - if (isPinned) { - [EMClient.sharedClient.chatManager - getPinnedConversationsFromServerWithCursor:cursor - pageSize:pageSize - completion:^(EMCursorResult *_Nullable ret, - EMError *_Nullable error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:[ret toJsonObject]]; - }]; - return; - } - - if (isMark) { - EMConversationFilter *filter = [EMConversationFilter fromJsonObject:param]; - [EMClient.sharedClient.chatManager - getConversationsFromServerWithCursor:cursor - filter:filter - completion:^(EMCursorResult *_Nullable ret, - EMError *_Nullable error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:[ret toJsonObject]]; - }]; - return; - } - - [EMClient.sharedClient.chatManager - getConversationsFromServerWithCursor:cursor - pageSize:pageSize - completion:^(EMCursorResult *_Nullable ret, - EMError *_Nullable error) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:[ret toJsonObject]]; - }]; -} - - (void)deleteAllMessageAndConversation:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -1376,15 +1204,13 @@ - (void)modifyMsgBody:(NSDictionary *)param }]; } -#pragma mark - EMChatManagerDelegate +#pragma mark - EMConversationDelegate - (void)conversationListDidUpdate:(NSArray *)aConversationList { [self onReceive:ExtSdkMethodKeyOnConversationUpdate withParams:nil]; } -- (void)onConversationRead:(NSString *)from to:(NSString *)to { - [self onReceive:ExtSdkMethodKeyOnConversationHasRead withParams:@{@"from" : from, @"to" : to}]; -} +#pragma mark - EMChatManagerDelegate - (void)messagesDidReceive:(NSArray *)aMessages { NSMutableArray *msgList = [NSMutableArray array]; @@ -1411,23 +1237,11 @@ - (void)cmdMessagesDidReceive:(NSArray *)aCmdMessages { [self onReceive:ExtSdkMethodKeyOnCmdMessagesReceived withParams:cmdMsgList]; } -- (void)messagesDidRead:(NSArray *)aMessages { - NSMutableArray *list = [NSMutableArray array]; - for (EMChatMessage *msg in aMessages) { - NSDictionary *json = [msg toJsonObject]; - [list addObject:json]; - [self onReceive:ExtSdkMethodKeyOnMessageReadAck withParams:json]; - } - - [self onReceive:ExtSdkMethodKeyOnMessagesRead withParams:list]; -} - - (void)messagesDidDeliver:(NSArray *)aMessages { NSMutableArray *list = [NSMutableArray array]; for (EMChatMessage *msg in aMessages) { NSDictionary *json = [msg toJsonObject]; [list addObject:json]; - [self onReceive:ExtSdkMethodKeyOnMessageDeliveryAck withParams:@{@"message" : json}]; } [self onReceive:ExtSdkMethodKeyOnMessagesDelivered withParams:list]; @@ -1452,18 +1266,13 @@ - (void)messageStatusDidChange:(EMChatMessage *)aMessage withError:(EMError *)aE - (void)messageAttachmentStatusDidChange:(EMChatMessage *)aMessage withError:(EMError *)aError { } -- (void)groupMessageDidRead:(NSArray *)aGroupAcks { +- (void)onMessageReadReceipts:(NSArray *_Nonnull)aReceipts { NSMutableArray *list = [NSMutableArray array]; - for (EMGroupMessageAck *ack in aGroupAcks) { - NSDictionary *json = [ack toJsonObject]; - [list addObject:json]; + for (EMMessageReadReceipt *receipt in aReceipts) { + [list addObject:[receipt toJsonObject]]; } - [self onReceive:ExtSdkMethodKeyOnGroupMessageRead withParams:list]; -} - -- (void)groupMessageAckHasChanged { - [self onReceive:ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdated withParams:nil]; + [self onReceive:ExtSdkMethodKeyOnMessageReadReceipts withParams:@{@"receipts" : list}]; } - (void)messageReactionDidChange:(NSArray *)changes { diff --git a/modules/objc/dispatch/ExtSdkChatMessageWrapper.h b/modules/objc/dispatch/ExtSdkChatMessageWrapper.h index 1e91511b..6e17b2cf 100644 --- a/modules/objc/dispatch/ExtSdkChatMessageWrapper.h +++ b/modules/objc/dispatch/ExtSdkChatMessageWrapper.h @@ -17,10 +17,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)getGroupAckCount:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getMessagePinInfo:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; diff --git a/modules/objc/dispatch/ExtSdkChatMessageWrapper.m b/modules/objc/dispatch/ExtSdkChatMessageWrapper.m index 044b80c9..c2a09aac 100644 --- a/modules/objc/dispatch/ExtSdkChatMessageWrapper.m +++ b/modules/objc/dispatch/ExtSdkChatMessageWrapper.m @@ -40,23 +40,6 @@ - (void)getReactionList:(NSDictionary *)param withParams:list]; } -- (void)getGroupAckCount:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - NSString *msgId = param[@"msgId"]; - EMChatMessage *msg = - [EMClient.sharedClient.chatManager getMessageWithMessageId:msgId]; - if ([self getMessageParams:result - withMethodType:aChannelName - withMessage:msg]) { - return; - } - [self onResult:result - withMethodType:aChannelName - withError:nil - withParams:@(msg.groupAckCount ?: 0)]; -} - - (void)getMessagePinInfo:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { diff --git a/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.h b/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.h index bf445357..cac051e9 100644 --- a/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.h +++ b/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.h @@ -18,10 +18,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)createChatRoom:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)joinChatRoom:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -30,10 +26,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)destroyChatRoom:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)fetchChatroomFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -42,10 +34,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)getAllChatRooms:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getChatroomMemberListFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -135,10 +123,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)fetchChatRoomAllAttributes:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)setChatRoomAttributes:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; diff --git a/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.m b/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.m index e6b1db76..02c22b75 100644 --- a/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.m +++ b/modules/objc/dispatch/ExtSdkChatroomManagerWrapper.m @@ -52,31 +52,6 @@ - (void)getChatroomsFromServer:(NSDictionary *)param }]; } -- (void)createChatRoom:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - - __weak typeof(self) weakSelf = self; - - NSString *subject = param[@"subject"]; - NSString *description = param[@"desc"]; - NSArray *invitees = param[@"members"]; - NSString *message = param[@"welcomeMsg"]; - NSInteger maxMembersCount = [param[@"maxUserCount"] integerValue]; - [EMClient.sharedClient.roomManager - createChatroomWithSubject:subject - description:description - invitees:invitees - message:message - maxMembersCount:maxMembersCount - completion:^(EMChatroom *aChatroom, EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aChatroom toJsonObject]]; - }]; -} - - (void)joinChatRoom:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -115,59 +90,22 @@ - (void)leaveChatRoom:(NSDictionary *)param }]; } -- (void)destroyChatRoom:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - - __weak typeof(self) weakSelf = self; - - NSString *chatroomId = param[@"roomId"]; - [EMClient.sharedClient.roomManager destroyChatroom:chatroomId - completion:^(EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:nil]; - }]; -} - - (void)fetchChatroomFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { __weak typeof(self) weakSelf = self; NSString *chatroomId = param[@"roomId"]; - BOOL hasFetchMembers = NO; - BOOL fetchMembers = NO; - if (param[@"fetchMembers"]) { - fetchMembers = [param[@"fetchMembers"] boolValue]; - hasFetchMembers = YES; - } - if (hasFetchMembers) { - [EMClient.sharedClient.roomManager - getChatroomSpecificationFromServerWithId:chatroomId - fetchMembers:fetchMembers - completion:^(EMChatroom *aChatroom, - EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams: - [aChatroom - toJsonObject]]; - }]; - } else { - [EMClient.sharedClient.roomManager - getChatroomSpecificationFromServerWithId:chatroomId - completion:^(EMChatroom *aChatroom, - EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams: - [aChatroom - toJsonObject]]; - }]; - } + [EMClient.sharedClient.roomManager + getChatroomSpecificationFromServerWithId:chatroomId + completion:^(EMChatroom *aChatroom, + EMError *aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams: + [aChatroom + toJsonObject]]; + }]; } - (void)getChatRoom:(NSDictionary *)param @@ -182,28 +120,6 @@ - (void)getChatRoom:(NSDictionary *)param withParams:[chatroom toJsonObject]]; } -- (void)getAllChatRooms:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient.roomManager - getChatroomsFromServerWithPage:0 - pageSize:-1 - completion:^(EMPageResult *aResult, - EMError *aError) { - NSMutableArray *list = [NSMutableArray array]; - for (EMChatroom *room in aResult.list) { - [list addObject:[room toJsonObject]]; - } - - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:list]; - }]; -} - - (void)getChatroomMemberListFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -625,23 +541,6 @@ - (void)fetchChatRoomAttributes:(NSDictionary *)param }]; } -- (void)fetchChatRoomAllAttributes:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - NSString *roomId = param[@"roomId"]; - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient.roomManager - fetchChatroomAllAttributes:roomId - completion:^(EMError *_Nullable error, - NSDictionary - *_Nullable properties) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:properties]; - }]; -} - - (void)setChatRoomAttributes:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -780,18 +679,6 @@ - (void)didDismissFromChatroom:(EMChatroom *)aChatroom [self onReceive:ExtSdkMethodKeyChatroomChanged withParams:map]; } -- (void)chatroomMuteListDidUpdate:(EMChatroom *)aChatroom - addedMutedMembers:(NSArray *)aMutes - muteExpire:(NSInteger)aMuteExpire { - NSDictionary *map = @{ - @"type" : @"onMuteListAdded", - @"roomId" : aChatroom.chatroomId, - @"mutes" : aMutes, - @"expireTime" : [NSString stringWithFormat:@"%ld", aMuteExpire] - }; - [self onReceive:ExtSdkMethodKeyChatroomChanged withParams:map]; -} - - (void)chatroomMuteListDidUpdate:(EMChatroom *)aChatroom addedMutedMembers: (NSDictionary *)aMutes { diff --git a/modules/objc/dispatch/ExtSdkClientWrapper.h b/modules/objc/dispatch/ExtSdkClientWrapper.h index 4adc3f13..9cba92ca 100644 --- a/modules/objc/dispatch/ExtSdkClientWrapper.h +++ b/modules/objc/dispatch/ExtSdkClientWrapper.h @@ -21,10 +21,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)createAccount:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)login:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -45,10 +41,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)uploadLog:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)compressLogs:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -61,41 +53,21 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)isLoggedInBefore:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getLoggedInDevicesFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)loginWithAgoraToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)isConnected:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -// 2026-09-24 1.21.0: the implementation existed in the .m file but was never -// declared here, so ExtSdkDispatch.m could not call it and renewToken fell -// through to the default "not implement" branch on iOS. - (void)renewToken:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)updatePushConfig:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -// 2026-09-24 1.21.0 -- (void)bindPushKitToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)unbindPushKitToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; +- (void)bindDeviceToken:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; - (void)getRTCTokenInfoWithChannelName:(NSDictionary *)param withMethodType:(NSString *)aChannelName @@ -105,6 +77,14 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; +- (void)bindPushKitToken:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + +- (void)unbindPushKitToken:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + @end NS_ASSUME_NONNULL_END diff --git a/modules/objc/dispatch/ExtSdkClientWrapper.m b/modules/objc/dispatch/ExtSdkClientWrapper.m index b5727c39..38362bc8 100644 --- a/modules/objc/dispatch/ExtSdkClientWrapper.m +++ b/modules/objc/dispatch/ExtSdkClientWrapper.m @@ -21,6 +21,10 @@ #import @interface ExtSdkClientWrapper () +// Set when a forced-logout delegate fires; the SDK then tears down the +// connection and connectionStateDidChange:(EMConnectionDisconnected) would +// duplicate the coded onDisconnected event, so it is swallowed once. +@property(nonatomic, assign) BOOL forcedLogoutPending; @end @implementation ExtSdkClientWrapper @@ -75,51 +79,22 @@ - (void)getToken:(NSDictionary *)param withParams:EMClient.sharedClient.accessUserToken]; } -- (void)createAccount:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *username = param[@"username"]; - NSString *password = param[@"password"]; - [EMClient.sharedClient registerWithUsername:username - password:password - completion:^(NSString *aUsername, EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyCreateAccount - withError:aError - withParams:aUsername]; - }]; -} - - (void)login:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { __weak typeof(self) weakSelf = self; NSString *username = param[@"username"]; - NSString *pwdOrToken = param[@"pwdOrToken"]; - BOOL isPwd = [param[@"isPassword"] boolValue]; - - if (isPwd) { - [EMClient.sharedClient - loginWithUsername:username - password:pwdOrToken - completion:^(NSString *aUsername, EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyLogin - withError:aError - withParams:@{@"username" : aUsername, @"token" : EMClient.sharedClient.accessUserToken}]; - }]; - } else { - [EMClient.sharedClient - loginWithUsername:username - token:pwdOrToken - completion:^(NSString *aUsername, EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyLogin - withError:aError - withParams:@{@"username" : aUsername, @"token" : EMClient.sharedClient.accessUserToken}]; - }]; - } + NSString *token = param[@"pwdOrToken"]; + + [EMClient.sharedClient + loginWithUsername:username + token:token + completion:^(NSString *aUsername, EMError *aError) { + [weakSelf onResult:result + withMethodType:ExtSdkMethodKeyLogin + withError:aError + withParams:@{@"username" : aUsername, @"token" : EMClient.sharedClient.accessUserToken}]; + }]; } - (void)logout:(NSDictionary *)param @@ -162,15 +137,6 @@ - (void)getCurrentUser:(NSDictionary *)param [self onResult:result withMethodType:ExtSdkMethodKeyGetCurrentUser withError:nil withParams:username]; } -- (void)uploadLog:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient uploadDebugLogToServerWithCompletion:^(EMError *aError) { - [weakSelf onResult:result withMethodType:ExtSdkMethodKeyUploadLog withError:aError withParams:nil]; - }]; -} - - (void)compressLogs:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -185,31 +151,18 @@ - (void)kickDevice:(NSDictionary *)param result:(nonnull id)result { __weak typeof(self) weakSelf = self; NSString *username = param[@"username"]; - NSString *password = param[@"password"]; + NSString *token = param[@"token"]; NSString *resource = param[@"resource"]; - Boolean isPassword = [param[@"isPassword"] boolValue]; - - if (isPassword) { - [EMClient.sharedClient kickDeviceWithUsername:username - password:password - resource:resource - completion:^(EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyKickDevice - withError:aError - withParams:nil]; - }]; - } else { - [EMClient.sharedClient kickDeviceWithUserId:username - token:password - resource:resource - completion:^(EMError *_Nullable aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:nil]; - }]; - } + + [EMClient.sharedClient kickDeviceWithUserId:username + token:token + resource:resource + completion:^(EMError *_Nullable aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:nil]; + }]; } - (void)kickAllDevices:(NSDictionary *)param @@ -217,37 +170,16 @@ - (void)kickAllDevices:(NSDictionary *)param result:(nonnull id)result { __weak typeof(self) weakSelf = self; NSString *username = param[@"username"]; - NSString *password = param[@"password"]; - Boolean isPassword = [param[@"isPassword"] boolValue]; - - if (isPassword) { - [EMClient.sharedClient kickAllDevicesWithUsername:username - password:password - completion:^(EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyKickAllDevices - withError:aError - withParams:nil]; - }]; - } else { - [EMClient.sharedClient kickAllDevicesWithUserId:username - token:password - completion:^(EMError *_Nullable aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:nil]; - }]; - } -} + NSString *token = param[@"token"]; -- (void)isLoggedInBefore:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - [self onResult:result - withMethodType:ExtSdkMethodKeyIsLoggedInBefore - withError:nil - withParams:@(EMClient.sharedClient.isLoggedIn)]; + [EMClient.sharedClient kickAllDevicesWithUserId:username + token:token + completion:^(EMError *_Nullable aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:nil]; + }]; } - (void)getLoggedInDevicesFromServer:(NSDictionary *)param @@ -255,57 +187,21 @@ - (void)getLoggedInDevicesFromServer:(NSDictionary *)param result:(nonnull id)result { __weak typeof(self) weakSelf = self; NSString *username = param[@"username"]; - NSString *password = param[@"password"]; - Boolean isPassword = [param[@"isPassword"] boolValue]; - - if (isPassword) { - [EMClient.sharedClient - getLoggedInDevicesFromServerWithUsername:username - password:password - completion:^(NSArray *aList, EMError *aError) { - NSMutableArray *list = [NSMutableArray array]; - for (EMDeviceConfig *deviceInfo in aList) { - [list addObject:[deviceInfo toJsonObject]]; - } - - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyGetLoggedInDevicesFromServer - withError:aError - withParams:aError ? nil : list]; - }]; - } else { - [EMClient.sharedClient getLoggedInDevicesFromServerWithUserId:username - token:password - completion:^(NSArray *_Nullable aList, - EMError *_Nullable aError) { - NSMutableArray *list = [NSMutableArray array]; - for (EMDeviceConfig *deviceInfo in aList) { - [list addObject:[deviceInfo toJsonObject]]; - } - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:aError ? nil : list]; - }]; - } -} - -- (void)loginWithAgoraToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // !!! It has been marked as invalid in the typescript language. - __weak typeof(self) weakSelf = self; - NSString *username = param[@"username"]; - NSString *agoraToken = param[@"agoratoken"]; - [EMClient.sharedClient - loginWithUsername:username - agoraToken:agoraToken - completion:^(NSString *aUsername, EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyLoginWithAgoraToken - withError:aError - withParams:@{@"username" : aUsername, @"token" : EMClient.sharedClient.accessUserToken}]; - }]; + NSString *token = param[@"token"]; + + [EMClient.sharedClient getLoggedInDevicesFromServerWithUserId:username + token:token + completion:^(NSArray *_Nullable aList, + EMError *_Nullable aError) { + NSMutableArray *list = [NSMutableArray array]; + for (EMDeviceConfig *deviceInfo in aList) { + [list addObject:[deviceInfo toJsonObject]]; + } + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:aError ? nil : list]; + }]; } - (void)isConnected:(NSDictionary *)param @@ -320,9 +216,9 @@ - (void)isConnected:(NSDictionary *)param - (void)renewToken:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { - NSString *newAgoraToken = param[@"agora_token"]; + NSString *token = param[@"token"]; __weak typeof(self) weakSelf = self; - [EMClient.sharedClient renewToken:newAgoraToken + [EMClient.sharedClient renewToken:token completion:^(EMError *_Nullable aError) { [weakSelf onResult:result withMethodType:ExtSdkMethodKeyRenewToken @@ -331,77 +227,22 @@ - (void)renewToken:(NSDictionary *)param }]; } -- (void)updatePushConfig:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - NSDictionary *dict = param[@"config"]; - NSString *deviceId = dict[@"deviceId"]; - NSString *deviceToken = dict[@"deviceToken"]; - // NSData *deviceTokenData = - // [deviceToken dataUsingEncoding:NSUTF8StringEncoding]; - - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient registerForRemoteNotificationsWithCertName:deviceId - deviceToken:deviceToken - completion:^(EMError *_Nullable aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:nil]; - }]; - // EMError* error = [EMClient.sharedClient bindDeviceToken:[deviceToken - // dataUsingEncoding:NSUTF8StringEncoding]]; [self onResult:result - // withMethodType:aChannelName withError:error withParams:nil]; - - // [EMClient.sharedClient asyncBindDeviceToken:[deviceToken - // dataUsingEncoding:NSUTF8StringEncoding]] success:^{ - // [self onResult:result withMethodType:aChannelName withError:nil - // withParams:nil]; - // } failure:^(EMError *aError) { - // [self onResult:result withMethodType:aChannelName withError:aError - // withParams:nil]; - // }]; - - // must be NSString* type for deviceToken - // [EMClient.sharedClient - // registerForRemoteNotificationsWithDeviceToken:deviceToken - // completion:^( - // EMError *_Nullable aError) - // { - // [self onResult:result - // withMethodType:aChannelName - // withError:aError - // withParams:nil]; - // }]; -} - -// 2026-09-24 1.21.0 -- (void)bindPushKitToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // The PushKit certificate name is not taken from the request: it is an initialization option - // (ChatOptions.pushKitCertName -> EMOptions.pushKitCertName) and cannot change at runtime. - // An empty certificate name makes the native call fail with EMErrorUserIllegalArgument. +- (void)bindDeviceToken:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + // The APNs certificate name is not taken from the request: it is an initialization option + // (ChatOptions.apnsCertName -> EMOptions.apnsCertName) and cannot change at runtime. + // The notifierName sent by the caller is the vendor push credential, which only Android uses. NSString *deviceToken = param[@"deviceToken"]; - // The native _extractTokenFromRawData: accepts both NSData and NSString, which is the - // same way the APNs route above passes its token through. - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient registerPushKitToken:(NSData *)deviceToken - completion:^(EMError *_Nullable aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:nil]; - }]; -} -- (void)unbindPushKitToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { __weak typeof(self) weakSelf = self; - [EMClient.sharedClient unRegisterPushKitTokenWithCompletion:^(EMError *_Nullable aError) { - [weakSelf onResult:result withMethodType:aChannelName withError:aError withParams:nil]; - }]; + [EMClient.sharedClient bindFCMToken:deviceToken + completion:^(EMError *_Nullable aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:nil]; + }]; } - (void)getRTCTokenInfoWithChannelName:(NSDictionary *)param @@ -427,7 +268,7 @@ - (void)getRTCTokenInfoWithChannelName:(NSDictionary *)param withError:aError withParams:nil]; } - + }]; } @@ -443,8 +284,32 @@ - (void)getUserIdsWithRTCUids:(NSDictionary *)param }]; } -- (void)activeNumbersReachLimitation { - [self onReceive:ExtSdkMethodKeyOnAppActiveNumberReachLimit withParams:nil]; +- (void)bindPushKitToken:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + // The PushKit certificate name is not taken from the request: it is an initialization option + // (ChatOptions.pushKitCertName -> EMOptions.pushKitCertName) and cannot change at runtime. + // An empty certificate name makes the native call fail with EMErrorUserIllegalArgument. + NSString *deviceToken = param[@"deviceToken"]; + // The native _extractTokenFromRawData: accepts both NSData and NSString, which is the + // same way the APNs route above passes its token through. + [EMClient.sharedClient registerPushKitToken:(NSData *)deviceToken + completion:^(EMError *_Nullable aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:nil]; + }]; +} + +- (void)unbindPushKitToken:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + [EMClient.sharedClient unRegisterPushKitTokenWithCompletion:^(EMError *_Nullable aError) { + [weakSelf onResult:result withMethodType:aChannelName withError:aError withParams:nil]; + }]; } #pragma - mark EMClientDelegate @@ -452,18 +317,25 @@ - (void)activeNumbersReachLimitation { - (void)connectionStateDidChange:(EMConnectionState)aConnectionState { BOOL isConnected = aConnectionState == EMConnectionConnected; if (isConnected) { + self.forcedLogoutPending = NO; [self onReceive:ExtSdkMethodKeyOnConnected withParams:nil]; } else { + if (self.forcedLogoutPending) { + self.forcedLogoutPending = NO; + return; + } [self onReceive:ExtSdkMethodKeyOnDisconnected withParams:nil]; } } -- (void)autoLoginDidCompleteWithError:(EMError *)aError { - if (aError.code == EMErrorServerServingForbidden) { - [self userDidForbidByServer]; - } else if (aError.code == EMErrorAppActiveNumbersReachLimitation) { - [self activeNumbersReachLimitation]; +- (void)emitDisconnectedWithCode:(EMErrorCode)code params:(NSDictionary *_Nullable)extra { + NSMutableDictionary *data = [NSMutableDictionary dictionary]; + data[@"errorCode"] = @(code); + if (extra) { + [data addEntriesFromDictionary:extra]; } + self.forcedLogoutPending = YES; + [self onReceive:ExtSdkMethodKeyOnDisconnected withParams:data]; } - (void)tokenWillExpire:(EMErrorCode)aErrorCode { @@ -482,34 +354,44 @@ - (void)onOfflineMessageSyncFinish { [self onReceive:ExtSdkMethodKeyOnOfflineMessageSyncFinish withParams:nil]; } -- (void)userAccountDidLoginFromOtherDevice:(NSString *_Nullable)aDeviceName { - // !!! It has been marked as invalid in the typescript language. - [self onReceive:ExtSdkMethodKeyOnUserDidLoginFromOtherDevice withParams:@{@"deviceName" : aDeviceName}]; +- (void)syncDataStartWithType:(EMDataSyncType)type { + [self onReceive:ExtSdkMethodKeyOnDataSyncStart withParams:@{@"type" : @(type)}]; +} + +- (void)syncDataFinished:(EMError *_Nullable)error type:(EMDataSyncType)type { + [self onReceive:ExtSdkMethodKeyOnDataSyncFinish + withParams:@{@"type" : @(type), @"errorCode" : @(error ? error.code : 0)}]; +} + +- (void)onDatabaseOpened:(EMError *_Nullable)error username:(NSString *_Nonnull)username { + [self onReceive:ExtSdkMethodKeyOnDatabaseOpened + withParams:@{@"username" : username, @"errorCode" : @(error ? error.code : 0)}]; } - (void)userAccountDidLoginFromOtherDeviceWithInfo:(EMLoginExtensionInfo *_Nullable)info { - [self onReceive:ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceWithInfo - withParams:@{@"deviceName" : info.deviceName, @"ext" : info.extensionInfo}]; + NSMutableDictionary *extra = [NSMutableDictionary dictionary]; + if (info.deviceName) { + extra[@"deviceName"] = info.deviceName; + } + if (info.extensionInfo) { + extra[@"ext"] = info.extensionInfo; + } + [self emitDisconnectedWithCode:EMErrorUserLoginOnAnotherDevice params:extra]; } - (void)userAccountDidRemoveFromServer { - [self onReceive:ExtSdkMethodKeyOnUserDidRemoveFromServer withParams:nil]; + [self emitDisconnectedWithCode:EMErrorUserRemoved params:nil]; } - (void)userDidForbidByServer { - [self onReceive:ExtSdkMethodKeyOnUserDidForbidByServer withParams:nil]; + [self emitDisconnectedWithCode:EMErrorServerServingForbidden params:nil]; } - (void)userAccountDidForcedToLogout:(EMError *)aError { - if (aError.code == EMErrorUserKickedByChangePassword) { - [self onReceive:ExtSdkMethodKeyOnUserDidChangePassword withParams:nil]; - } else if (aError.code == EMErrorUserLoginTooManyDevices) { - [self onReceive:ExtSdkMethodKeyOnUserDidLoginTooManyDevice withParams:nil]; - } else if (aError.code == EMErrorUserKickedByOtherDevice) { - [self onReceive:ExtSdkMethodKeyOnUserKickedByOtherDevice withParams:nil]; - } else if (aError.code == EMErrorUserAuthenticationFailed) { - [self onReceive:ExtSdkMethodKeyOnUserAuthenticationFailed withParams:nil]; + if (!aError) { + return; } + [self emitDisconnectedWithCode:aError.code params:nil]; } #pragma mark - EMMultiDevicesDelegate diff --git a/modules/objc/dispatch/ExtSdkContactManagerWrapper.h b/modules/objc/dispatch/ExtSdkContactManagerWrapper.h index 2c58305b..3b640a87 100644 --- a/modules/objc/dispatch/ExtSdkContactManagerWrapper.h +++ b/modules/objc/dispatch/ExtSdkContactManagerWrapper.h @@ -23,10 +23,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)getAllContactsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getAllContactsFromDB:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -71,14 +67,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)fetchAllContacts:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)fetchContacts:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - @end NS_ASSUME_NONNULL_END diff --git a/modules/objc/dispatch/ExtSdkContactManagerWrapper.m b/modules/objc/dispatch/ExtSdkContactManagerWrapper.m index 968d17ba..db87fc2c 100644 --- a/modules/objc/dispatch/ExtSdkContactManagerWrapper.m +++ b/modules/objc/dispatch/ExtSdkContactManagerWrapper.m @@ -64,19 +64,6 @@ - (void)deleteContact:(NSDictionary *)param }]; } -- (void)getAllContactsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient.contactManager - getContactsFromServerWithCompletion:^(NSArray *aList, EMError *aError) { - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyGetAllContactsFromServer - withError:aError - withParams:aList]; - }]; -} - - (void)getAllContactsFromDB:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -232,43 +219,6 @@ - (void)getContact:(NSDictionary *)param withParams:[contact toJsonObject]]; } -- (void)fetchAllContacts:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient.contactManager - getAllContactsFromServerWithCompletion:^( - NSArray *_Nullable aList, EMError *_Nullable aError) { - NSMutableArray *contactList = [NSMutableArray array]; - for (EMContact *contact in aList) { - [contactList addObject:[contact toJsonObject]]; - } - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:contactList]; - }]; -} - -- (void)fetchContacts:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *cursor = param[@"cursor"]; - int pageSize = [param[@"pageSize"] intValue]; - [EMClient.sharedClient.contactManager - getContactsFromServerWithCursor:cursor - pageSize:pageSize - completion:^( - EMCursorResult *_Nullable aResult, - EMError *_Nullable aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aResult toJsonObject]]; - }]; -} - #pragma mark - ExtSdkContactManagerDelegate - (void)friendshipDidAddByUser:(NSString *)aUsername { @@ -304,20 +254,6 @@ - (void)friendRequestDidDeclineByUser:(NSString *)aUsername { [self onReceive:ExtSdkMethodKeyOnContactChanged withParams:map]; } -- (void)onFriendStartSync { - NSDictionary *map = @{@"type" : @"onContactSyncStart"}; - [self onReceive:ExtSdkMethodKeyOnContactChanged withParams:map]; -} - -- (void)onFriendSyncFinished:(EMError *_Nullable)error { - NSMutableDictionary *map = [NSMutableDictionary dictionary]; - map[@"type"] = @"onContactSyncFinish"; - if (error) { - map[@"error"] = [error toJsonObject]; - } - [self onReceive:ExtSdkMethodKeyOnContactChanged withParams:map]; -} - - (void)onFriendInfoChanged:(EMContact *_Nonnull)contact { NSDictionary *map = @{@"type" : @"onContactInfoUpdate", @"contact" : [contact toJsonObject]}; diff --git a/modules/objc/dispatch/ExtSdkConversationWrapper.h b/modules/objc/dispatch/ExtSdkConversationWrapper.h index bf5c05cc..97bc9a3a 100644 --- a/modules/objc/dispatch/ExtSdkConversationWrapper.h +++ b/modules/objc/dispatch/ExtSdkConversationWrapper.h @@ -30,26 +30,14 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)markMsgAsRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)syncConversationExt:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)markAllMsgsAsRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)insertMsg:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)appendMsg:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)updateConversationMsg:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -63,10 +51,6 @@ NS_ASSUME_NONNULL_BEGIN result:(nonnull id)result; #pragma mark - load messages -- (void)loadMsgWithId:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)loadMsgWithMsgType:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; diff --git a/modules/objc/dispatch/ExtSdkConversationWrapper.m b/modules/objc/dispatch/ExtSdkConversationWrapper.m index 22be7ecf..333fc64f 100644 --- a/modules/objc/dispatch/ExtSdkConversationWrapper.m +++ b/modules/objc/dispatch/ExtSdkConversationWrapper.m @@ -95,23 +95,6 @@ - (void)getLatestMsgFromOthers:(NSDictionary *)param }]; } -- (void)markMsgAsRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [self getConversationWithParam:param - completion:^(EMConversation *conversation) { - NSString *msgId = param[@"msg_id"]; - EMError *error = nil; - [conversation markMessageAsReadWithId:msgId error:&error]; - - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyMarkMsgAsRead - withError:error - withParams:nil]; - }]; -} - - (void)syncConversationExt:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -127,21 +110,6 @@ - (void)syncConversationExt:(NSDictionary *)param }]; } -- (void)markAllMsgsAsRead:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [self getConversationWithParam:param - completion:^(EMConversation *conversation) { - EMError *error = nil; - [conversation markAllMessagesAsRead:&error]; - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyMarkAllMsgsAsRead - withError:error - withParams:nil]; - }]; -} - - (void)insertMsg:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -160,24 +128,6 @@ - (void)insertMsg:(NSDictionary *)param }]; } -- (void)appendMsg:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [self getConversationWithParam:param - completion:^(EMConversation *conversation) { - NSDictionary *msgDict = param[@"msg"]; - EMChatMessage *msg = [EMChatMessage fromJsonObject:msgDict]; - - EMError *error = nil; - [conversation appendMessage:msg error:&error]; - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyAppendMsg - withError:error - withParams:nil]; - }]; -} - - (void)updateConversationMsg:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -317,23 +267,6 @@ - (void)getMessageCountWithTimestamp:(NSDictionary *)param } #pragma mark - load messages -- (void)loadMsgWithId:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *msgId = param[@"msg_id"]; - [self getConversationWithParam:param - completion:^(EMConversation *conversation) { - EMError *error = nil; - EMChatMessage *msg = [conversation loadMessageWithId:msgId error:&error]; - - [weakSelf onResult:result - withMethodType:ExtSdkMethodKeyLoadMsgWithId - withError:error - withParams:[msg toJsonObject]]; - }]; -} - - (void)loadMsgWithMsgType:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -402,50 +335,26 @@ - (void)loadMsgWithKeywords:(NSDictionary *)param EMMessageSearchDirection direction = [ExtSdkConvertHelper searchDirectionFromString:param[@"direction"]]; EMMessageSearchScope scope = [ExtSdkConvertHelper searchScopeFromInt:[param[@"searchScope"] integerValue]]; NSArray *senders = param[@"senders"]; - if (senders == nil) { - NSString *sender = param[@"sender"]; - // !!! It has been marked as invalid in the typescript language. - [self getConversationWithParam:param - completion:^(EMConversation *conversation) { - [conversation loadMessagesWithKeyword:keywords - timestamp:timestamp - count:count - fromUser:sender - searchDirection:direction - scope:scope - completion:^(NSArray *aMessages, EMError *aError) { - NSMutableArray *msgJsonAry = [NSMutableArray array]; - for (EMChatMessage *msg in aMessages) { - [msgJsonAry addObject:[msg toJsonObject]]; - } - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:msgJsonAry]; - }]; - }]; - } else { - [self getConversationWithParam:param - completion:^(EMConversation *conversation) { - [conversation loadMessagesWithKeyword:keywords - timestamp:timestamp - count:count - fromUsers:senders - searchDirection:direction - scope:scope - completion:^(NSArray *_Nullable aMessages, - EMError *_Nullable aError) { - NSMutableArray *msgJsonAry = [NSMutableArray array]; - for (EMChatMessage *msg in aMessages) { - [msgJsonAry addObject:[msg toJsonObject]]; - } - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:msgJsonAry]; - }]; - }]; - } + [self getConversationWithParam:param + completion:^(EMConversation *conversation) { + [conversation loadMessagesWithKeyword:keywords + timestamp:timestamp + count:count + fromUsers:senders + searchDirection:direction + scope:scope + completion:^(NSArray *_Nullable aMessages, + EMError *_Nullable aError) { + NSMutableArray *msgJsonAry = [NSMutableArray array]; + for (EMChatMessage *msg in aMessages) { + [msgJsonAry addObject:[msg toJsonObject]]; + } + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:msgJsonAry]; + }]; + }]; } - (void)loadMsgWithTime:(NSDictionary *)param diff --git a/modules/objc/dispatch/ExtSdkDispatch.m b/modules/objc/dispatch/ExtSdkDispatch.m index ffe932d4..f4253a9b 100644 --- a/modules/objc/dispatch/ExtSdkDispatch.m +++ b/modules/objc/dispatch/ExtSdkDispatch.m @@ -61,9 +61,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyInitValue: [[ExtSdkClientWrapper getInstance] initSDKWithDict:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyCreateAccountValue: - [[ExtSdkClientWrapper getInstance] createAccount:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyLoginValue: [[ExtSdkClientWrapper getInstance] login:ps withMethodType:methodType result:callback]; break; @@ -76,12 +73,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyChangeAppIdValue: [[ExtSdkClientWrapper getInstance] changeAppId:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyIsLoggedInBeforeValue: - [[ExtSdkClientWrapper getInstance] isLoggedInBefore:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyUploadLogValue: - [[ExtSdkClientWrapper getInstance] uploadLog:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyCompressLogsValue: [[ExtSdkClientWrapper getInstance] compressLogs:ps withMethodType:methodType result:callback]; break; @@ -91,18 +82,12 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyKickAllDevicesValue: [[ExtSdkClientWrapper getInstance] kickAllDevices:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyCurrentUserValue: - [[ExtSdkClientWrapper getInstance] getCurrentUser:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyGetLoggedInDevicesFromServerValue: [[ExtSdkClientWrapper getInstance] getLoggedInDevicesFromServer:ps withMethodType:methodType result:callback]; break; case ExtSdkMethodKeyGetTokenValue: [[ExtSdkClientWrapper getInstance] getToken:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyLoginWithAgoraTokenValue: - [[ExtSdkClientWrapper getInstance] loginWithAgoraToken:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyIsConnectedValue: [[ExtSdkClientWrapper getInstance] isConnected:ps withMethodType:methodType result:callback]; break; @@ -112,10 +97,9 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyRenewTokenValue: [[ExtSdkClientWrapper getInstance] renewToken:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyUpdatePushConfigValue: - [[ExtSdkClientWrapper getInstance] updatePushConfig:ps withMethodType:methodType result:callback]; + case ExtSdkMethodKeyBindDeviceTokenValue: + [[ExtSdkClientWrapper getInstance] bindDeviceToken:ps withMethodType:methodType result:callback]; break; - // 2026-09-24 1.21.0 case ExtSdkMethodKeyBindPushKitTokenValue: [[ExtSdkClientWrapper getInstance] bindPushKitToken:ps withMethodType:methodType result:callback]; break; @@ -142,27 +126,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyOnTokenDidExpireValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; - case ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnUserDidRemoveFromServerValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnUserDidForbidByServerValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnUserDidChangePasswordValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnUserDidLoginTooManyDeviceValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnUserKickedByOtherDeviceValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnUserAuthenticationFailedValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; /// #pragma mark - EMContactManagerWrapper value case ExtSdkMethodKeyAddContactValue: @@ -171,11 +134,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyDeleteContactValue: [[ExtSdkContactManagerWrapper getInstance] deleteContact:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyGetAllContactsFromServerValue: - [[ExtSdkContactManagerWrapper getInstance] getAllContactsFromServer:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyGetAllContactsFromDBValue: [[ExtSdkContactManagerWrapper getInstance] getAllContactsFromDB:ps withMethodType:methodType result:callback]; break; @@ -217,24 +175,12 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyResendMessageValue: [[ExtSdkChatManagerWrapper getInstance] resendMessage:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyAckMessageReadValue: - [[ExtSdkChatManagerWrapper getInstance] ackMessageRead:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyAckGroupMessageReadValue: - [[ExtSdkChatManagerWrapper getInstance] ackGroupMessageRead:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyAckConversationReadValue: - [[ExtSdkChatManagerWrapper getInstance] ackConversationRead:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyRecallMessageValue: [[ExtSdkChatManagerWrapper getInstance] recallMessage:ps withMethodType:methodType result:callback]; break; case ExtSdkMethodKeyGetConversationValue: [[ExtSdkChatManagerWrapper getInstance] getConversationApi:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyMarkAllChatMsgAsReadValue: - [[ExtSdkChatManagerWrapper getInstance] markAllMessagesAsRead:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyGetUnreadMessageCountValue: [[ExtSdkChatManagerWrapper getInstance] getUnreadMessageCount:ps withMethodType:methodType result:callback]; break; @@ -263,36 +209,25 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyLoadAllConversationsValue: [[ExtSdkChatManagerWrapper getInstance] loadAllConversations:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyGetConversationsFromServerValue: - [[ExtSdkChatManagerWrapper getInstance] getConversationsFromServer:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyDeleteConversationValue: [[ExtSdkChatManagerWrapper getInstance] deleteConversation:ps withMethodType:methodType result:callback]; break; + case ExtSdkMethodKeyDeleteConversationsValue: + [[ExtSdkChatManagerWrapper getInstance] deleteConversations:ps withMethodType:methodType result:callback]; + break; // case ExtSdkMethodKeySetVoiceMessageListenedValue: // [callback onFail:1 withExtension:[NSString // stringWithFormat:@"not implement: %@", methodType]]; break; // case ExtSdkMethodKeyUpdateParticipantValue: // [callback onFail:1 withExtension:[NSString // stringWithFormat:@"not implement: %@", methodType]]; break; - case ExtSdkMethodKeyUpdateConversationsNameValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyFetchHistoryMessagesValue: - [[ExtSdkChatManagerWrapper getInstance] fetchHistoryMessages:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeySearchChatMsgFromDBValue: [[ExtSdkChatManagerWrapper getInstance] searchChatMsgFromDB:ps withMethodType:methodType result:callback]; break; case ExtSdkMethodKeyGetMessageValue: [[ExtSdkChatManagerWrapper getInstance] getMessageWithMessageId:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyAsyncFetchGroupAcksValue: - [[ExtSdkChatManagerWrapper getInstance] fetchGroupReadAck:ps withMethodType:methodType result:callback]; - break; /// #pragma mark - EMChatManagerDelegate value case ExtSdkMethodKeyOnMessagesReceivedValue: @@ -301,25 +236,13 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyOnCmdMessagesReceivedValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; - case ExtSdkMethodKeyOnMessagesReadValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnGroupMessageReadValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; case ExtSdkMethodKeyOnMessagesDeliveredValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; - case ExtSdkMethodKeyOnMessagesRecalledValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; case ExtSdkMethodKeyOnConversationUpdateValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; - case ExtSdkMethodKeyOnConversationHasReadValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; /// #pragma mark - EMMessageListener value case ExtSdkMethodKeyOnMessageProgressUpdateValue: @@ -331,15 +254,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyOnMessageErrorValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; - case ExtSdkMethodKeyOnMessageReadAckValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnMessageDeliveryAckValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyOnMessageStatusChangedValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; /// #pragma mark - EMConversationWrapper value @@ -349,18 +263,9 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyGetMsgCountValue: [[ExtSdkConversationWrapper getInstance] getMsgCount:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyMarkAllMsgsAsReadValue: - [[ExtSdkConversationWrapper getInstance] markAllMsgsAsRead:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyMarkMsgAsReadValue: - [[ExtSdkConversationWrapper getInstance] markMsgAsRead:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeySyncConversationExtValue: [[ExtSdkConversationWrapper getInstance] syncConversationExt:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeySyncConversationNameValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; case ExtSdkMethodKeyRemoveMsgValue: [[ExtSdkConversationWrapper getInstance] removeMsg:ps withMethodType:methodType result:callback]; break; @@ -376,16 +281,10 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyInsertMsgValue: [[ExtSdkConversationWrapper getInstance] insertMsg:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyAppendMsgValue: - [[ExtSdkConversationWrapper getInstance] appendMsg:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyUpdateConversationMsgValue: [[ExtSdkConversationWrapper getInstance] updateConversationMsg:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyLoadMsgWithIdValue: - [[ExtSdkConversationWrapper getInstance] loadMsgWithId:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyLoadMsgWithStartIdValue: [[ExtSdkConversationWrapper getInstance] loadMsgWithStartId:ps withMethodType:methodType result:callback]; break; @@ -420,15 +319,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyGetChatRoomValue: [[ExtSdkChatroomManagerWrapper getInstance] getChatRoom:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyGetAllChatRoomsValue: - [[ExtSdkChatroomManagerWrapper getInstance] getAllChatRooms:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyCreateChatRoomValue: - [[ExtSdkChatroomManagerWrapper getInstance] createChatRoom:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyDestroyChatRoomValue: - [[ExtSdkChatroomManagerWrapper getInstance] destroyChatRoom:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyChatRoomUpdateSubjectValue: [[ExtSdkChatroomManagerWrapper getInstance] chatRoomUpdateSubject:ps withMethodType:methodType result:callback]; break; @@ -534,19 +424,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyGetJoinedGroupsValue: [[ExtSdkGroupManagerWrapper getInstance] getJoinedGroups:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyGetGroupsWithoutPushNotificationValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - case ExtSdkMethodKeyGetJoinedGroupsFromServerValue: - [[ExtSdkGroupManagerWrapper getInstance] getJoinedGroupsFromServer:ps - withMethodType:methodType - result:callback]; - break; - case ExtSdkMethodKeyGetPublicGroupsFromServerValue: - [[ExtSdkGroupManagerWrapper getInstance] getPublicGroupsFromServer:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyCreateGroupValue: [[ExtSdkGroupManagerWrapper getInstance] createGroup:ps withMethodType:methodType result:callback]; break; @@ -687,19 +564,12 @@ - (void)callSdkApi:(nonnull NSString *)methodType withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyIgnoreGroupPushValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - /// #pragma mark - ExtSdkGroupManagerDelegate case ExtSdkMethodKeyOnGroupChangedValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; /// #pragma mark - EMPushManagerWrapper value - case ExtSdkMethodKeyGetImPushConfigValue: - [[ExtSdkPushManagerWrapper getInstance] getImPushConfig:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyGetImPushConfigFromServerValue: [[ExtSdkPushManagerWrapper getInstance] getImPushConfigFromServer:ps withMethodType:methodType result:callback]; break; @@ -707,60 +577,16 @@ - (void)callSdkApi:(nonnull NSString *)methodType [[ExtSdkPushManagerWrapper getInstance] updatePushNickname:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyImPushNoDisturbValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; case ExtSdkMethodKeyUpdateImPushStyleValue: [[ExtSdkPushManagerWrapper getInstance] updateImPushStyle:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyUpdateGroupPushServiceValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - case ExtSdkMethodKeyGetNoDisturbGroupsValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyBindDeviceTokenValue: - [[ExtSdkPushManagerWrapper getInstance] bindDeviceToken:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyEnablePushValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - case ExtSdkMethodKeyDisablePushValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - case ExtSdkMethodKeyGetNoPushGroupsValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - case ExtSdkMethodKeySetNoDisturbUsersValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyGetNoDisturbUsersFromServerValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; - case ExtSdkMethodKeyUpdateUserPushServiceValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - case ExtSdkMethodKeyGetNoPushUsersValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"%@ is deprecated", methodType]]; - break; - /// #pragma mark - EMUserInfoManagerWrapper value case ExtSdkMethodKeyUpdateOwnUserInfoValue: [[ExtSdkUserInfoManagerWrapper getInstance] updateOwnUserInfo:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyUpdateOwnUserInfoWithTypeValue: - [[ExtSdkUserInfoManagerWrapper getInstance] updateOwnUserInfoWithType:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyFetchUserInfoByIdValue: [[ExtSdkUserInfoManagerWrapper getInstance] fetchUserInfoById:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyFetchUserInfoByIdWithTypeValue: - [[ExtSdkUserInfoManagerWrapper getInstance] fetchUserInfoByIdWithType:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyGetLocalUserInfoByIdsValue: [[ExtSdkUserInfoManagerWrapper getInstance] getLocalUserInfoByIds:ps withMethodType:methodType @@ -819,15 +645,7 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyChatFetchReactionDetailValue: [[ExtSdkChatManagerWrapper getInstance] fetchReactionDetail:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyChatReportMessageValue: - [[ExtSdkChatManagerWrapper getInstance] reportMessage:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyFetchConversationsFromServerWithPageValue: - [[ExtSdkChatManagerWrapper getInstance] fetchConversationsFromServerWithPage:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyRemoveMessagesFromServerWithMsgIdsValue: [[ExtSdkChatManagerWrapper getInstance] removeMessagesFromServerWithMsgIds:ps withMethodType:methodType @@ -839,9 +657,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType result:callback]; break; - case ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdatedValue: - [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; - break; case ExtSdkMethodKeyChatOnMessageReactionDidChangeValue: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; @@ -862,9 +677,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeyChatGetReactionListValue: [[ExtSdkChatMessageWrapper getInstance] getReactionList:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyChatGroupAckCountValue: - [[ExtSdkChatMessageWrapper getInstance] getGroupAckCount:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyChatFetchChatThreadDetailValue: [[ExtSdkChatThreadManagerWrapper getInstance] fetchChatThreadDetail:ps @@ -922,9 +734,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType [[ExtSdkChatThreadManagerWrapper getInstance] getChatThread:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyReportPushActionValue: - [[ExtSdkPushManagerWrapper getInstance] reportPushAction:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeySetConversationSilentModeValue: [[ExtSdkPushManagerWrapper getInstance] setConversationSilentMode:ps withMethodType:methodType result:callback]; break; @@ -981,11 +790,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyfetchChatRoomAllAttributesValue: - [[ExtSdkChatroomManagerWrapper getInstance] fetchChatRoomAllAttributes:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeysetChatRoomAttributesValue: [[ExtSdkChatroomManagerWrapper getInstance] setChatRoomAttributes:ps withMethodType:methodType result:callback]; break; @@ -1020,27 +824,9 @@ - (void)callSdkApi:(nonnull NSString *)methodType [[ExtSdkGroupManagerWrapper getInstance] getGroupNamecard:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyGetConversationsFromServerWithCursorValue: - [[ExtSdkChatManagerWrapper getInstance] getConversationsFromServerWithCursor:ps - withMethodType:methodType - result:callback]; - break; - case ExtSdkMethodKeyGetPinnedConversationsFromServerWithCursorValue: - [[ExtSdkChatManagerWrapper getInstance] getPinnedConversationsFromServerWithCursor:ps - withMethodType:methodType - result:callback]; - break; - case ExtSdkMethodKeyGetConversationsFromDBWithCursorValue: - [[ExtSdkChatManagerWrapper getInstance] getConversationsFromDBWithCursor:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeyPinConversationValue: [[ExtSdkChatManagerWrapper getInstance] pinConversation:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyModifyMessageValue: - [[ExtSdkChatManagerWrapper getInstance] modifyMessage:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeyDownloadAndParseCombineMessageValue: [[ExtSdkChatManagerWrapper getInstance] downloadAndParseCombineMessage:ps withMethodType:methodType @@ -1074,12 +860,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType case ExtSdkMethodKeygetContactValue: [[ExtSdkContactManagerWrapper getInstance] getContact:ps withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyfetchAllContactsValue: - [[ExtSdkContactManagerWrapper getInstance] fetchAllContacts:ps withMethodType:methodType result:callback]; - break; - case ExtSdkMethodKeyfetchContactsValue: - [[ExtSdkContactManagerWrapper getInstance] fetchContacts:ps withMethodType:methodType result:callback]; - break; case ExtSdkMethodKeygetPinInfoValue: [[ExtSdkChatMessageWrapper getInstance] getMessagePinInfo:ps withMethodType:methodType result:callback]; @@ -1097,11 +877,6 @@ - (void)callSdkApi:(nonnull NSString *)methodType withMethodType:methodType result:callback]; break; - case ExtSdkMethodKeyfetchConversationsByOptionsValue: - [[ExtSdkChatManagerWrapper getInstance] fetchConversationsByOptions:ps - withMethodType:methodType - result:callback]; - break; case ExtSdkMethodKeydeleteAllMessageAndConversationValue: [[ExtSdkChatManagerWrapper getInstance] deleteAllMessageAndConversation:ps withMethodType:methodType @@ -1170,6 +945,46 @@ - (void)callSdkApi:(nonnull NSString *)methodType [[ExtSdkClientWrapper getInstance] getUserIdsWithRTCUids:ps withMethodType:methodType result:callback]; break; + /// #pragma mark - 2026-09-15 5.0.0 + case ExtSdkMethodKeysendMessageReadReceiptsValue: + [[ExtSdkChatManagerWrapper getInstance] sendMessageReadReceipts:ps withMethodType:methodType result:callback]; + break; + case ExtSdkMethodKeyclearConversationUnreadMessageCountValue: + [[ExtSdkChatManagerWrapper getInstance] clearConversationUnreadMessageCount:ps + withMethodType:methodType + result:callback]; + break; + case ExtSdkMethodKeyclearAllConversationUnreadMessageCountValue: + [[ExtSdkChatManagerWrapper getInstance] clearAllConversationUnreadMessageCount:ps + withMethodType:methodType + result:callback]; + break; + case ExtSdkMethodKeygetGroupMessageReadReceiptsValue: + [[ExtSdkChatManagerWrapper getInstance] getGroupMessageReadReceipts:ps + withMethodType:methodType + result:callback]; + break; + case ExtSdkMethodKeyfetchGroupMessageReadReceiptsValue: + [[ExtSdkChatManagerWrapper getInstance] fetchGroupMessageReadReceipts:ps + withMethodType:methodType + result:callback]; + break; + case ExtSdkMethodKeyupdateGroupConfigsValue: + [[ExtSdkGroupManagerWrapper getInstance] updateGroupConfigs:ps withMethodType:methodType result:callback]; + break; + case ExtSdkMethodKeyOnDataSyncStartValue: + [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; + break; + case ExtSdkMethodKeyOnDataSyncFinishValue: + [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; + break; + case ExtSdkMethodKeyOnDatabaseOpenedValue: + [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; + break; + case ExtSdkMethodKeyOnMessageReadReceiptsValue: + [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; + break; + default: [callback onFail:1 withExtension:[NSString stringWithFormat:@"not implement: %@", methodType]]; break; diff --git a/modules/objc/dispatch/ExtSdkGroupManagerWrapper.h b/modules/objc/dispatch/ExtSdkGroupManagerWrapper.h index acc70053..818e3a6e 100644 --- a/modules/objc/dispatch/ExtSdkGroupManagerWrapper.h +++ b/modules/objc/dispatch/ExtSdkGroupManagerWrapper.h @@ -24,22 +24,14 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)getGroupsWithoutPushNotification:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)getJoinedGroupsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)getPublicGroupsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)createGroup:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; +- (void)updateGroupConfigs:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result; + - (void)getGroupSpecificationFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; diff --git a/modules/objc/dispatch/ExtSdkGroupManagerWrapper.m b/modules/objc/dispatch/ExtSdkGroupManagerWrapper.m index 83e97009..6d253a40 100644 --- a/modules/objc/dispatch/ExtSdkGroupManagerWrapper.m +++ b/modules/objc/dispatch/ExtSdkGroupManagerWrapper.m @@ -54,60 +54,6 @@ - (void)getJoinedGroups:(NSDictionary *)param [weakSelf onResult:result withMethodType:aChannelName withError:nil withParams:list]; } -- (void)getGroupsWithoutPushNotification:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - EMError *error = nil; - // !!! It has been marked as invalid in the typescript language. - NSArray *groups = [EMClient.sharedClient.groupManager getGroupsWithoutPushNotification:&error]; - NSMutableArray *list = [NSMutableArray array]; - for (EMGroup *group in groups) { - [list addObject:[group toJsonObject]]; - } - [weakSelf onResult:result withMethodType:aChannelName withError:error withParams:list]; -} - -- (void)getJoinedGroupsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - int pageNum = [param[@"pageNum"] intValue]; - int pageSize = [param[@"pageSize"] intValue]; - BOOL needRole = [param[@"needRole"] boolValue]; - BOOL needMemberCount = [param[@"needMemberCount"] boolValue]; - [EMClient.sharedClient.groupManager - getJoinedGroupsFromServerWithPage:pageNum - pageSize:pageSize - needMemberCount:needMemberCount - needRole:needRole - completion:^(NSArray *aList, EMError *_Nullable aError) { - NSMutableArray *list = [NSMutableArray array]; - for (EMGroup *group in aList) { - [list addObject:[group toJsonObject]]; - } - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:list]; - }]; -} - -- (void)getPublicGroupsFromServer:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - [EMClient.sharedClient.groupManager - getPublicGroupsFromServerWithCursor:param[@"cursor"] - pageSize:[param[@"pageSize"] integerValue] - completion:^(EMCursorResult *aResult, EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aResult toJsonObject]]; - }]; -} - - (void)createGroup:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -117,7 +63,7 @@ - (void)createGroup:(NSDictionary *)param description:param[@"desc"] invitees:param[@"inviteMembers"] message:param[@"inviteReason"] - setting:[EMGroupOptions fromJsonObject:param[@"options"]] + setting:[EMGroupConfigs fromJsonObject:param[@"configs"]] completion:^(EMGroup *aGroup, EMError *aError) { [weakSelf onResult:result withMethodType:aChannelName @@ -126,35 +72,36 @@ - (void)createGroup:(NSDictionary *)param }]; } +- (void)updateGroupConfigs:(NSDictionary *)param + withMethodType:(NSString *)aChannelName + result:(nonnull id)result { + __weak typeof(self) weakSelf = self; + NSString *groupId = param[@"group_id"]; + EMGroupConfigsType types = (EMGroupConfigsType)[param[@"types"] unsignedIntegerValue]; + EMGroupConfigs *configs = [EMGroupConfigs fromJsonObject:param[@"configs"]]; + [EMClient.sharedClient.groupManager updateGroupWithId:groupId + types:types + configs:configs + completion:^(EMGroup *_Nullable group, EMError *_Nullable error) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:error + withParams:[group toJsonObject]]; + }]; +} + - (void)getGroupSpecificationFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { __weak typeof(self) weakSelf = self; NSString *groupId = param[@"groupId"]; - BOOL hasFetchMembers = NO; - BOOL fetchMembers = NO; - if (param[@"fetchMembers"]) { - fetchMembers = [param[@"fetchMembers"] boolValue]; - hasFetchMembers = YES; - } - if (hasFetchMembers) { - [EMClient.sharedClient.groupManager getGroupSpecificationFromServerWithId:groupId - fetchMembers:fetchMembers - completion:^(EMGroup *aGroup, EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aGroup toJsonObject]]; - }]; - } else { - [EMClient.sharedClient.groupManager getGroupSpecificationFromServerWithId:groupId - completion:^(EMGroup *aGroup, EMError *aError) { - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:[aGroup toJsonObject]]; - }]; - } + [EMClient.sharedClient.groupManager getGroupSpecificationFromServerWithId:groupId + completion:^(EMGroup *aGroup, EMError *aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:[aGroup toJsonObject]]; + }]; } - (void)getGroupMemberListFromServer:(NSDictionary *)param @@ -932,8 +879,13 @@ - (void)joinGroupRequestDidDecline:(NSString *_Nonnull)aGroupId reason:(NSString *_Nullable)aReason decliner:(NSString *_Nullable)aDecliner applicant:(NSString *_Nonnull)aApplicant { - NSDictionary *map = - @{@"type" : @"onRequestToJoinDeclined", @"groupId" : aGroupId, @"applicant" : aApplicant, @"reason" : aReason}; + NSDictionary *map = @{ + @"type" : @"onRequestToJoinDeclined", + @"groupId" : aGroupId, + @"applicant" : aApplicant, + @"decliner" : aDecliner ?: @"", + @"reason" : aReason ?: @"" + }; [self onReceive:ExtSdkMethodKeyOnGroupChanged withParams:map]; } @@ -996,21 +948,12 @@ - (void)groupOwnerDidUpdate:(EMGroup *)aGroup newOwner:(NSString *)aNewOwner old [self onReceive:ExtSdkMethodKeyOnGroupChanged withParams:map]; } -- (void)userDidJoinGroup:(EMGroup *)aGroup user:(NSString *)aUsername { - NSDictionary *map = @{@"type" : @"onMemberJoined", @"groupId" : aGroup.groupId, @"member" : aUsername}; - [self onReceive:ExtSdkMethodKeyOnGroupChanged withParams:map]; -} - (void)userDidJoinGroup:(EMGroup *)group users:(NSArray *)userIds { // !!! It has been marked as invalid in the typescript language. NSDictionary *map = @{@"type" : @"onMembersJoined", @"groupId" : group.groupId, @"members" : userIds}; [self onReceive:ExtSdkMethodKeyOnGroupChanged withParams:map]; } -- (void)userDidLeaveGroup:(EMGroup *)aGroup user:(NSString *)aUsername { - NSDictionary *map = @{@"type" : @"onMemberExited", @"groupId" : aGroup.groupId, @"member" : aUsername}; - [self onReceive:ExtSdkMethodKeyOnGroupChanged withParams:map]; -} - - (void)userDidLeaveGroup:(EMGroup *)group users:(NSArray *)userIds { // !!! It has been marked as invalid in the typescript language. NSDictionary *map = @{@"type" : @"onMembersExited", @"groupId" : group.groupId, @"members" : userIds}; diff --git a/modules/objc/dispatch/ExtSdkPushManagerWrapper.h b/modules/objc/dispatch/ExtSdkPushManagerWrapper.h index 8b0ada75..d239bde1 100644 --- a/modules/objc/dispatch/ExtSdkPushManagerWrapper.h +++ b/modules/objc/dispatch/ExtSdkPushManagerWrapper.h @@ -15,10 +15,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)initSDK; -- (void)getImPushConfig:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getImPushConfigFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; @@ -31,38 +27,6 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)updateGroupPushService:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)bindDeviceToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)enablePush:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)disablePush:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)getNoPushGroups:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)updateUserPushService:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)getNoPushUsers:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - -- (void)reportPushAction:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)setConversationSilentMode:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; diff --git a/modules/objc/dispatch/ExtSdkPushManagerWrapper.m b/modules/objc/dispatch/ExtSdkPushManagerWrapper.m index 94342dd2..27b91821 100644 --- a/modules/objc/dispatch/ExtSdkPushManagerWrapper.m +++ b/modules/objc/dispatch/ExtSdkPushManagerWrapper.m @@ -23,17 +23,6 @@ + (nonnull instancetype)getInstance { - (void)initSDK { } -- (void)getImPushConfig:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - EMPushOptions *options = EMClient.sharedClient.pushManager.pushOptions; - [weakSelf onResult:result - withMethodType:aChannelName - withError:nil - withParams:[options toJsonObject]]; -} - - (void)getImPushConfigFromServer:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -71,119 +60,14 @@ - (void)updateImPushStyle:(NSDictionary *)param EMPushDisplayStyle pushStyle = [param[@"pushStyle"] intValue]; - dispatch_async( - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - EMError *aError = [EMClient.sharedClient.pushManager - updatePushDisplayStyle:pushStyle]; - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:@(!aError)]; - }); -} - -- (void)updateGroupPushService:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // __weak typeof(self) weakSelf = self; - // NSString *groupId = param[@"group_id"]; - // bool enablePush = [param[@"enablePush"] boolValue]; - - // [EMClient.sharedClient.pushManager - // updatePushServiceForGroups:@[ groupId ] - // disablePush:!enablePush - // completion:^(EMError *_Nonnull aError) { - // EMGroup *aGroup = [EMGroup groupWithId:groupId]; - // [weakSelf onResult:result - // withMethodType:aChannelName - // withError:aError - // withParams:[aGroup toJsonObject]]; - // }]; -} - -- (void)bindDeviceToken:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSString *deviceToken = param[@"token"]; - dispatch_async( - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - EMError *error = [EMClient.sharedClient bindDeviceToken:deviceToken]; - [weakSelf onResult:result - withMethodType:aChannelName - withError:error - withParams:nil]; - }); -} - -- (void)enablePush:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // EMError *error = [EMClient.sharedClient.pushManager - // enableOfflinePush]; [self onResult:result - // withMethodType:aChannelName - // withError:error - // withParams:nil]; -} - -- (void)disablePush:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // int startTime = [param[@"start"] intValue]; - // int endTime = [param[@"end"] intValue]; - // EMError *error = - // [EMClient.sharedClient.pushManager - // disableOfflinePushStart:startTime - // end:endTime]; - // [self onResult:result - // withMethodType:aChannelName - // withError:error - // withParams:nil]; -} - -- (void)getNoPushGroups:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // NSArray *groups = - // [EMClient.sharedClient.pushManager noPushGroups]; - // [self onResult:result - // withMethodType:aChannelName - // withError:nil - // withParams:groups]; -} - -- (void)updateUserPushService:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // __weak typeof(self) weakSelf = self; - // NSArray *userIds = param[@"user_ids"]; - // bool noPush = [param[@"noPush"] boolValue]; - // [EMClient.sharedClient.pushManager - // updatePushServiceForUsers:userIds - // disablePush:noPush - // completion:^(EMError *_Nonnull aError) { - // [weakSelf onResult:result - // withMethodType:aChannelName - // withError:aError - // withParams:nil]; - // }]; -} - -- (void)getNoPushUsers:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // NSArray *userIds = - // [EMClient.sharedClient.pushManager noPushUIds]; - // [self onResult:result - // withMethodType:aChannelName - // withError:nil - // withParams:userIds]; -} - -- (void)reportPushAction:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - // TODO: + [EMClient.sharedClient.pushManager + updatePushDisplayStyle:pushStyle + completion:^(EMError *_Nullable aError) { + [weakSelf onResult:result + withMethodType:aChannelName + withError:aError + withParams:@(!aError)]; + }]; } - (void)setConversationSilentMode:(NSDictionary *)param diff --git a/modules/objc/dispatch/ExtSdkToJson.h b/modules/objc/dispatch/ExtSdkToJson.h index 0ee51b92..8526998a 100644 --- a/modules/objc/dispatch/ExtSdkToJson.h +++ b/modules/objc/dispatch/ExtSdkToJson.h @@ -13,21 +13,21 @@ #import #import #import -#import #import #import #import #import #import #import +#import #import -#import -#import +#import #import #import #import #import #import +#import #import #import #import @@ -49,8 +49,6 @@ NS_ASSUME_NONNULL_BEGIN + (int)roomPremissionTypeToInt:(EMChatroomPermissionType)type; + (EMGroupPermissionType)groupPremissionTypeFromInt:(int)type; + (int)groupPremissionTypeToInt:(EMGroupPermissionType)type; -+ (EMGroupStyle)groupStyleFromInt:(int)style; -+ (int)groupStyleToInt:(EMGroupStyle)style; + (EMChatType)chatTypeFromInt:(int)aType; + (int)chatTypeToInt:(EMChatType)aType; + (EMChatRoomMessagePriority)priorityFromInt:(int)priority; @@ -101,8 +99,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)toJsonObject; @end -@interface EMGroupOptions (Json) -+ (EMGroupOptions *)fromJsonObject:(NSDictionary *)dict; +@interface EMGroupConfigs (Json) ++ (EMGroupConfigs *)fromJsonObject:(NSDictionary *)dict; - (NSDictionary *)toJsonObject; @end @@ -110,7 +108,11 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)toJsonObject; @end -@interface EMGroupMessageAck (Json) +@interface EMGroupReadReceipt (Json) +- (NSDictionary *)toJsonObject; +@end + +@interface EMMessageReadReceipt (Json) - (NSDictionary *)toJsonObject; @end @@ -201,14 +203,6 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)toJsonObject; @end -@interface EMConversationFilter (Json) -+ (EMConversationFilter *)fromJsonObject:(NSDictionary *)dict; -+ (NSString *)getCursor:(NSDictionary *)dict; -+ (BOOL)getPinned:(NSDictionary *)dict; -+ (BOOL)hasMark:(NSDictionary *)dict; -+ (NSInteger)pageSize:(NSDictionary *)dict; -@end - @interface EMMessagePinInfo (Json) + (EMMessagePinInfo *)fromJsonObject:(NSDictionary *)dict; - (NSDictionary *)toJsonObject; diff --git a/modules/objc/dispatch/ExtSdkToJson.m b/modules/objc/dispatch/ExtSdkToJson.m index f648a0be..6ceca80f 100644 --- a/modules/objc/dispatch/ExtSdkToJson.m +++ b/modules/objc/dispatch/ExtSdkToJson.m @@ -104,50 +104,6 @@ + (EMGroupPermissionType)groupPremissionTypeFromInt:(int)type { return ret; } -+ (EMGroupStyle)groupStyleFromInt:(int)style { - EMGroupStyle ret = EMGroupStylePrivateOnlyOwnerInvite; - switch (style) { - case 0: { - ret = EMGroupStylePrivateOnlyOwnerInvite; - } break; - case 1: { - ret = EMGroupStylePrivateMemberCanInvite; - } break; - case 2: { - ret = EMGroupStylePublicJoinNeedApproval; - } break; - case 3: { - ret = EMGroupStylePublicOpenJoin; - } break; - default: - break; - } - - return ret; -} - -+ (int)groupStyleToInt:(EMGroupStyle)style { - int ret = 0; - switch (style) { - case EMGroupStylePrivateOnlyOwnerInvite: { - ret = 0; - } break; - case EMGroupStylePrivateMemberCanInvite: { - ret = 1; - } break; - case EMGroupStylePublicJoinNeedApproval: { - ret = 2; - } break; - case EMGroupStylePublicOpenJoin: { - ret = 3; - } break; - default: - break; - } - - return ret; -} - + (EMChatType)chatTypeFromInt:(int)aType { EMChatType type = EMChatTypeChat; switch (aType) { @@ -550,7 +506,7 @@ - (NSDictionary *)toJsonObject { ret[@"memberList"] = self.memberList; ret[@"blockList"] = self.blacklist; // !!! It has been marked as invalid in the typescript language. - ret[@"muteList"] = self.muteList; + ret[@"muteList"] = self.muteMembers.allKeys; ret[@"muteKVList"] = self.muteMembers; ret[@"isAllMemberMuted"] = @(self.isMuteAllMembers); ret[@"announcement"] = self.announcement; @@ -575,6 +531,10 @@ - (NSDictionary *)toJsonObject { ret[@"ext"] = self.ext; ret[@"marks"] = self.marks; ret[@"remindType"] = @(self.disturbType); + NSString *name = [self conversationName]; + if (name) { ret[@"name"] = name; } + NSString *avatar = [self conversationAvatar]; + if (avatar) { ret[@"avatar"] = avatar; } return ret; } @@ -634,59 +594,44 @@ - (NSDictionary *)toJsonObject { ret[@"adminList"] = self.adminList; ret[@"blockList"] = self.blacklist; ret[@"muteList"] = self.muteList; - ret[@"noticeEnable"] = @(self.isPushNotificationEnabled); ret[@"messageBlocked"] = @(self.isBlocked); ret[@"isAllMemberMuted"] = @(self.isMuteAllMembers); ret[@"permissionType"] = @([ExtSdkConvertHelper groupPremissionTypeToInt:self.permissionType]); + ret[@"isDisabled"] = @(self.isDisabled); if (self.settings != nil) { - NSMutableDictionary *opt = [NSMutableDictionary dictionary]; - opt[@"maxCount"] = @(self.settings.maxUsers); - opt[@"style"] = @(self.settings.style); - opt[@"inviteNeedConfirm"] = @([self isMemberAllowToInvite]); - opt[@"ext"] = self.settings.ext; - opt[@"isDisabled"] = @(self.isDisabled); - opt[@"isMemberOnly"] = @([self isMemberOnly]); - ret[@"options"] = opt; + ret[@"configs"] = [self.settings toJsonObject]; } return ret; } -- (BOOL)isMemberOnly { - - if (self.settings.style == EMGroupStylePrivateOnlyOwnerInvite || - self.settings.style == EMGroupStylePrivateMemberCanInvite || - self.settings.style == EMGroupStylePublicJoinNeedApproval) { - return YES; - } - - return NO; -} - -- (BOOL)isMemberAllowToInvite { - return self.settings.style == EMGroupStylePrivateMemberCanInvite; -} - @end -@implementation EMGroupOptions (Json) +@implementation EMGroupConfigs (Json) - (NSDictionary *)toJsonObject { NSMutableDictionary *ret = [NSMutableDictionary dictionary]; ret[@"maxCount"] = @(self.maxUsers); - ret[@"ext"] = self.ext; - ret[@"style"] = @([ExtSdkConvertHelper groupStyleToInt:self.style]); ret[@"inviteNeedConfirm"] = @(self.IsInviteNeedConfirm); + ret[@"ext"] = self.ext; + ret[@"isPublic"] = @(self.isPublic); + ret[@"joinApprovalRequired"] = @(self.joinApprovalRequired); + ret[@"allowInvites"] = @(self.allowInvites); return ret; } -+ (EMGroupOptions *)fromJsonObject:(NSDictionary *)dict { - EMGroupOptions *options = [[EMGroupOptions alloc] init]; - if (dict[@"maxCount"]) { options.maxUsers = [dict[@"maxCount"] intValue]; } - (dict[@"ext"] && [dict[@"ext"] length] > 0) ? (options.ext = dict[@"ext"]) : nil; - if (dict[@"inviteNeedConfirm"]) { options.IsInviteNeedConfirm = [dict[@"inviteNeedConfirm"] boolValue]; } - if (dict[@"style"]) { options.style = [ExtSdkConvertHelper groupStyleFromInt:[dict[@"style"] intValue]]; } - return options; ++ (EMGroupConfigs *)fromJsonObject:(NSDictionary *)dict { + EMGroupConfigs *configs = [[EMGroupConfigs alloc] init]; + if (dict == nil) { + return configs; + } + if (dict[@"maxCount"]) { configs.maxUsers = [dict[@"maxCount"] integerValue]; } + (dict[@"ext"] && [dict[@"ext"] length] > 0) ? (configs.ext = dict[@"ext"]) : nil; + if (dict[@"inviteNeedConfirm"]) { configs.IsInviteNeedConfirm = [dict[@"inviteNeedConfirm"] boolValue]; } + if (dict[@"isPublic"]) { configs.isPublic = [dict[@"isPublic"] boolValue]; } + if (dict[@"joinApprovalRequired"]) { configs.joinApprovalRequired = [dict[@"joinApprovalRequired"] boolValue]; } + if (dict[@"allowInvites"]) { configs.allowInvites = [dict[@"allowInvites"] boolValue]; } + return configs; } @end @@ -704,19 +649,29 @@ - (NSDictionary *)toJsonObject { @end -@implementation EMGroupMessageAck (Json) +@implementation EMGroupReadReceipt (Json) - (NSDictionary *)toJsonObject { NSMutableDictionary *data = [NSMutableDictionary dictionary]; data[@"msg_id"] = self.messageId; - data[@"ack_id"] = self.readAckId; - data[@"from"] = self.from; - data[@"content"] = self.content; + data[@"ack_id"] = self.readReceiptId; + if (self.from) { data[@"from"] = [self.from toJsonObject]; } data[@"count"] = @(self.readCount); data[@"timestamp"] = @(self.timestamp); return data; } @end +@implementation EMMessageReadReceipt (Json) +- (NSDictionary *)toJsonObject { + NSMutableDictionary *data = [NSMutableDictionary dictionary]; + data[@"msg_id"] = self.messageId; + data[@"conv_id"] = self.conversationId; + data[@"isPeerReceipt"] = @(self.isPeerReceipt); + data[@"readCount"] = @(self.readCount); + return data; +} +@end + @interface LocalFileHandler : NSObject + (NSString *)reset:(NSString *)localPath; @@ -773,12 +728,9 @@ + (EMChatMessage *)fromJsonObject:(NSDictionary *)aJson { if (aJson[@"status"]) { msg.status = [ExtSdkConvertHelper messageStatusFromInt:[aJson[@"status"] intValue]]; } if (aJson[@"localTime"]) { msg.localTime = [aJson[@"localTime"] longLongValue]; } if (aJson[@"serverTime"]) { msg.timestamp = [aJson[@"serverTime"] longLongValue]; } - if (aJson[@"hasReadAck"]) { msg.isReadAcked = [aJson[@"hasReadAck"] boolValue]; } if (aJson[@"hasDeliverAck"]) { msg.isDeliverAcked = [aJson[@"hasDeliverAck"] boolValue]; } - if (aJson[@"hasRead"]) { msg.isRead = [aJson[@"hasRead"] boolValue]; } - if (aJson[@"needGroupAck"]) { msg.isNeedGroupAck = [aJson[@"needGroupAck"] boolValue]; } - // read only - // msg.groupAckCount = [aJson[@"groupAckCount"] intValue] + if (aJson[@"isNeedReadReceipt"]) { msg.isNeedReadReceipt = [aJson[@"isNeedReadReceipt"] boolValue]; } + // read only since 5.0.0: isPeerRead / isRead / groupReadReceiptCount // msg.isContentReplaced = [aJson[@"isContentReplaced"] boolValue]; if (aJson[@"isChatThread"]) { msg.isChatThreadMessage = [aJson[@"isChatThread"] boolValue]; } if (aJson[@"attributes"]) { msg.ext = aJson[@"attributes"]; } @@ -796,12 +748,12 @@ - (NSDictionary *)toJsonObject { ret[@"msgId"] = self.messageId; ret[@"to"] = self.to; ret[@"conversationId"] = self.conversationId; - ret[@"hasRead"] = @(self.isRead); + ret[@"isRead"] = @(self.isRead); ret[@"hasDeliverAck"] = @(self.isDeliverAcked); - ret[@"hasReadAck"] = @(self.isReadAcked); - ret[@"needGroupAck"] = @(self.isNeedGroupAck); + ret[@"isPeerRead"] = @(self.isPeerRead); + ret[@"isNeedReadReceipt"] = @(self.isNeedReadReceipt); ret[@"serverTime"] = @(self.timestamp); - ret[@"groupAckCount"] = @(self.groupAckCount); + ret[@"groupReadReceiptCount"] = @(self.groupReadReceiptCount); ret[@"attributes"] = self.ext ?: @{}; ret[@"localTime"] = @(self.localTime); ret[@"status"] = @([ExtSdkConvertHelper messageStatusToInt:self.status]); @@ -1119,7 +1071,6 @@ + (EMMessageBody *)fromJsonObject:(NSDictionary *)aJson { if (aJson[@"fileStatus"]) { ret.downloadStatus = [ExtSdkConvertHelper downloadStatusFromInt:[aJson[@"fileStatus"] intValue]]; } (aJson[@"thumbnailLocalPath"] && [aJson[@"thumbnailLocalPath"] length] > 0) ? (ret.thumbnailLocalPath = aJson[@"thumbnailLocalPath"]) : nil; (aJson[@"thumbnailRemotePath"] && [aJson[@"thumbnailRemotePath"] length] > 0) ? (ret.thumbnailRemotePath = aJson[@"thumbnailRemotePath"]) : nil; - (aJson[@"thumbnailSecret"] && [aJson[@"thumbnailSecret"] length] > 0) ? (ret.thumbnailSecretKey = aJson[@"thumbnailSecret"]) : nil; if (aJson[@"width"] && aJson[@"height"]) { ret.size = CGSizeMake([aJson[@"width"] floatValue], [aJson[@"height"] floatValue]); } if (aJson[@"thumbnailStatus"]) { ret.thumbnailDownloadStatus = [ExtSdkConvertHelper downloadStatusFromInt:[aJson[@"thumbnailStatus"] intValue]]; } if (aJson[@"sendOriginalImage"]) { ret.compressionRatio = [aJson[@"sendOriginalImage"] boolValue] ? 1.0 : 0.6; } @@ -1130,7 +1081,6 @@ - (NSDictionary *)toJsonObject { NSMutableDictionary *ret = [[super toJsonObject] mutableCopy]; ret[@"thumbnailLocalPath"] = self.thumbnailLocalPath; ret[@"thumbnailRemotePath"] = self.thumbnailRemotePath; - ret[@"thumbnailSecret"] = self.thumbnailSecretKey; ret[@"thumbnailStatus"] = @([ExtSdkConvertHelper downloadStatusToInt:self.thumbnailDownloadStatus]); ret[@"fileStatus"] = @([ExtSdkConvertHelper downloadStatusToInt:self.downloadStatus]); ret[@"width"] = @(self.size.width); @@ -1234,9 +1184,7 @@ @implementation EMOptions (Json) - (NSDictionary *)toJsonObject { NSMutableDictionary *data = [NSMutableDictionary dictionary]; data[@"appKey"] = self.appkey; - data[@"autoLogin"] = @(self.isAutoLogin); data[@"debugModel"] = @(self.enableConsoleLog); - data[@"requireAck"] = @(self.enableRequireReadAck); data[@"requireDeliveryAck"] = @(self.enableDeliveryAck); data[@"sortMessageByServerTime"] = @(self.sortMessageByServerTime); data[@"acceptInvitationAlways"] = @(self.autoAcceptFriendInvitation); @@ -1247,8 +1195,6 @@ - (NSDictionary *)toJsonObject { data[@"isChatRoomOwnerLeaveAllowed"] = @(self.canChatroomOwnerLeave); data[@"serverTransfer"] = @(self.isAutoTransferMessageAttachments); data[@"usingHttpsOnly"] = @(self.usingHttpsOnly); - data[@"pushConfig"] = @{@"deviceId" : self.apnsCertName}; - // 2026-09-24 1.21.0 data[@"apnsCertName"] = self.apnsCertName; data[@"pushKitCertName"] = self.pushKitCertName; data[@"enableDNSConfig"] = @(self.enableDnsConfig); @@ -1271,12 +1217,9 @@ - (NSDictionary *)toJsonObject { data[@"webSocketPort"] = @(self.webSocketPort); data[@"dohVendor"] = @(self.dohVendor); data[@"enableUserInfo"] = @(self.enableUserInfo); - data[@"enableAutoSyncContacts"] = @(self.enableAutoSyncContacts); + data[@"dataSyncType"] = @(self.dataSyncType); // 2026-08-25 4.24.1 data[@"ntpServers"] = self.ntpServers; - // 2026-09-10 4.25.0 - data[@"enableChatroomConversation"] = @(self.enableChatroomConversation); - data[@"autoLoadConversations"] = @(self.autoLoadConversations); return data; } @@ -1292,9 +1235,7 @@ + (EMOptions *)fromJsonObject:(NSDictionary *)aJson { NSLog(@"EMOptions: fromJsonObject: appKey and appId is empty"); } - if (aJson[@"autoLogin"]) { options.isAutoLogin = [aJson[@"autoLogin"] boolValue]; } if (aJson[@"debugModel"]) { options.enableConsoleLog = [aJson[@"debugModel"] boolValue]; } - if (aJson[@"requireAck"]) { options.enableRequireReadAck = [aJson[@"requireAck"] boolValue]; } if (aJson[@"requireDeliveryAck"]) { options.enableDeliveryAck = [aJson[@"requireDeliveryAck"] boolValue]; } if (aJson[@"sortMessageByServerTime"]) { options.sortMessageByServerTime = [aJson[@"sortMessageByServerTime"] boolValue]; } if (aJson[@"acceptInvitationAlways"]) { options.autoAcceptFriendInvitation = [aJson[@"acceptInvitationAlways"] boolValue]; } @@ -1305,11 +1246,9 @@ + (EMOptions *)fromJsonObject:(NSDictionary *)aJson { if (aJson[@"isChatRoomOwnerLeaveAllowed"]) { options.canChatroomOwnerLeave = [aJson[@"isChatRoomOwnerLeaveAllowed"] boolValue]; } if (aJson[@"serverTransfer"]) { options.isAutoTransferMessageAttachments = [aJson[@"serverTransfer"] boolValue]; } if (aJson[@"usingHttpsOnly"]) { options.usingHttpsOnly = [aJson[@"usingHttpsOnly"] boolValue]; } - (aJson[@"pushConfig"] && aJson[@"pushConfig"][@"deviceId"] && [aJson[@"pushConfig"][@"deviceId"] length] > 0) ? (options.apnsCertName = aJson[@"pushConfig"][@"deviceId"]) : nil; - // 2026-09-24 1.21.0 + // The certificate names can only be set here, at initialization time: both are read by the + // native SDK when a token is bound, and must not be modified while the app is running. (aJson[@"apnsCertName"] && [aJson[@"apnsCertName"] length] > 0) ? (options.apnsCertName = aJson[@"apnsCertName"]) : nil; - // The PushKit certificate name is iOS only and must be set at initialization: the native SDK - // reads EMOptions.pushKitCertName while binding a PushKit token and rejects an empty value. (aJson[@"pushKitCertName"] && [aJson[@"pushKitCertName"] length] > 0) ? (options.pushKitCertName = aJson[@"pushKitCertName"]) : nil; if (aJson[@"enableDNSConfig"]) { options.enableDnsConfig = [aJson[@"enableDNSConfig"] boolValue]; } if (aJson[@"imPort"]) { options.chatPort = [aJson[@"imPort"] intValue]; } @@ -1337,13 +1276,10 @@ + (EMOptions *)fromJsonObject:(NSDictionary *)aJson { if (aJson[@"dohVendor"]) { options.dohVendor = [aJson[@"dohVendor"] intValue]; } if (aJson[@"enableUserInfo"]) { options.enableUserInfo = [aJson[@"enableUserInfo"] boolValue]; } - if (aJson[@"enableAutoSyncContacts"]) { options.enableAutoSyncContacts = [aJson[@"enableAutoSyncContacts"] boolValue]; } + if (aJson[@"dataSyncType"]) { options.dataSyncType = (EMDataSyncType)[aJson[@"dataSyncType"] integerValue]; } // 2026-08-25 4.24.1 if (aJson[@"ntpServers"]) { options.ntpServers = aJson[@"ntpServers"]; } - // 2026-09-10 4.25.0 - if (aJson[@"enableChatroomConversation"]) { options.enableChatroomConversation = [aJson[@"enableChatroomConversation"] boolValue]; } - if (aJson[@"autoLoadConversations"]) { options.autoLoadConversations = [aJson[@"autoLoadConversations"] boolValue]; } return options; } @@ -1580,8 +1516,6 @@ + (EMFetchServerMessagesOption *)fromJsonObject:(NSDictionary *)dict { if (dict[@"direction"]) { options.direction = [dict[@"direction"] isEqual:@(0)] ? EMMessageSearchDirectionUp : EMMessageSearchDirectionDown; } if (dict[@"startTs"]) { options.startTime = [dict[@"startTs"] longLongValue]; } if (dict[@"endTs"]) { options.endTime = [dict[@"endTs"] longLongValue]; } - // !!! It has been marked as invalid in the typescript language. - (dict[@"from"] && [dict[@"from"] length] > 0) ? (options.from = dict[@"from"]) : nil; (dict[@"senders"] && [dict[@"senders"] count] > 0) ? (options.fromIds = dict[@"senders"]) : nil; if (dict[@"needSave"]) { options.isSave = [dict[@"needSave"] boolValue]; } NSArray *types = dict[@"msgTypes"]; @@ -1644,33 +1578,6 @@ + (nonnull EMContact *)fromJsonObject:(nonnull NSDictionary *)dict { @end -@implementation EMConversationFilter (Json) - -+ (EMConversationFilter *)fromJsonObject:(NSDictionary *)dict { - EMConversationFilter *filter = [[EMConversationFilter alloc] init]; - if (dict[@"mark"]) { filter.mark = (EMMarkType)[dict[@"mark"] integerValue]; } - if (dict[@"pageSize"]) { filter.pageSize = [dict[@"pageSize"] intValue]; } - return filter; -} - -+ (NSString *)getCursor:(NSDictionary *)dict { - return dict[@"cursor"]; -} - -+ (BOOL)getPinned:(NSDictionary *)dict { - return [dict[@"pinned"] boolValue]; -} - -+ (BOOL)hasMark:(NSDictionary *)dict { - return dict[@"mark"] != nil; -} - -+ (NSInteger)pageSize:(NSDictionary *)dict { - return [dict[@"pageSize"] intValue]; -} - -@end - @implementation EMMessagePinInfo (Json) + (EMMessagePinInfo *)fromJsonObject:(NSDictionary *)dict { EMMessagePinInfo *info = [[EMMessagePinInfo alloc] init]; diff --git a/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.h b/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.h index 1c105baa..5facd586 100644 --- a/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.h +++ b/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.h @@ -19,18 +19,10 @@ NS_ASSUME_NONNULL_BEGIN withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)updateOwnUserInfoWithType:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)fetchUserInfoById:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; -- (void)fetchUserInfoByIdWithType:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result; - - (void)getLocalUserInfoByIds:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result; diff --git a/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.m b/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.m index 668be6f0..92a60358 100644 --- a/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.m +++ b/modules/objc/dispatch/ExtSdkUserInfoManagerWrapper.m @@ -47,27 +47,6 @@ - (void)updateOwnUserInfo:(NSDictionary *)param }]; } -- (void)updateOwnUserInfoWithType:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - - int typeValue = [param[@"userInfoType"] intValue]; - EMUserInfoType userInfoType = [self userInfoTypeFromInt:typeValue]; - NSString *userInfoValue = param[@"userInfoValue"]; - - [EMClient.sharedClient.userInfoManager - updateOwnUserInfo:userInfoValue - withType:userInfoType - completion:^(EMUserInfo *aUserInfo, EMError *aError) { - NSDictionary *objDic = [aUserInfo toJsonObject]; - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:objDic]; - }]; -} - - (void)fetchUserInfoById:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { @@ -91,31 +70,6 @@ - (void)fetchUserInfoById:(NSDictionary *)param }]; } -- (void)fetchUserInfoByIdWithType:(NSDictionary *)param - withMethodType:(NSString *)aChannelName - result:(nonnull id)result { - __weak typeof(self) weakSelf = self; - NSArray *userIds = param[@"userIds"]; - NSArray *userInfoTypes = param[@"userInfoTypes"]; - - [EMClient.sharedClient.userInfoManager - fetchUserInfoById:userIds - type:userInfoTypes - completion:^(NSDictionary *aUserDatas, EMError *aError) { - NSMutableDictionary *dic = NSMutableDictionary.new; - [aUserDatas enumerateKeysAndObjectsUsingBlock:^( - id _Nonnull key, id _Nonnull obj, - BOOL *_Nonnull stop) { - dic[key] = [(EMUserInfo *)obj toJsonObject]; - }]; - - [weakSelf onResult:result - withMethodType:aChannelName - withError:aError - withParams:dic]; - }]; -} - - (void)getLocalUserInfoByIds:(NSDictionary *)param withMethodType:(NSString *)aChannelName result:(nonnull id)result { diff --git a/modules/objc/dispatch/ExtSdkWrapper.m b/modules/objc/dispatch/ExtSdkWrapper.m index 860c4ea3..b993ddee 100644 --- a/modules/objc/dispatch/ExtSdkWrapper.m +++ b/modules/objc/dispatch/ExtSdkWrapper.m @@ -168,11 +168,9 @@ - (void)mergeMessage:(EMChatMessage *)msg withDBMessage:(EMChatMessage *)dbMsg { dbMsg.timestamp = msg.timestamp; dbMsg.localTime = msg.localTime; dbMsg.status = msg.status; - dbMsg.isReadAcked = msg.isReadAcked; dbMsg.isChatThreadMessage = msg.isChatThreadMessage; - dbMsg.isNeedGroupAck = msg.isNeedGroupAck; + dbMsg.isNeedReadReceipt = msg.isNeedReadReceipt; dbMsg.isDeliverAcked = msg.isDeliverAcked; - dbMsg.isRead = msg.isRead; dbMsg.isListened = msg.isListened; dbMsg.receiverList = msg.receiverList; dbMsg.priority = msg.priority; diff --git a/modules/objc/rn/ExtSdkApiObjcRN.mm b/modules/objc/rn/ExtSdkApiObjcRN.mm index 59ddd643..9725695e 100644 --- a/modules/objc/rn/ExtSdkApiObjcRN.mm +++ b/modules/objc/rn/ExtSdkApiObjcRN.mm @@ -118,21 +118,20 @@ + (BOOL)requiresMainQueueSetup { NSArray *ret = @[ /// EMClientWrapper ExtSdkMethodKeyInit, - ExtSdkMethodKeyCreateAccount, ExtSdkMethodKeyLogin, ExtSdkMethodKeyLogout, ExtSdkMethodKeyChangeAppKey, - ExtSdkMethodKeyIsLoggedInBefore, - ExtSdkMethodKeyUploadLog, ExtSdkMethodKeyCompressLogs, ExtSdkMethodKeyKickDevice, ExtSdkMethodKeyKickAllDevices, - ExtSdkMethodKeyCurrentUser, ExtSdkMethodKeyGetLoggedInDevicesFromServer, ExtSdkMethodKeyGetToken, - ExtSdkMethodKeyLoginWithAgoraToken, ExtSdkMethodKeyGetCurrentUser, ExtSdkMethodKeyIsConnected, + ExtSdkMethodKeyRenewToken, + ExtSdkMethodKeyBindDeviceToken, + ExtSdkMethodKeyBindPushKitToken, + ExtSdkMethodKeyUnbindPushKitToken, /// EMClientDelegate ExtSdkMethodKeyOnConnected, @@ -143,19 +142,13 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyOnTokenDidExpire, ExtSdkMethodKeyOnOfflineMessageSyncStart, ExtSdkMethodKeyOnOfflineMessageSyncFinish, - - ExtSdkMethodKeyOnUserDidLoginFromOtherDevice, - ExtSdkMethodKeyOnUserDidRemoveFromServer, - ExtSdkMethodKeyOnUserDidForbidByServer, - ExtSdkMethodKeyOnUserDidChangePassword, - ExtSdkMethodKeyOnUserDidLoginTooManyDevice, - ExtSdkMethodKeyOnUserKickedByOtherDevice, - ExtSdkMethodKeyOnUserAuthenticationFailed, + ExtSdkMethodKeyOnDataSyncStart, + ExtSdkMethodKeyOnDataSyncFinish, + ExtSdkMethodKeyOnDatabaseOpened, /// EMContactManagerWrapper ExtSdkMethodKeyAddContact, ExtSdkMethodKeyDeleteContact, - ExtSdkMethodKeyGetAllContactsFromServer, ExtSdkMethodKeyGetAllContactsFromDB, ExtSdkMethodKeyAddUserToBlockList, ExtSdkMethodKeyRemoveUserFromBlockList, @@ -171,28 +164,20 @@ + (BOOL)requiresMainQueueSetup { /// EMChatManagerWrapper ExtSdkMethodKeySendMessage, ExtSdkMethodKeyResendMessage, - ExtSdkMethodKeyAckMessageRead, - ExtSdkMethodKeyAckGroupMessageRead, - ExtSdkMethodKeyAckConversationRead, ExtSdkMethodKeyRecallMessage, ExtSdkMethodKeyGetConversation, - ExtSdkMethodKeyMarkAllChatMsgAsRead, ExtSdkMethodKeyGetUnreadMessageCount, ExtSdkMethodKeyUpdateChatMessage, ExtSdkMethodKeyDownloadAttachment, ExtSdkMethodKeyDownloadThumbnail, ExtSdkMethodKeyImportMessages, ExtSdkMethodKeyLoadAllConversations, - ExtSdkMethodKeyGetConversationsFromServer, ExtSdkMethodKeyDeleteConversation, // ExtSdkMethodKeySetVoiceMessageListened, // ExtSdkMethodKeyUpdateParticipant, - ExtSdkMethodKeyUpdateConversationsName, - ExtSdkMethodKeyFetchHistoryMessages, ExtSdkMethodKeySearchChatMsgFromDB, ExtSdkMethodKeyGetMessage, - ExtSdkMethodKeyAsyncFetchGroupAcks, ExtSdkMethodKeydeleteRemoteConversation, ExtSdkMethodKeyDeleteMessagesBeforeTimestamp, @@ -203,33 +188,24 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyChatRemoveReaction, ExtSdkMethodKeyChatFetchReactionList, ExtSdkMethodKeyChatFetchReactionDetail, - ExtSdkMethodKeyChatReportMessage, - ExtSdkMethodKeyFetchConversationsFromServerWithPage, ExtSdkMethodKeyRemoveMessagesFromServerWithMsgIds, ExtSdkMethodKeyRemoveMessagesFromServerWithTs, /// EMChatManagerDelegate ExtSdkMethodKeyOnMessagesReceived, ExtSdkMethodKeyOnCmdMessagesReceived, - ExtSdkMethodKeyOnMessagesRead, - ExtSdkMethodKeyOnGroupMessageRead, ExtSdkMethodKeyOnMessagesDelivered, - ExtSdkMethodKeyOnMessagesRecalled, + ExtSdkMethodKeyOnMessageReadReceipts, ExtSdkMethodKeyOnConversationUpdate, - ExtSdkMethodKeyOnConversationHasRead, - ExtSdkMethodKeyChatOnReadAckForGroupMessageUpdated, ExtSdkMethodKeyChatOnMessageReactionDidChange, /// EMMessageListener ExtSdkMethodKeyOnMessageProgressUpdate, ExtSdkMethodKeyOnMessageSuccess, ExtSdkMethodKeyOnMessageError, - ExtSdkMethodKeyOnMessageReadAck, - ExtSdkMethodKeyOnMessageDeliveryAck, - ExtSdkMethodKeyOnMessageStatusChanged, /// EMPresenceManagerDelegate ExtSdkMethodKeyOnPresenceStatusChanged, @@ -237,26 +213,20 @@ + (BOOL)requiresMainQueueSetup { /// EMConversationWrapper ExtSdkMethodKeyGetUnreadMsgCount, - ExtSdkMethodKeyMarkAllMsgsAsRead, - ExtSdkMethodKeyMarkMsgAsRead, ExtSdkMethodKeySyncConversationExt, - ExtSdkMethodKeySyncConversationName, ExtSdkMethodKeyRemoveMsg, ExtSdkMethodKeyGetLatestMsg, ExtSdkMethodKeyGetLatestMsgFromOthers, ExtSdkMethodKeyClearAllMsg, ExtSdkMethodKeyInsertMsg, - ExtSdkMethodKeyAppendMsg, ExtSdkMethodKeyUpdateConversationMsg, - ExtSdkMethodKeyLoadMsgWithId, ExtSdkMethodKeyLoadMsgWithStartId, ExtSdkMethodKeyLoadMsgWithKeywords, ExtSdkMethodKeyLoadMsgWithMsgType, ExtSdkMethodKeyLoadMsgWithTime, ExtSdkMethodKeyChatGetReactionList, - ExtSdkMethodKeyChatGroupAckCount, /// EMChatroomManagerWrapper @@ -265,9 +235,6 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyGetChatroomsFromServer, ExtSdkMethodKeyFetchChatRoomFromServer, ExtSdkMethodKeyGetChatRoom, - ExtSdkMethodKeyGetAllChatRooms, - ExtSdkMethodKeyCreateChatRoom, - ExtSdkMethodKeyDestroyChatRoom, ExtSdkMethodKeyChatRoomUpdateSubject, ExtSdkMethodKeyChatRoomUpdateDescription, ExtSdkMethodKeyGetChatroomMemberListFromServer, @@ -293,7 +260,6 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyUnMuteAllChatRoomMembers, ExtSdkMethodKeyfetchChatRoomAttributes, - ExtSdkMethodKeyfetchChatRoomAllAttributes, ExtSdkMethodKeysetChatRoomAttributes, ExtSdkMethodKeyremoveChatRoomAttributes, @@ -303,9 +269,6 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyGetGroupWithId, ExtSdkMethodKeyGetJoinedGroups, - ExtSdkMethodKeyGetGroupsWithoutPushNotification, - ExtSdkMethodKeyGetJoinedGroupsFromServer, - ExtSdkMethodKeyGetPublicGroupsFromServer, ExtSdkMethodKeyCreateGroup, ExtSdkMethodKeyGetGroupSpecificationFromServer, ExtSdkMethodKeyGetGroupMemberListFromServer, @@ -348,30 +311,15 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyDeclineInvitationFromGroup, ExtSdkMethodKeyUpdateGroupNamecard, ExtSdkMethodKeyGetGroupNamecard, - ExtSdkMethodKeyIgnoreGroupPush, ExtSdkMethodKeyOnGroupChanged, /// EMPushManagerWrapper - ExtSdkMethodKeyGetImPushConfig, ExtSdkMethodKeyGetImPushConfigFromServer, ExtSdkMethodKeyUpdatePushNickname, - ExtSdkMethodKeyImPushNoDisturb, ExtSdkMethodKeyUpdateImPushStyle, - ExtSdkMethodKeyUpdateGroupPushService, - ExtSdkMethodKeyGetNoDisturbGroups, - ExtSdkMethodKeyBindDeviceToken, - ExtSdkMethodKeyEnablePush, - ExtSdkMethodKeyDisablePush, - ExtSdkMethodKeyGetNoPushGroups, - ExtSdkMethodKeySetNoDisturbUsers, - ExtSdkMethodKeyGetNoDisturbUsersFromServer, - ExtSdkMethodKeyUpdateUserPushService, - ExtSdkMethodKeyGetNoPushUsers, - ExtSdkMethodKeyUpdatePushConfig, - - ExtSdkMethodKeyReportPushAction, + ExtSdkMethodKeySetConversationSilentMode, ExtSdkMethodKeyRemoveConversationSilentMode, ExtSdkMethodKeyFetchConversationSilentMode, @@ -383,9 +331,7 @@ + (BOOL)requiresMainQueueSetup { /// EMUserInfoManagerWrapper ExtSdkMethodKeyUpdateOwnUserInfo, - ExtSdkMethodKeyUpdateOwnUserInfoWithType, ExtSdkMethodKeyFetchUserInfoById, - ExtSdkMethodKeyFetchUserInfoByIdWithType, ExtSdkMethodKeyGetLocalUserInfoByIds, ExtSdkMethodKeySubscribeUsersInfo, ExtSdkMethodKeyUnsubscribeUsersInfo, @@ -429,12 +375,8 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeysetMemberAttributesFromGroup, ExtSdkMethodKeyfetchMemberAttributesFromGroup, ExtSdkMethodKeyfetchMembersAttributesFromGroup, - ExtSdkMethodKeyOnAppActiveNumberReachLimit, - ExtSdkMethodKeyGetConversationsFromServerWithCursor, - ExtSdkMethodKeyGetPinnedConversationsFromServerWithCursor, ExtSdkMethodKeyPinConversation, - ExtSdkMethodKeyModifyMessage, ExtSdkMethodKeyDownloadAndParseCombineMessage, ExtSdkMethodKeyVoiceMessageToText, ExtSdkMethodKeyVoiceFileToText, @@ -453,8 +395,6 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeygetAllContacts, ExtSdkMethodKeysetContactRemark, ExtSdkMethodKeygetContact, - ExtSdkMethodKeyfetchAllContacts, - ExtSdkMethodKeyfetchContacts, ExtSdkMethodKeyfetchJoinedGroupCount, ExtSdkMethodKeyDownloadAttachmentInCombine, ExtSdkMethodKeyDownloadThumbnailInCombine, @@ -464,7 +404,6 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeyonMessagePinChanged, ExtSdkMethodKeyaddRemoteAndLocalConversationsMark, ExtSdkMethodKeydeleteRemoteAndLocalConversationsMark, - ExtSdkMethodKeyfetchConversationsByOptions, ExtSdkMethodKeydeleteAllMessageAndConversation, ExtSdkMethodKeypinMessage, ExtSdkMethodKeyunpinMessage, @@ -476,7 +415,6 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeySearchMessagesFromServer, ExtSdkMethodKeyremoveMessagesWithTimestamp, ExtSdkMethodKeygetMessageCountWithTimestamp, - ExtSdkMethodKeyOnUserDidLoginFromOtherDeviceWithInfo, ExtSdkMethodKeygetMessageCount, ExtSdkMethodKeyfetchMemberInfoListFromServer, @@ -491,6 +429,13 @@ + (BOOL)requiresMainQueueSetup { ExtSdkMethodKeygetUserIdsWithRTCUids, ExtSdkMethodKeyOnStreamMessagesReceived, + + ExtSdkMethodKeysendMessageReadReceipts, + ExtSdkMethodKeyclearConversationUnreadMessageCount, + ExtSdkMethodKeyclearAllConversationUnreadMessageCount, + ExtSdkMethodKeygetGroupMessageReadReceipts, + ExtSdkMethodKeyfetchGroupMessageReadReceipts, + ExtSdkMethodKeyupdateGroupConfigs, ]; // NSLog(@"%@: supportedEvents: %@", TAG, ret); return ret; diff --git a/package.json b/package.json index 45b32ff0..8f75ff17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-chat-sdk", - "version": "1.21.0", + "version": "5.0.0", "description": "An instant messaging SDK product is a software development kit that provides interfaces and functionalities for building and integrating real-time messaging features, enabling developers to implement instant messaging services within their applications.", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", @@ -49,11 +49,14 @@ "test": "jest", "test:unit": "jest src/__tests__/unit", "test:contract": "jest src/__tests__/contract", + "test:ci-scripts": "node --test scripts/ci/test_script_results.js", "test:coverage": "jest --coverage", + "report:api": "node scripts/ci/auto_report.js", "scan:deprecated": "bash scripts/scan-deprecated.sh", "scan:deprecated:android": "bash scripts/scan-deprecated-android.sh", "scan:deprecated:ios": "bash scripts/scan-deprecated-ios.sh", "gen:env_file": "cd scripts && node generate-env.js", + "env:gettoken": "node scripts/env-gettoken.js", "gen:version_file": "cd scripts && node generate-version.js", "gen:cmake_file": "cd scripts && node generate-cmake.js", "check:circular:dpdm": "dpdm --circular --transform --include ts src/", diff --git a/scripts/ci/assert_script.js b/scripts/ci/assert_script.js index 1ed0c14d..aeced2e4 100755 --- a/scripts/ci/assert_script.js +++ b/scripts/ci/assert_script.js @@ -1,77 +1,28 @@ #!/usr/bin/env node /** - * Auto-mode script assertion script (no third-party dependencies). Shared by - * the no-login smoke test and the single-account nightly test. + * Assert an example auto-mode run against the expectations in its script. + * The parsing and classification logic is shared with auto_report.js. * * Usage: * node scripts/ci/assert_script.js * - * Reads the auto-mode script JSON and the device log file produced by the - * example app (one JSON entry per line, optionally prefixed with - * "[APITEST] " when captured from logcat), then verifies: - * 1. `api.ChatClient.init` exists and succeeded; - * 2. when the log contains `api.ChatClient.login` entries (the script has a - * login section, or login params are derived from API_CONFIG), the last - * one must have succeeded; - * 3. every step has a `script.step[i]` entry whose api matches the script - * and whose result matches the expectation: - * - `expect.errorCode`: `result.success === false` and - * `result.error.code === expect.errorCode` (no-login smoke); - * - `expect.success === true`: `result.success === true` - * (logged-in nightly); - * 4. `script.done` exists with `total === steps.length`. - * - * When several entries share a source (stale entries from an earlier run in - * the same log file), the last one wins. - * - * Exit code 0 on success, 1 on any assertion failure, 2 on usage/IO errors. + * Exit code 0 on success, 1 on assertion failure, 2 on usage/IO errors. */ -const fs = require('fs'); - -const APITEST_PREFIX = '[APITEST] '; +const fs = require('node:fs'); +const { evaluateScript, parseLogText } = require('./script_results'); function fail2(message) { console.error(`assert_script: error: ${message}`); process.exit(2); } -function parseLogEntries(logPath) { - const text = fs.readFileSync(logPath, 'utf8'); - /** @type {Map} source -> last entry with that source */ - const bySource = new Map(); - let parsed = 0; - for (const rawLine of text.split('\n')) { - let line = rawLine.trim(); - if (line.length === 0) { - continue; - } - const idx = line.indexOf(APITEST_PREFIX); - if (idx >= 0) { - line = line.slice(idx + APITEST_PREFIX.length).trim(); - } - if (!line.startsWith('{')) { - continue; - } - let entry; - try { - entry = JSON.parse(line); - } catch { - continue; // not a structured log line (e.g. truncated logcat output) - } - if (entry == null || typeof entry.source !== 'string') { - continue; - } - bySource.set(entry.source, entry); - parsed++; - } - return { bySource, parsed }; -} - function main() { const [, , scriptPath, logPath] = process.argv; if (scriptPath == null || logPath == null) { - fail2('usage: node assert_script.js '); + fail2( + 'usage: node scripts/ci/assert_script.js ' + ); } if (!fs.existsSync(scriptPath)) { fail2(`script not found: ${scriptPath}`); @@ -80,112 +31,40 @@ function main() { fail2(`log file not found: ${logPath}`); } - const script = JSON.parse(fs.readFileSync(scriptPath, 'utf8')); - const steps = Array.isArray(script.steps) ? script.steps : []; - if (steps.length === 0) { + let script; + try { + script = JSON.parse(fs.readFileSync(scriptPath, 'utf8')); + } catch (error) { + fail2(`unable to parse script: ${error.message}`); + } + if (!Array.isArray(script.steps) || script.steps.length === 0) { fail2('script has no steps'); } - const { bySource, parsed } = parseLogEntries(logPath); + const entries = parseLogText(fs.readFileSync(logPath, 'utf8')); console.log( - `assert_script: ${parsed} structured entries parsed from ${logPath}` + `assert_script: ${entries.length} structured entries parsed from ${logPath}` ); + const evaluation = evaluateScript(script, entries); - const failures = []; - - // 1. init must have succeeded. - const initEntry = bySource.get('api.ChatClient.init'); - if (initEntry == null) { - failures.push('missing log entry: api.ChatClient.init'); - } else if (initEntry.payload?.success !== true) { - failures.push( - `api.ChatClient.init did not succeed: ${JSON.stringify( - initEntry.payload - )}` - ); - } - - // 2. when login was attempted (script login section or API_CONFIG-derived - // credentials), the last login entry must have succeeded. - const loginEntry = bySource.get('api.ChatClient.login'); - if (loginEntry != null && loginEntry.payload?.success !== true) { - failures.push( - `api.ChatClient.login did not succeed: ${JSON.stringify( - loginEntry.payload - )}` + if (evaluation.failures.length > 0) { + console.error( + `assert_script: FAILED (${evaluation.failures.length} problem(s)):` ); - } - - // 3. every step must match its expectation. - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const tag = `step[${i}] ${step.api}`; - const expectCode = step.expect?.errorCode; - const expectSuccess = step.expect?.success === true; - if (expectCode == null && !expectSuccess) { - failures.push(`${tag}: script step has neither expect.errorCode nor expect.success`); - continue; - } - const entry = bySource.get(`script.step[${i}]`); - if (entry == null) { - failures.push(`${tag}: missing log entry script.step[${i}]`); - continue; - } - const payload = entry.payload ?? {}; - if (payload.api !== step.api) { - failures.push( - `${tag}: log api mismatch, got ${JSON.stringify(payload.api)}` - ); - continue; - } - const result = payload.result ?? {}; - if (expectSuccess) { - if (result.success !== true) { - failures.push( - `${tag}: expected success but got: ${JSON.stringify(result)}` - ); - } - continue; - } - if (result.success !== false) { - failures.push( - `${tag}: expected failure but got success: ${JSON.stringify(result)}` - ); - continue; - } - const actualCode = result.error?.code; - if (actualCode !== expectCode) { - failures.push( - `${tag}: expected error code ${expectCode}, got ${JSON.stringify( - actualCode - )} (${JSON.stringify(result.error?.message)})` - ); - } - } - - // 4. script.done must cover all steps. - const doneEntry = bySource.get('script.done'); - if (doneEntry == null) { - failures.push('missing log entry: script.done (script did not finish?)'); - } else if (doneEntry.payload?.total !== steps.length) { - failures.push( - `script.done total mismatch: expected ${steps.length}, got ${JSON.stringify( - doneEntry.payload?.total - )}` - ); - } - - if (failures.length > 0) { - console.error(`assert_script: FAILED (${failures.length} problem(s)):`); - for (const f of failures) { - console.error(` - ${f}`); + for (const failure of evaluation.failures) { + console.error(` - ${failure}`); } process.exit(1); } - const scope = loginEntry != null ? 'init + login' : 'init'; + + const scope = evaluation.loginEntry != null ? 'init + login' : 'init'; console.log( - `assert_script: OK (${scope} + ${steps.length} step(s), all as expected)` + `assert_script: OK (${scope} + ${script.steps.length} step(s), all as expected)` ); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { main }; diff --git a/scripts/ci/auto_report.js b/scripts/ci/auto_report.js new file mode 100755 index 00000000..fb497b21 --- /dev/null +++ b/scripts/ci/auto_report.js @@ -0,0 +1,784 @@ +#!/usr/bin/env node +/** + * Run an example auto-mode script and archive traceable, local-only evidence. + * It reuses smoke_local.sh and the platform drivers instead of duplicating + * React Native build/install logic. + */ + +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const { execFileSync, spawnSync } = require('node:child_process'); +const { evaluateScript, parseLogText } = require('./script_results'); + +const REPO_ROOT = path.resolve(__dirname, '../..'); +const DEFAULT_SCRIPT = path.join( + REPO_ROOT, + 'example/ci/port_5_0_0_positive.json' +); +const CRASH_PATTERN = + /FATAL EXCEPTION|SIG(?:SEGV|ABRT)|NullPointerException|uncaught exception|Terminating app|fatal error|REPORT_TIMEOUT|Lost connection to device/i; + +function usage() { + return `Usage: + yarn report:api --platform [--device ] [--script ] + yarn report:api --platform --from-log [--crash-log ] + yarn report:api --android-report --ios-report + +The default script is example/ci/port_5_0_0_positive.json. Run the negative +path with --script example/ci/port_5_0_0_negative.json. Per-run evidence is +written under build/reports/// and remains Git-ignored. +`; +} + +function parseArgs(argv) { + const options = {}; + for (let index = 0; index < argv.length; index++) { + const arg = argv[index]; + if (arg === '--help' || arg === '-h') { + options.help = true; + continue; + } + if (!arg.startsWith('--')) { + throw new Error(`unknown argument: ${arg}`); + } + const value = argv[++index]; + if (value == null || value.startsWith('--')) { + throw new Error(`missing value for ${arg}`); + } + options[arg.slice(2)] = value; + } + return options; +} + +function safeName(value) { + return String(value).replace(/[^A-Za-z0-9_-]/g, '_'); +} + +function timestamp(date) { + return date + .toISOString() + .replace(/[^0-9]/g, '') + .slice(0, 14); +} + +function sha256(filePath) { + return crypto + .createHash('sha256') + .update(fs.readFileSync(filePath)) + .digest('hex'); +} + +function runText(executable, args, options = {}) { + try { + return execFileSync(executable, args, { + cwd: REPO_ROOT, + encoding: 'utf8', + env: options.env ?? process.env, + maxBuffer: 50 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } catch { + return ''; + } +} + +function readPackageVersion() { + const packageJson = JSON.parse( + fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8') + ); + return String(packageJson.version); +} + +function readNativeVersions() { + const podspec = fs.readFileSync( + path.join(REPO_ROOT, 'ChatSdk.podspec'), + 'utf8' + ); + const gradle = fs.readFileSync( + path.join(REPO_ROOT, 'android/build.gradle'), + 'utf8' + ); + return { + ios: + /minimumVersion:\s*'([^']+)'/.exec(podspec)?.[1] ?? + /HyphenateChat',\s*'~>\s*([^']+)'/.exec(podspec)?.[1] ?? + null, + android: /io\.hyphenate:hyphenate-chat:([^'\s]+)/.exec(gradle)?.[1] ?? null, + }; +} + +function relativeToRepo(filePath) { + const relative = path.relative(REPO_ROOT, filePath); + return relative.startsWith('..') ? filePath : relative; +} + +function resolveAndroidDevice(requested) { + if (requested != null) return requested; + const output = runText('adb', ['devices']); + const devices = output + .split('\n') + .map((line) => /^(\S+)\s+device$/.exec(line)?.[1]) + .filter(Boolean); + if (devices.length > 1) { + throw new Error('multiple Android devices are online; pass --device'); + } + return devices[0] ?? 'auto'; +} + +function resolveIosDevice(requested) { + if (requested != null) return requested; + const output = runText('xcrun', [ + 'simctl', + 'list', + 'devices', + 'booted', + '-j', + ]); + if (output.length === 0) return 'auto'; + try { + const decoded = JSON.parse(output); + for (const values of Object.values(decoded.devices ?? {})) { + for (const item of values) { + if (item.state === 'Booted') return item.udid; + } + } + } catch { + // Let smoke_local.sh report simulator discovery failures. + } + return 'auto'; +} + +function resolveDevice(platform, requested) { + return platform === 'android' + ? resolveAndroidDevice(requested) + : resolveIosDevice(requested); +} + +function sanitize(value) { + if (Array.isArray(value)) return value.map(sanitize); + if (value != null && typeof value === 'object') { + const result = {}; + for (const [key, item] of Object.entries(value)) { + if (/token|password|client.?secret|authorization/i.test(key)) { + result[key] = ''; + } else { + result[key] = sanitize(item); + } + } + return result; + } + if (typeof value === 'string') { + return value + .replace( + /(token|password|client[_-]?secret|authorization)\s*[:=]\s*[^,}\s]+/gi, + '$1=' + ) + .replace(/(appkey|app_key)\s*[:=]\s*[^,}\s]+/gi, '$1='); + } + return value; +} + +function extractCrashEvidence(text) { + const lines = String(text).split('\n'); + const selected = new Set(); + for (let index = 0; index < lines.length; index++) { + if (!CRASH_PATTERN.test(lines[index])) continue; + const start = Math.max(0, index - 2); + const end = Math.min(lines.length, index + 25); + for (let cursor = start; cursor < end; cursor++) selected.add(cursor); + } + return Array.from(selected) + .sort((a, b) => a - b) + .map((index) => sanitize(lines[index])) + .join('\n') + .trim(); +} + +function captureCrashEvidence(platform, device, env) { + if (platform === 'android') { + const output = runText('adb', ['logcat', '-d', '-v', 'threadtime'], { + env, + }); + return extractCrashEvidence(output); + } + if (device === 'auto') return ''; + const output = runText('xcrun', [ + 'simctl', + 'spawn', + device, + 'log', + 'show', + '--style', + 'compact', + '--last', + '15m', + '--predicate', + 'process == "ChatSdkExample"', + ]); + return extractCrashEvidence(output); +} + +function clearAndroidLogcat(platform, env) { + if (platform !== 'android') return; + spawnSync('adb', ['logcat', '-c'], { + cwd: REPO_ROOT, + env, + stdio: 'ignore', + }); +} + +function markdownCell(value, maxLength = 360) { + let text = typeof value === 'string' ? value : JSON.stringify(value); + if (text == null) text = 'null'; + text = text.replace(/\|/g, '\\|').replace(/\r?\n/g, ' '); + return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text; +} + +function collectIssues(scriptPath, platform, script, evaluation, crashText) { + const issues = []; + let crashSeen = false; + for (const outcome of evaluation.outcomes) { + if (outcome.status === 'passed') continue; + if (crashSeen && outcome.status === 'not-run') continue; + const crashKey = + outcome.id === 'fetch_group_receipt_missing' && + outcome.status === 'crashed' + ? 'android-missing-message-crash' + : null; + const iosEmptySuccessKey = + platform === 'ios' && + outcome.id === 'fetch_group_receipt_missing' && + outcome.actual?.success === true + ? 'ios-unknown-message-pagination-empty-success' + : null; + issues.push({ + key: crashKey ?? iosEmptySuccessKey ?? `${outcome.id}-unexpected-result`, + status: outcome.status === 'blocked' ? 'blocked' : 'needs-review', + summary: `${outcome.api} was ${outcome.status}`, + evidence: outcome.reason, + }); + if (outcome.status === 'crashed') crashSeen = true; + } + if (crashSeen) { + const skipped = evaluation.outcomes.filter( + (item) => item.status === 'not-run' + ); + if (skipped.length > 0) { + issues.push({ + key: 'steps-not-run-after-crash', + status: 'blocked', + summary: `${skipped.length} later step(s) could not run after the crash`, + evidence: skipped.map((item) => item.id).join(', '), + }); + } + } + + if ( + crashText.length > 0 && + !issues.some((item) => item.key.includes('crash')) + ) { + issues.push({ + key: 'native-crash-evidence', + status: 'needs-review', + summary: 'Native crash evidence was captured', + evidence: 'See crash.log', + }); + } + + const scriptName = path.basename(scriptPath); + if (!scriptName.includes('port_5_0_0')) return issues; + const isPositivePath = scriptName === 'port_5_0_0_positive.json'; + const isNegativePath = scriptName === 'port_5_0_0_negative.json'; + + const requiredEvents = [ + 'ChatConnectEventListener.onDatabaseOpened', + 'ChatConnectEventListener.onDataSyncStart', + 'ChatConnectEventListener.onDataSyncFinish', + ]; + for (const source of requiredEvents) { + if (!evaluation.bySource.has(source)) { + issues.push({ + key: `${source.split('.').at(-1)}-unobserved`, + status: 'needs-review', + summary: `Required 5.0.0 event ${source} was not observed`, + evidence: 'events.jsonl', + }); + } + } + + if ( + isPositivePath && + script.steps.some( + (step) => step.api === 'ChatManager.sendMessageReadReceipts' + ) && + !evaluation.bySource.has('ChatMessageEventListener.onMessageReadReceipts') + ) { + issues.push({ + key: 'message-receipt-event-unverified', + status: 'needs-second-account', + summary: + 'onMessageReadReceipts was not observed in the single-account run', + evidence: 'Run a coordinated second-account read scenario', + }); + } + + const conversations = evaluation.outcomes.find((item) => + ['convs', 'conversations'].includes(item.id) + ); + if (conversations?.status === 'passed') { + const data = conversations.actual?.data; + const inspectable = + Array.isArray(data) && + data.every( + (item) => + item != null && + typeof item === 'object' && + Object.hasOwn(item, 'displayName') && + Object.hasOwn(item, 'displayAvatar') + ); + if (!inspectable) { + issues.push({ + key: 'conversation-output-not-inspectable', + status: 'confirmed', + summary: + 'Conversation output does not expose displayName/displayAvatar', + evidence: 'See the conversation step in steps.json', + }); + } + } + + const serverReceipts = evaluation.outcomes.find( + (item) => item.id === 'grp_receipts_server' + ); + if ( + platform === 'ios' && + serverReceipts?.status === 'passed' && + typeof serverReceipts.actual?.data?.totalCount !== 'number' + ) { + issues.push({ + key: 'ios-total-count-missing', + status: 'confirmed', + summary: 'iOS group-receipt pagination did not return numeric totalCount', + evidence: 'See grp_receipts_server in steps.json', + }); + } + if ( + serverReceipts?.status === 'passed' && + Array.isArray(serverReceipts.actual?.data?.list) && + serverReceipts.actual.data.list.length === 0 + ) { + issues.push({ + key: 'group-member-info-unverified', + status: 'needs-second-account', + summary: + 'The receipt list was empty, so ChatGroupMemberInfo was not decoded', + evidence: 'Use a second account to read the group message', + }); + } + + if (isNegativePath) { + for (const id of ['receipt_missing', 'group_receipt_missing']) { + if (!script.steps.some((step) => step.id === id)) { + issues.push({ + key: `${id}-coverage-gap`, + status: 'blocked', + summary: `Missing-message error path ${id} is absent from the script`, + evidence: relativeToRepo(scriptPath), + }); + } + } + issues.push({ + key: 'fetch-group-receipt-missing-disabled', + status: 'known-crash', + summary: + 'The missing-message fetchGroupMessageReadReceipts case is intentionally disabled', + evidence: + 'Android native crashes before returning; see docs/porting/5.0.0/04-verification.md', + }); + } + return issues; +} + +function writeSummary(reportDir, metadata, evaluation, issues) { + const listenerSources = Array.from(evaluation.bySource.keys()) + .filter((source) => source.includes('EventListener.')) + .sort(); + const lines = [ + `# Auto-mode run ${metadata.runId}`, + '', + `- Platform: \`${metadata.platform}\``, + `- Device: \`${metadata.device}\``, + `- Commit: \`${metadata.commit}\`${metadata.worktreeDirty ? ' (dirty)' : ''}`, + `- Started: \`${metadata.startedAt}\``, + `- Finished: \`${metadata.finishedAt}\``, + `- Structured events: \`${evaluation.entries.length}\``, + `- script.done: \`${markdownCell(evaluation.doneEntry?.payload ?? 'missing')}\``, + `- Expected outcomes: \`${markdownCell(evaluation.counts)}\``, + `- Crash evidence: \`${metadata.crashEvidence ? 'see crash.log' : 'none'}\``, + `- Overall: \`${evaluation.failures.length === 0 ? 'passed' : 'failed'}\``, + '', + '> `script.done.failed` counts raw API failures, including expected error-path results. The expected-outcome classification above is authoritative.', + '', + '## Step outcomes', + '', + '| # | id | API | status | expected | actual |', + '| ---: | --- | --- | --- | --- | --- |', + ]; + for (const outcome of evaluation.outcomes) { + lines.push( + `| ${outcome.index} | \`${markdownCell(outcome.id)}\` | \`${markdownCell( + outcome.api + )}\` | ${outcome.status} | \`${markdownCell( + outcome.expected + )}\` | \`${markdownCell(outcome.actual)}\` |` + ); + } + lines.push('', '## Observed listener events', ''); + if (listenerSources.length === 0) { + lines.push('- None'); + } else { + for (const source of listenerSources) lines.push(`- \`${source}\``); + } + lines.push('', '## Issue candidates', ''); + if (issues.length === 0) { + lines.push('- None'); + } else { + for (const issue of issues) { + lines.push(`- \`${issue.key}\` — ${issue.status}: ${issue.summary}`); + } + } + fs.writeFileSync(path.join(reportDir, 'summary.md'), `${lines.join('\n')}\n`); +} + +function writeIssues(reportDir, metadata, issues) { + const lines = [ + '# Issue candidates', + '', + `Run: \`${metadata.runId}\``, + '', + 'Candidate keys are local review labels, not external issue IDs. Confirm and sanitize findings before copying them into docs/porting/.', + '', + ]; + if (issues.length === 0) { + lines.push('No issue candidates.'); + } + for (const issue of issues) { + lines.push( + `## \`${issue.key}\``, + '', + `- Status: \`${issue.status}\``, + `- Summary: ${issue.summary}`, + `- Evidence: ${issue.evidence}`, + '' + ); + } + fs.writeFileSync(path.join(reportDir, 'issues.md'), `${lines.join('\n')}\n`); +} + +function rewriteSanitizedEvents(eventsFile, entries) { + const text = entries + .map((entry) => JSON.stringify(sanitize(entry))) + .join('\n'); + fs.writeFileSync(eventsFile, text.length === 0 ? '' : `${text}\n`); +} + +function writeReport({ + reportDir, + metadata, + scriptPath, + script, + eventsFile, + crashText, +}) { + const rawEvents = fs.existsSync(eventsFile) + ? fs.readFileSync(eventsFile, 'utf8') + : ''; + const entries = parseLogText(rawEvents); + rewriteSanitizedEvents(eventsFile, entries); + const evaluation = evaluateScript(script, entries, { + crashEvidence: crashText.length > 0, + }); + const issues = collectIssues( + scriptPath, + metadata.platform, + script, + evaluation, + crashText + ); + const steps = evaluation.outcomes.map((outcome) => sanitize(outcome)); + fs.writeFileSync( + path.join(reportDir, 'steps.json'), + `${JSON.stringify(steps, null, 2)}\n` + ); + fs.writeFileSync(path.join(reportDir, 'crash.log'), crashText); + fs.writeFileSync( + path.join(reportDir, 'run.json'), + `${JSON.stringify(metadata, null, 2)}\n` + ); + writeSummary(reportDir, metadata, evaluation, issues); + writeIssues(reportDir, metadata, issues); + return { evaluation, issues }; +} + +function shapeOf(value) { + if (value === null) return 'null'; + if (Array.isArray(value)) { + return value.length === 0 ? [] : [shapeOf(value[0])]; + } + if (typeof value === 'object') { + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, shapeOf(value[key])]) + ); + } + return typeof value; +} + +function differentShapePaths(first, second, prefix = '') { + if (JSON.stringify(first) === JSON.stringify(second)) return []; + if ( + first == null || + second == null || + Array.isArray(first) !== Array.isArray(second) || + typeof first !== 'object' || + typeof second !== 'object' + ) { + return [prefix || '$']; + } + if (Array.isArray(first)) { + if (first.length === 0 || second.length === 0) return [prefix || '$']; + return differentShapePaths(first[0], second[0], `${prefix}[]`); + } + const keys = Array.from( + new Set([...Object.keys(first), ...Object.keys(second)]) + ).sort(); + return keys.flatMap((key) => + differentShapePaths( + first[key], + second[key], + prefix.length === 0 ? key : `${prefix}.${key}` + ) + ); +} + +function resultSemantics(outcome) { + return { + success: outcome.actual?.success ?? null, + errorCode: outcome.actual?.error?.code ?? null, + }; +} + +function loadRun(runPath) { + const directory = fs.statSync(runPath).isDirectory() + ? runPath + : path.dirname(runPath); + return { + directory, + metadata: JSON.parse( + fs.readFileSync(path.join(directory, 'run.json'), 'utf8') + ), + steps: JSON.parse( + fs.readFileSync(path.join(directory, 'steps.json'), 'utf8') + ), + }; +} + +function compareReports(androidPath, iosPath, outputRoot) { + const android = loadRun(path.resolve(androidPath)); + const ios = loadRun(path.resolve(iosPath)); + const androidById = new Map(android.steps.map((item) => [item.id, item])); + const iosById = new Map(ios.steps.map((item) => [item.id, item])); + const ids = Array.from(new Set([...androidById.keys(), ...iosById.keys()])); + let mismatches = 0; + const lines = [ + '# Android / iOS auto-mode comparison', + '', + `- Android: \`${android.metadata.runId}\``, + `- iOS: \`${ios.metadata.runId}\``, + '', + '| step | Android | iOS | result semantics | response shape | status |', + '| --- | --- | --- | --- | --- | --- |', + ]; + for (const id of ids) { + const a = androidById.get(id); + const i = iosById.get(id); + let status = '✅'; + if ( + a == null || + i == null || + a.status !== i.status || + a.status !== 'passed' + ) { + status = '❌'; + mismatches++; + } + const aSemantics = a == null ? null : resultSemantics(a); + const iSemantics = i == null ? null : resultSemantics(i); + const semanticsEqual = + JSON.stringify(aSemantics) === JSON.stringify(iSemantics); + const aShape = a == null ? null : shapeOf(a.actual); + const iShape = i == null ? null : shapeOf(i.actual); + const shapeEqual = JSON.stringify(aShape) === JSON.stringify(iShape); + const shapeDifferences = differentShapePaths(aShape, iShape); + if (status === '✅' && (!semanticsEqual || !shapeEqual)) status = '⚠️'; + lines.push( + `| \`${markdownCell(id)}\` | ${markdownCell(a?.status ?? 'missing')} | ${markdownCell( + i?.status ?? 'missing' + )} | ${semanticsEqual ? 'same' : `A=${markdownCell(aSemantics)}; I=${markdownCell(iSemantics)}`} | ${ + shapeEqual ? 'same' : markdownCell(shapeDifferences) + } | ${status} |` + ); + } + const output = path.join( + outputRoot, + `comparison-${timestamp(new Date())}.md` + ); + fs.mkdirSync(outputRoot, { recursive: true }); + fs.writeFileSync(output, `${lines.join('\n')}\n`); + return { output, mismatches }; +} + +function runMain(options) { + const platform = options.platform; + if (platform !== 'android' && platform !== 'ios') { + throw new Error('--platform must be android or ios'); + } + const scriptPath = path.resolve(options.script ?? DEFAULT_SCRIPT); + if (!fs.existsSync(scriptPath)) { + throw new Error(`script not found: ${scriptPath}`); + } + const script = JSON.parse(fs.readFileSync(scriptPath, 'utf8')); + if (!Array.isArray(script.steps) || script.steps.length === 0) { + throw new Error('script has no steps'); + } + + const version = options.version ?? readPackageVersion(); + const startedAt = new Date(); + let device = resolveDevice(platform, options.device); + const runId = `${timestamp(startedAt)}-${platform}-${safeName(device)}`; + const outputRoot = path.resolve( + options['output-root'] ?? path.join(REPO_ROOT, 'build/reports', version) + ); + const reportDir = path.join(outputRoot, runId); + fs.mkdirSync(reportDir, { recursive: true }); + const eventsFile = path.join(reportDir, 'events.jsonl'); + const env = { + ...process.env, + SCRIPT_JSON: scriptPath, + OUT_LOG: eventsFile, + }; + if (options.device != null && platform === 'android') { + env.ANDROID_SERIAL = options.device; + } + if (options.device != null && platform === 'ios') { + env.DEVICE_UDID = options.device; + } + + const commit = runText('git', ['rev-parse', 'HEAD']); + const dirty = runText('git', ['status', '--porcelain']).length > 0; + let runnerExitCode = null; + let crashText = ''; + + if (options['from-log'] != null) { + fs.copyFileSync(path.resolve(options['from-log']), eventsFile); + if (options['crash-log'] != null) { + crashText = extractCrashEvidence( + fs.readFileSync(path.resolve(options['crash-log']), 'utf8') + ); + } + } else { + clearAndroidLogcat(platform, env); + const result = spawnSync('bash', ['scripts/ci/smoke_local.sh', platform], { + cwd: REPO_ROOT, + env, + stdio: 'inherit', + }); + runnerExitCode = result.status ?? 1; + if (device === 'auto') device = resolveDevice(platform, null); + crashText = captureCrashEvidence(platform, device, env); + } + + const finishedAt = new Date(); + const metadata = { + schemaVersion: 1, + runId, + startedAt: startedAt.toISOString(), + finishedAt: finishedAt.toISOString(), + platform, + device, + script: relativeToRepo(scriptPath), + sdkVersion: version, + nativeVersions: readNativeVersions(), + commit, + worktreeDirty: dirty, + scriptSha256: sha256(scriptPath), + runnerSha256: sha256(__filename), + evaluatorSha256: sha256(path.join(__dirname, 'script_results.js')), + runnerExitCode, + crashEvidence: crashText.length > 0, + sensitiveValues: 'redacted or not included', + }; + const report = writeReport({ + reportDir, + metadata, + scriptPath, + script, + eventsFile, + crashText, + }); + console.log(`Auto report: ${reportDir}`); + if (report.evaluation.failures.length > 0 || crashText.length > 0) { + process.exitCode = 1; + } +} + +function main() { + let options; + try { + options = parseArgs(process.argv.slice(2)); + if (options.help) { + console.log(usage()); + return; + } + if (options['android-report'] != null || options['ios-report'] != null) { + if (options['android-report'] == null || options['ios-report'] == null) { + throw new Error( + 'comparison requires --android-report and --ios-report' + ); + } + const version = options.version ?? readPackageVersion(); + const outputRoot = path.resolve( + options['output-root'] ?? path.join(REPO_ROOT, 'build/reports', version) + ); + const comparison = compareReports( + options['android-report'], + options['ios-report'], + outputRoot + ); + console.log(`Comparison report: ${comparison.output}`); + if (comparison.mismatches > 0) process.exitCode = 1; + return; + } + runMain(options); + } catch (error) { + console.error(`auto_report: ${error.message}`); + process.exitCode = 2; + } +} + +if (require.main === module) { + main(); +} + +module.exports = { + collectIssues, + compareReports, + differentShapePaths, + extractCrashEvidence, + parseArgs, + sanitize, + shapeOf, + writeReport, +}; diff --git a/scripts/ci/fetch_e2e_user_token.js b/scripts/ci/fetch_e2e_user_token.js new file mode 100644 index 00000000..3436cc32 --- /dev/null +++ b/scripts/ci/fetch_e2e_user_token.js @@ -0,0 +1,131 @@ +#!/usr/bin/env node +// Fetch a fresh user token for the single-account nightly CI run. User tokens +// expire (server default ~24h), so they cannot be stored as GitHub secrets; +// each test job calls this script right before generating the API_CONFIG. +// +// Flow (same as scripts/env-gettoken.js): exchange clientId/clientSecret for +// an app token, then exchange it for a user token via grant_type=inherit +// (autoCreateUser=true, so the account is created on first use). +// +// Required environment: +// E2E_REST_API REST base URL of the environment, e.g. http://host +// E2E_APP_KEY app key in orgName#appName form +// E2E_CLIENT_ID app client id +// E2E_CLIENT_SECRET app client secret +// E2E_USER_ID account user id to fetch a token for +// +// Output contract: the user token is the ONLY stdout line (callers capture it +// with $(...)); diagnostics go to stderr. Exit code is non-zero on failure. +// +// Usage: +// E2E_USER_TOKEN="$(node scripts/ci/fetch_e2e_user_token.js)" + +const MAX_ATTEMPTS = 3; +const RETRY_INTERVAL_MS = 1000; +const REQUEST_TIMEOUT_MS = 15000; + +function requireEnv(name) { + const value = process.env[name]; + if (typeof value !== 'string' || value.length === 0) { + throw new Error(`environment variable ${name} is not set`); + } + return value; +} + +function parseAppKey(appKey) { + const parts = appKey.split('#'); + if (parts.length !== 2 || parts[0] === '' || parts[1] === '') { + throw new Error(`invalid E2E_APP_KEY (expect orgName#appName): ${appKey}`); + } + return { orgName: parts[0], appName: parts[1] }; +} + +async function postJson(url, body, bearerToken) { + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (bearerToken != null) { + headers.Authorization = `Bearer ${bearerToken}`; + } + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + const text = await res.text(); + let json; + try { + json = JSON.parse(text); + } catch { + throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`); + } + if (res.status !== 200) { + throw new Error( + `HTTP ${res.status}: ${json.error ?? ''} ${json.error_description ?? ''}`.trim() + ); + } + if (typeof json.access_token !== 'string') { + throw new Error('token response missing access_token'); + } + return json.access_token; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function fetchUserTokenOnce(env) { + const { orgName, appName } = parseAppKey(env.E2E_APP_KEY); + const url = `${env.E2E_REST_API}/${orgName}/${appName}/token`; + const appToken = await postJson(url, { + grant_type: 'client_credentials', + client_id: env.E2E_CLIENT_ID, + client_secret: env.E2E_CLIENT_SECRET, + }); + return postJson( + url, + { + username: env.E2E_USER_ID, + grant_type: 'inherit', + autoCreateUser: true, + }, + appToken + ); +} + +async function main() { + const env = { + E2E_REST_API: requireEnv('E2E_REST_API'), + E2E_APP_KEY: requireEnv('E2E_APP_KEY'), + E2E_CLIENT_ID: requireEnv('E2E_CLIENT_ID'), + E2E_CLIENT_SECRET: requireEnv('E2E_CLIENT_SECRET'), + E2E_USER_ID: requireEnv('E2E_USER_ID'), + }; + let lastError; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + const token = await fetchUserTokenOnce(env); + console.error( + `fetch_e2e_user_token: got token for ${env.E2E_USER_ID} (attempt ${attempt})` + ); + process.stdout.write(token + '\n'); + return; + } catch (e) { + lastError = e; + console.error( + `fetch_e2e_user_token: attempt ${attempt}/${MAX_ATTEMPTS} failed: ${e.message}` + ); + if (attempt < MAX_ATTEMPTS) { + await sleep(RETRY_INTERVAL_MS); + } + } + } + throw lastError; +} + +main().catch((e) => { + console.error(`fetch_e2e_user_token: giving up: ${e.message}`); + process.exitCode = 1; +}); diff --git a/scripts/ci/nightly_local.sh b/scripts/ci/nightly_local.sh index fc657677..197c1696 100755 --- a/scripts/ci/nightly_local.sh +++ b/scripts/ci/nightly_local.sh @@ -3,11 +3,13 @@ # and run steps of .github/workflows/single-account-nightly.yml so a local run # exercises exactly what CI runs. # -# Required environment (same names as the GitHub secrets): -# E2E_APP_KEY E2E_USER_ID E2E_USER_PASSWORD +# Required environment (same names as the GitHub secrets; the user token is +# fetched fresh via scripts/ci/fetch_e2e_user_token.js, not passed in): +# E2E_APP_KEY E2E_USER_ID E2E_REST_API E2E_CLIENT_ID E2E_CLIENT_SECRET # # Usage: -# E2E_APP_KEY=... E2E_USER_ID=... E2E_USER_PASSWORD=... \ +# E2E_APP_KEY=... E2E_USER_ID=... E2E_REST_API=... \ +# E2E_CLIENT_ID=... E2E_CLIENT_SECRET=... \ # bash scripts/ci/nightly_local.sh android # needs a booted emulator/device # ... ios # boots a simulator if none is booted # @@ -26,7 +28,7 @@ case "$PLATFORM" in ;; esac -for var in E2E_APP_KEY E2E_USER_ID E2E_USER_PASSWORD; do +for var in E2E_APP_KEY E2E_USER_ID E2E_REST_API E2E_CLIENT_ID E2E_CLIENT_SECRET; do if [ -z "${!var:-}" ]; then echo "error: environment variable $var is not set" >&2 exit 2 @@ -35,6 +37,10 @@ done cd "$REPO_ROOT" +# User tokens expire in ~24h, so fetch a fresh one for every run (same as CI). +E2E_USER_TOKEN="$(node scripts/ci/fetch_e2e_user_token.js)" +export E2E_USER_TOKEN + mkdir -p build/reports CONFIG="$REPO_ROOT/build/reports/rn_single_account_config.json" bash scripts/ci/write_single_account_config.sh "$CONFIG" diff --git a/scripts/ci/script_results.js b/scripts/ci/script_results.js new file mode 100755 index 00000000..3a6eb7c0 --- /dev/null +++ b/scripts/ci/script_results.js @@ -0,0 +1,232 @@ +#!/usr/bin/env node + +const APITEST_PREFIX = '[APITEST] '; + +function parseLogText(text) { + const entries = []; + for (const rawLine of text.split('\n')) { + let line = rawLine.trim(); + if (line.length === 0) { + continue; + } + const marker = line.indexOf(APITEST_PREFIX); + if (marker >= 0) { + line = line.slice(marker + APITEST_PREFIX.length).trim(); + } + if (!line.startsWith('{')) { + continue; + } + try { + const entry = JSON.parse(line); + if (entry != null && typeof entry.source === 'string') { + entries.push(entry); + } + } catch { + // Ignore non-JSON or truncated device-output lines. + } + } + return entries; +} + +function indexLastEntries(entries) { + const bySource = new Map(); + for (const entry of entries) { + bySource.set(entry.source, entry); + } + return bySource; +} + +function hasOwn(value, key) { + return Object.prototype.hasOwnProperty.call(value, key); +} + +function matchExpectation(result, expectation) { + if (expectation == null || typeof expectation !== 'object') { + return { valid: false, matched: false, reason: 'missing expect object' }; + } + if (hasOwn(expectation, 'errorCode')) { + const matched = + result?.success === false && + result?.error?.code === expectation.errorCode; + return { + valid: true, + matched, + reason: matched + ? null + : `expected error code ${JSON.stringify(expectation.errorCode)}`, + }; + } + if (typeof expectation.success === 'boolean') { + const matched = result?.success === expectation.success; + return { + valid: true, + matched, + reason: matched ? null : `expected success=${expectation.success}`, + }; + } + return { + valid: false, + matched: false, + reason: 'expect must contain success or errorCode', + }; +} + +function collectStepReferences(value, references = new Set()) { + if (Array.isArray(value)) { + for (const item of value) { + collectStepReferences(item, references); + } + return references; + } + if (value != null && typeof value === 'object') { + for (const item of Object.values(value)) { + collectStepReferences(item, references); + } + return references; + } + if (typeof value === 'string') { + const match = /^\$step\.([^.]+)(?:\.|$)/.exec(value); + if (match != null) { + references.add(match[1]); + } + } + return references; +} + +function laterEntry(first, second) { + if (first == null) return second; + if (second == null) return first; + const firstSeq = Number(first.seq ?? 0); + const secondSeq = Number(second.seq ?? 0); + return secondSeq >= firstSeq ? second : first; +} + +function evaluateScript(script, entries, options = {}) { + const bySource = indexLastEntries(entries); + const steps = Array.isArray(script.steps) ? script.steps : []; + const outcomes = []; + const outcomesById = new Map(); + + for (let index = 0; index < steps.length; index++) { + const step = steps[index]; + const source = `script.step[${index}]`; + const entry = bySource.get(source); + const dependencies = Array.from(collectStepReferences(step.params)).sort(); + let status = 'not-run'; + let reason = 'missing structured step result'; + let actual = null; + + if (entry != null) { + actual = entry.payload?.result ?? null; + if (entry.payload?.api !== step.api) { + status = 'failed'; + reason = `log API mismatch: ${JSON.stringify(entry.payload?.api)}`; + } else { + const match = matchExpectation(actual, step.expect); + if (match.matched) { + status = 'passed'; + reason = null; + } else { + const failedDependency = dependencies.find( + (id) => outcomesById.get(id)?.status !== 'passed' + ); + if (failedDependency != null) { + status = 'blocked'; + reason = `dependency ${failedDependency} did not pass`; + } else { + status = 'failed'; + reason = match.reason; + } + } + } + } + + const outcome = { + index, + source, + id: step.id ?? `step-${index}`, + api: step.api, + status, + reason, + dependencies, + expected: step.expect ?? null, + actual, + }; + outcomes.push(outcome); + if (typeof step.id === 'string' && step.id.length > 0) { + outcomesById.set(step.id, outcome); + } + } + + if (options.crashEvidence === true) { + const firstMissing = outcomes.find((item) => item.status === 'not-run'); + if (firstMissing != null) { + firstMissing.status = 'crashed'; + firstMissing.reason = 'process terminated before this step returned'; + } + } + + const initEntry = bySource.get('api.ChatClient.init'); + const loginEntry = laterEntry( + bySource.get('api.ChatClient.login'), + bySource.get('api.ChatClient.loginWithToken') + ); + const doneEntry = bySource.get('script.done'); + const failures = []; + + if (initEntry == null) { + failures.push('missing log entry: api.ChatClient.init'); + } else if (initEntry.payload?.success !== true) { + failures.push( + `api.ChatClient.init did not succeed: ${JSON.stringify(initEntry.payload)}` + ); + } + if (loginEntry != null && loginEntry.payload?.success !== true) { + failures.push( + `api.ChatClient.login did not succeed: ${JSON.stringify( + loginEntry.payload + )}` + ); + } + for (const outcome of outcomes) { + if (outcome.status !== 'passed') { + failures.push( + `step[${outcome.index}] ${outcome.api}: ${outcome.status} (${outcome.reason})` + ); + } + } + if (doneEntry == null) { + failures.push('missing log entry: script.done (script did not finish?)'); + } else if (doneEntry.payload?.total !== steps.length) { + failures.push( + `script.done total mismatch: expected ${steps.length}, got ${JSON.stringify( + doneEntry.payload?.total + )}` + ); + } + + const counts = {}; + for (const outcome of outcomes) { + counts[outcome.status] = (counts[outcome.status] ?? 0) + 1; + } + + return { + entries, + bySource, + initEntry, + loginEntry, + doneEntry, + outcomes, + counts, + failures, + }; +} + +module.exports = { + APITEST_PREFIX, + collectStepReferences, + evaluateScript, + indexLastEntries, + matchExpectation, + parseLogText, +}; diff --git a/scripts/ci/test_script_results.js b/scripts/ci/test_script_results.js new file mode 100755 index 00000000..3020ef29 --- /dev/null +++ b/scripts/ci/test_script_results.js @@ -0,0 +1,232 @@ +#!/usr/bin/env node + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const { + collectStepReferences, + evaluateScript, + matchExpectation, + parseLogText, +} = require('./script_results'); +const { + collectIssues, + differentShapePaths, + extractCrashEvidence, + sanitize, + shapeOf, +} = require('./auto_report'); + +const REPO_ROOT = path.resolve(__dirname, '../..'); + +function readScript(name) { + return JSON.parse( + fs.readFileSync(path.join(REPO_ROOT, 'example/ci', name), 'utf8') + ); +} + +function entry(source, payload, seq = 1) { + return { ts: 1, seq, source, payload }; +} + +test('parseLogText accepts plain JSON and APITEST-prefixed lines', () => { + const entries = parseLogText( + [ + JSON.stringify(entry('plain', { success: true }, 1)), + `noise [APITEST] ${JSON.stringify( + entry('prefixed', { success: true }, 2) + )}`, + 'truncated {', + ].join('\n') + ); + assert.deepEqual( + entries.map((item) => item.source), + ['plain', 'prefixed'] + ); +}); + +test('matchExpectation treats expected failures as passed outcomes', () => { + assert.equal( + matchExpectation( + { success: false, error: { code: 305 } }, + { success: false } + ).matched, + true + ); + assert.equal( + matchExpectation( + { success: false, error: { code: 305 } }, + { errorCode: 305 } + ).matched, + true + ); +}); + +test('evaluateScript ignores script.done raw failure count when expectations pass', () => { + const script = { + steps: [ + { + id: 'expected_error', + api: 'ChatManager.modifyMsgBody', + expect: { errorCode: 305 }, + }, + ], + }; + const entries = [ + entry('api.ChatClient.init', { success: true }, 1), + entry( + 'script.step[0]', + { + api: 'ChatManager.modifyMsgBody', + id: 'expected_error', + result: { success: false, error: { code: 305 } }, + }, + 2 + ), + entry('script.done', { total: 1, failed: 1 }, 3), + ]; + const evaluation = evaluateScript(script, entries); + assert.deepEqual(evaluation.counts, { passed: 1 }); + assert.deepEqual(evaluation.failures, []); +}); + +test('evaluateScript classifies a dependent mismatch as blocked', () => { + const script = { + steps: [ + { id: 'producer', api: 'Producer', expect: { success: true } }, + { + id: 'consumer', + api: 'Consumer', + params: { value: '$step.producer.id' }, + expect: { success: true }, + }, + ], + }; + const entries = [ + entry('api.ChatClient.init', { success: true }, 1), + entry( + 'script.step[0]', + { api: 'Producer', result: { success: false, error: { code: 500 } } }, + 2 + ), + entry( + 'script.step[1]', + { api: 'Consumer', result: { success: false, error: { code: -1 } } }, + 3 + ), + entry('script.done', { total: 2, failed: 2 }, 4), + ]; + const evaluation = evaluateScript(script, entries); + assert.equal(evaluation.outcomes[0].status, 'failed'); + assert.equal(evaluation.outcomes[1].status, 'blocked'); +}); + +test('evaluateScript attributes crash evidence to the first missing step', () => { + const script = { + steps: [ + { id: 'crash', api: 'Crash', expect: { success: false } }, + { id: 'after', api: 'After', expect: { success: true } }, + ], + }; + const entries = [entry('api.ChatClient.init', { success: true }, 1)]; + const evaluation = evaluateScript(script, entries, { crashEvidence: true }); + assert.equal(evaluation.outcomes[0].status, 'crashed'); + assert.equal(evaluation.outcomes[1].status, 'not-run'); +}); + +test('collectStepReferences finds nested step dependencies', () => { + assert.deepEqual( + Array.from( + collectStepReferences({ a: ['$step.first.id'], b: '$config.user' }) + ), + ['first'] + ); +}); + +test('report helpers redact credential-shaped values', () => { + assert.deepEqual(sanitize({ token: 'secret', nested: { password: 'pw' } }), { + token: '', + nested: { password: '' }, + }); +}); + +test('extractCrashEvidence keeps crash context and drops unrelated lines', () => { + const crash = extractCrashEvidence( + ['unrelated', 'before', 'FATAL EXCEPTION: main', 'after'].join('\n') + ); + assert.match(crash, /FATAL EXCEPTION/); + assert.match(crash, /after/); +}); + +test('shapeOf compares response structure without dynamic values', () => { + assert.deepEqual(shapeOf({ id: 'dynamic', values: [{ count: 1 }] }), { + id: 'string', + values: [{ count: 'number' }], + }); +}); + +test('differentShapePaths reports concise field-level differences', () => { + assert.deepEqual( + differentShapePaths( + { data: { cursor: 'string', list: [] } }, + { data: { cursor: 'string', list: [], totalCount: 'number' } } + ), + ['data.totalCount'] + ); +}); + +test('5.0.0 positive and negative scripts keep pairable APIs separated', () => { + const positive = readScript('port_5_0_0_positive.json'); + const negative = readScript('port_5_0_0_negative.json'); + const positiveApis = new Set(positive.steps.map((step) => step.api)); + const negativeApis = new Set(negative.steps.map((step) => step.api)); + const pairableApis = [ + 'ChatGroupManager.createGroupEx', + 'ChatGroupManager.updateGroupConfigs', + 'ChatManager.modifyMsgBody', + 'ChatManager.clearConversationUnreadMessageCount', + 'ChatManager.sendMessageReadReceipts', + 'ChatManager.getGroupMessageReadReceipts', + 'ChatClient.getLoggedInDevicesFromServer', + 'ChatClient.renewToken', + 'ChatClient.kickDevice', + 'ChatClient.kickAllDevices', + ]; + + assert.ok( + positive.steps.every((step) => step.expect?.success === true), + 'positive-path steps must all expect success' + ); + assert.ok( + negative.steps.every( + (step) => + step.expect?.success === false || + typeof step.expect?.errorCode === 'number' + ), + 'negative-path steps must all expect an error' + ); + for (const api of pairableApis) { + assert.ok(positiveApis.has(api), `positive path missing ${api}`); + assert.ok(negativeApis.has(api), `negative path missing ${api}`); + } + assert.ok(positiveApis.has('ChatManager.fetchGroupMessageReadReceipts')); + assert.ok(!negativeApis.has('ChatManager.fetchGroupMessageReadReceipts')); +}); + +test('negative report records the intentionally disabled native crash case', () => { + const scriptName = 'port_5_0_0_negative.json'; + const script = readScript(scriptName); + const issues = collectIssues( + path.join(REPO_ROOT, 'example/ci', scriptName), + 'android', + script, + { outcomes: [], bySource: new Map() }, + '' + ); + assert.ok( + issues.some( + (item) => item.key === 'fetch-group-receipt-missing-disabled' + ) + ); +}); diff --git a/scripts/ci/write_single_account_config.sh b/scripts/ci/write_single_account_config.sh index e80e238f..1892ba60 100755 --- a/scripts/ci/write_single_account_config.sh +++ b/scripts/ci/write_single_account_config.sh @@ -2,17 +2,19 @@ # Generate the single-account auto-mode config (API_CONFIG) from the E2E_* # environment variables. The config carries the appKey and the account that # example/ci/single_account.json references via `$config.*`; the output -# contains real credentials, so it is written with mode 0600 and must be +# contains a real user token, so it is written with mode 0600 and must be # deleted after the run (the workflow and nightly_local.sh both do this). # Never commit the generated file. # # Required environment: -# E2E_APP_KEY app key used for ChatClient.init -# E2E_USER_ID account user id used for login -# E2E_USER_PASSWORD account password used for login +# E2E_APP_KEY app key used for ChatClient.init +# E2E_USER_ID account user id used for login +# E2E_USER_TOKEN fresh user token for login (password login is gone in 5.0; +# fetch one via scripts/ci/fetch_e2e_user_token.js — tokens +# expire in ~24h, so they cannot be stored as CI secrets) # # Usage: -# E2E_APP_KEY=... E2E_USER_ID=... E2E_USER_PASSWORD=... \ +# E2E_APP_KEY=... E2E_USER_ID=... E2E_USER_TOKEN=... \ # bash scripts/ci/write_single_account_config.sh set -euo pipefail @@ -23,7 +25,7 @@ OUT="${1:-}" exit 2 } -for var in E2E_APP_KEY E2E_USER_ID E2E_USER_PASSWORD; do +for var in E2E_APP_KEY E2E_USER_ID E2E_USER_TOKEN; do if [ -z "${!var:-}" ]; then echo "error: environment variable $var is not set" >&2 exit 2 @@ -32,12 +34,14 @@ done # Shape mirrors example/src/env.ts (appKey as array, accounts as [{id, mm}]), # which auto_mode.ts derives the init/login params from when the script has no -# explicit init/login sections. +# explicit init/login sections. The token rides in the top-level `loginToken` +# key (see deriveLoginParams in auto_mode.ts), not in accounts[].mm. OUT="$OUT" node -e ' const fs = require("fs"); const config = { appKey: [process.env.E2E_APP_KEY], - accounts: [{ id: process.env.E2E_USER_ID, mm: process.env.E2E_USER_PASSWORD }], + accounts: [{ id: process.env.E2E_USER_ID }], + loginToken: process.env.E2E_USER_TOKEN, }; fs.writeFileSync(process.env.OUT, JSON.stringify(config, null, 2) + "\n", { mode: 0o600, diff --git a/scripts/env-gettoken.js b/scripts/env-gettoken.js new file mode 100644 index 00000000..d84a50a4 --- /dev/null +++ b/scripts/env-gettoken.js @@ -0,0 +1,198 @@ +#!/usr/bin/env node + +/** + * 用法: yarn env:gettoken(零参数) + * + * 本项目只配置「当前这一个环境」:example/config.local.json 描述它, + * 换环境=直接改/换这个文件。需要在多套环境间切换时,可自行在 Git 外 + * 维护 config.ngi.json / config.ebs.json 这类副本(不入库,敏感信息由 + * 使用者人工管理),用哪套就把它复制为 config.local.json。 + * + * - example/config.local.json 不存在 → 写入占位模板并退出。 + * 该文件被 git 忽略,手工填入真实配置后重跑本脚本。 + * - 存在 → 用 clientId/clientSecret 取 App Token,再逐个账号用 + * grant_type=inherit 取 User Token(autoCreateUser=true),最后把 + * 「配置 + token」整体写成 example/src/env.ts。 + * 脚本无状态:不落任何中间缓存,每次全量重取、整文件重写。 + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const projectRoot = path.resolve(__dirname, '..'); +const configFile = path.join(projectRoot, 'example', 'config.local.json'); +const envFile = path.join(projectRoot, 'example', 'src', 'env.ts'); + +const CONFIG_TEMPLATE = { + _comment: + '本文件被 git 忽略,存放当前这一个环境的敏感配置与测试数据,修改后运行 yarn env:gettoken 重新生成 env.ts。换环境=直接改/换本文件;如需多套环境,可自行在 Git 外维护 config.ngi.json / config.ebs.json 这类副本(不入库,敏感信息人工管理)。可选字段: tokenTtl(user token 有效期秒数,缺省由服务端决定)、pushInfo({sendId, keyId})。chatOptions 对应 SDK 的 ChatOptions,平铺为 env.ts 顶层导出(键见 example/src/auto/auto_mode.ts 的 CHAT_OPTIONS_KEYS)。enablePrivateConfig 为私有化部署开关,缺省 false(公有环境,webSocketServer/restServer/msyncServer 不生成);置 true 时三个私有化参数生成进 env.ts(TODO/空值跳过)。', + restApi: 'TODO', + appKey: 'TODO(orgName#appName)', + clientId: 'TODO', + clientSecret: 'TODO', + accounts: [{ id: 'TODO-user1' }, { id: 'TODO-user2' }], + groups: [{ id: 'TODO', owner: 'TODO-user1', name: 'TODO' }], + chatOptions: {}, + enablePrivateConfig: false, + webSocketServer: 'TODO', + restServer: 'TODO', + msyncServer: 'TODO', +}; + +function isPlaceholder(v) { + return typeof v !== 'string' || v.length === 0 || v.startsWith('TODO'); +} + +async function postJson(url, body, bearerToken) { + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (bearerToken != null) { + headers.Authorization = `Bearer ${bearerToken}`; + } + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); + const text = await res.text(); + let json; + try { + json = JSON.parse(text); + } catch { + throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`); + } + if (res.status !== 200) { + throw new Error( + `HTTP ${res.status}: ${json.error ?? ''} ${json.error_description ?? ''}`.trim() + ); + } + return json; +} + +function parseAppKey(appKey) { + const parts = String(appKey).split('#'); + if (parts.length !== 2 || parts[0] === '' || parts[1] === '') { + throw new Error(`invalid appKey (expect orgName#appName): ${appKey}`); + } + return { orgName: parts[0], appName: parts[1] }; +} + +async function fetchAppToken(config) { + const { orgName, appName } = parseAppKey(config.appKey); + const url = `${config.restApi}/${orgName}/${appName}/token`; + const json = await postJson(url, { + grant_type: 'client_credentials', + client_id: config.clientId, + client_secret: config.clientSecret, + }); + if (typeof json.access_token !== 'string') { + throw new Error('app token response missing access_token'); + } + return json.access_token; +} + +async function fetchUserToken(config, appToken, userId, ttl) { + const { orgName, appName } = parseAppKey(config.appKey); + const url = `${config.restApi}/${orgName}/${appName}/token`; + const body = { username: userId, grant_type: 'inherit', autoCreateUser: true }; + if (typeof ttl === 'number') { + body.ttl = ttl; + } + const json = await postJson(url, body, appToken); + if (typeof json.access_token !== 'string') { + throw new Error('user token response missing access_token'); + } + return json.access_token; +} + +function renderEnvTs(config, accounts) { + const v = (value) => JSON.stringify(value); + const generatedAt = new Date().toISOString(); + const groups = (config.groups ?? []).map((g) => { + const { cluster: _cluster, ...rest } = g; + return rest; + }); + const pushInfo = config.pushInfo ?? {}; + const lines = [ + `// Generated by scripts/env-gettoken.js from example/config.local.json`, + `// generated: ${generatedAt}`, + `// DO NOT EDIT — 手改请改 example/config.local.json 后重跑 yarn env:gettoken`, + `export const test = true;`, + `export const appKey = [${v(config.appKey ?? '')}];`, + `export const appId = [${v(config.appId ?? '')}];`, + `export const PushInfo = { sendId: ${v(pushInfo.sendId)}, KeyId: ${v( + pushInfo.keyId + )} };`, + `export const accounts = ${v(accounts)};`, + `export const groups = ${v(groups)};`, + ]; + // chatOptions 对应 SDK 的 ChatOptions,键值平铺为顶层导出, + // auto 模式的 CHAT_OPTIONS_KEYS 可直接消费。 + for (const [key, value] of Object.entries(config.chatOptions ?? {})) { + lines.push(`export const ${key} = ${v(value)};`); + } + // 私有化部署配置: enablePrivateConfig 为公私切换开关,缺省 false(公有环境, + // 三个参数不生成);置 true 时 webSocketServer/restServer/msyncServer 生成进 + // env.ts(TODO 占位或空值跳过)。 + if (config.enablePrivateConfig === true) { + for (const key of ['webSocketServer', 'restServer', 'msyncServer']) { + if (!isPlaceholder(config[key])) { + lines.push(`export const ${key} = ${v(config[key])};`); + } + } + } + return lines.join('\n') + '\n'; +} + +async function main() { + if (fs.existsSync(configFile) === false) { + fs.writeFileSync( + configFile, + JSON.stringify(CONFIG_TEMPLATE, null, 2) + '\n', + 'utf-8' + ); + console.log(`${configFile} created.`); + console.log( + '请填写该环境的 restApi/appKey/clientId/clientSecret 和 accounts 后重跑 yarn env:gettoken' + ); + return; + } + + const config = JSON.parse(fs.readFileSync(configFile, 'utf-8')); + if (config.clusters != null || config.defaultCluster != null) { + throw new Error( + 'example/config.local.json 仍是已移除的多集群格式(clusters/defaultCluster);' + + '请把当前环境的 restApi/appKey/clientId/clientSecret 移到顶层后重跑 yarn env:gettoken' + ); + } + for (const key of ['restApi', 'appKey', 'clientId', 'clientSecret']) { + if (isPlaceholder(config[key])) { + throw new Error(`example/config.local.json 缺少必填字段: ${key}`); + } + } + + // accounts/groups 条目里遗留的 cluster 键静默丢弃(兼容从旧格式粘贴的配置)。 + const accountIds = (config.accounts ?? []).map((a) => a.id); + if (accountIds.some((id) => isPlaceholder(id))) { + throw new Error('example/config.local.json 的 accounts 存在未填写的 id'); + } + + console.log(`获取 app token,共 ${accountIds.length} 个账号`); + const appToken = await fetchAppToken(config); + const accounts = []; + for (const id of accountIds) { + const token = await fetchUserToken(config, appToken, id, config.tokenTtl); + accounts.push({ id, mm: token }); + console.log(`${id} ok`); + } + + fs.writeFileSync(envFile, renderEnvTs(config, accounts), 'utf-8'); + console.log(`${envFile} generated`); +} + +main().catch((e) => { + console.error(e.message ?? e); + process.exitCode = 1; +}); diff --git a/src/ChatClient.ts b/src/ChatClient.ts index d27efff7..0ab58607 100644 --- a/src/ChatClient.ts +++ b/src/ChatClient.ts @@ -7,10 +7,11 @@ import { Factory } from './__internal__/Factory'; import { BaseManager } from './__internal__/Base'; import { + MTbindDeviceToken, + MTbindPushKitToken, MTchangeAppId, MTchangeAppKey, MTcompressLogs, - MTcreateAccount, MTgetCurrentUser, MTgetLoggedInDevicesFromServer, MTgetRTCTokenInfoWithChannelName, @@ -18,15 +19,15 @@ import { MTgetUserIdsWithRTCUids, MTinit, MTisConnected, - MTisLoggedInBefore, MTkickAllDevices, MTkickDevice, MTlogin, - MTloginWithAgoraToken, MTlogout, - MTonAppActiveNumberReachLimit, MTonConnected, MTonCustomEvent, + MTonDatabaseOpened, + MTonDataSyncFinish, + MTonDataSyncStart, MTonDisconnected, MTonMultiDeviceEvent, MTonMultiDeviceEventContact, @@ -38,18 +39,8 @@ import { MTonOfflineMessageSyncStart, MTonTokenDidExpire, MTonTokenWillExpire, - MTonUserAuthenticationFailed, - MTonUserDidChangePassword, - MTonUserDidForbidByServer, - MTonUserDidLoginFromOtherDevice, - MTonUserDidLoginFromOtherDeviceWithInfo, - MTonUserDidLoginTooManyDevice, - MTonUserDidRemoveFromServer, - MTonUserKickedByOtherDevice, MTrenewToken, - MTbindPushKitToken, MTunbindPushKitToken, - MTupdatePushConfig, } from './__internal__/Consts'; import { ExceptionHandler } from './__internal__/ErrorHandler'; import { ChatContactManager } from './ChatContactManager'; @@ -68,8 +59,7 @@ import { ChatRoomManager } from './ChatRoomManager'; import { ChatUserInfoManager } from './ChatUserInfoManager'; import { chatlog } from './common/ChatConst'; import { ChatDeviceInfo } from './common/ChatDeviceInfo'; -import { ChatOptions } from './common/ChatOptions'; -import { ChatPushConfig } from './common/ChatPushConfig'; +import { ChatOptions, type ChatDataSyncType } from './common/ChatOptions'; import { eventEmitter } from './__specs__'; import { Native } from './__internal__/Native'; import { ChatError } from './common/ChatError'; @@ -195,69 +185,6 @@ export class ChatClient extends BaseManager { event.addListener(MTonCustomEvent, this.onCustomEvent.bind(this)) ); - this._connectionSubscriptions.set( - MTonUserDidLoginFromOtherDevice, - event.addListener( - MTonUserDidLoginFromOtherDevice, - this.onUserDidLoginFromOtherDevice.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserDidLoginFromOtherDeviceWithInfo, - event.addListener( - MTonUserDidLoginFromOtherDeviceWithInfo, - this.onUserDidLoginFromOtherDeviceWithInfo.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserDidRemoveFromServer, - event.addListener( - MTonUserDidRemoveFromServer, - this.onUserDidRemoveFromServer.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserDidForbidByServer, - event.addListener( - MTonUserDidForbidByServer, - this.onUserDidForbidByServer.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserDidChangePassword, - event.addListener( - MTonUserDidChangePassword, - this.onUserDidChangePassword.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserDidLoginTooManyDevice, - event.addListener( - MTonUserDidLoginTooManyDevice, - this.onUserDidLoginTooManyDevice.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserKickedByOtherDevice, - event.addListener( - MTonUserKickedByOtherDevice, - this.onUserKickedByOtherDevice.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonUserAuthenticationFailed, - event.addListener( - MTonUserAuthenticationFailed, - this.onUserAuthenticationFailed.bind(this) - ) - ); - this._connectionSubscriptions.set( - MTonAppActiveNumberReachLimit, - event.addListener( - MTonAppActiveNumberReachLimit, - this.onAppActiveNumberReachLimit.bind(this) - ) - ); this._connectionSubscriptions.set( MTonOfflineMessageSyncStart, event.addListener( @@ -272,6 +199,18 @@ export class ChatClient extends BaseManager { this.onOfflineMessageSyncFinish.bind(this) ) ); + this._connectionSubscriptions.set( + MTonDataSyncStart, + event.addListener(MTonDataSyncStart, this.onDataSyncStart.bind(this)) + ); + this._connectionSubscriptions.set( + MTonDataSyncFinish, + event.addListener(MTonDataSyncFinish, this.onDataSyncFinish.bind(this)) + ); + this._connectionSubscriptions.set( + MTonDatabaseOpened, + event.addListener(MTonDatabaseOpened, this.onDatabaseOpened.bind(this)) + ); } private onConnected(): void { @@ -289,9 +228,16 @@ export class ChatClient extends BaseManager { } private onDisconnected(params?: any): void { chatlog.log(`${ChatClient.TAG}: onDisconnected: `, params); + const errorCode = params?.errorCode as number | undefined; + const info = + params?.deviceName !== undefined || params?.ext !== undefined + ? { + deviceName: params?.deviceName as string | undefined, + ext: params?.ext as string | undefined, + } + : undefined; this._connectionListeners.forEach((element) => { - // let ec = params?.errorCode as number; - element.onDisconnected?.(); + element.onDisconnected?.(errorCode, info); }); } private onTokenWillExpire(params?: any): void { @@ -354,71 +300,40 @@ export class ChatClient extends BaseManager { element.onDataReceived(params); }); } - private onUserDidLoginFromOtherDevice(params: any): void { - chatlog.log(`${ChatClient.TAG}: onUserDidLoginFromOtherDevice: `); - this._connectionListeners.forEach((element) => { - const deviceName = params.deviceName as string | undefined; - element.onUserDidLoginFromOtherDevice?.(deviceName); - }); - } - private onUserDidLoginFromOtherDeviceWithInfo(params: any): void { - chatlog.log(`${ChatClient.TAG}: onUserDidLoginFromOtherDeviceWithInfo: `); - this._connectionListeners.forEach((element) => { - element.onUserDidLoginFromOtherDeviceWithInfo?.(params); - }); - } - private onUserDidRemoveFromServer(): void { - chatlog.log(`${ChatClient.TAG}: onUserDidRemoveFromServer: `); - this._connectionListeners.forEach((element) => { - element.onUserDidRemoveFromServer?.(); - }); - } - private onUserDidForbidByServer(): void { - chatlog.log(`${ChatClient.TAG}: onUserDidForbidByServer: `); - this._connectionListeners.forEach((element) => { - element.onUserDidForbidByServer?.(); - }); - } - private onUserDidChangePassword(): void { - chatlog.log(`${ChatClient.TAG}: onUserDidChangePassword: `); - this._connectionListeners.forEach((element) => { - element.onUserDidChangePassword?.(); - }); - } - private onUserDidLoginTooManyDevice(): void { - chatlog.log(`${ChatClient.TAG}: onUserDidLoginTooManyDevice: `); - this._connectionListeners.forEach((element) => { - element.onUserDidLoginTooManyDevice?.(); - }); - } - private onUserKickedByOtherDevice(): void { - chatlog.log(`${ChatClient.TAG}: onUserKickedByOtherDevice: `); + private onOfflineMessageSyncStart(): void { + chatlog.log(`${ChatClient.TAG}: onOfflineMessageSyncStart: `); this._connectionListeners.forEach((element) => { - element.onUserKickedByOtherDevice?.(); + element.onOfflineMessageSyncStart?.(); }); } - private onUserAuthenticationFailed(): void { - chatlog.log(`${ChatClient.TAG}: onUserAuthenticationFailed: `); + private onOfflineMessageSyncFinish(): void { + chatlog.log(`${ChatClient.TAG}: onOfflineMessageSyncFinish: `); this._connectionListeners.forEach((element) => { - element.onUserAuthenticationFailed?.(); + element.onOfflineMessageSyncFinish?.(); }); } - private onAppActiveNumberReachLimit(): void { - chatlog.log(`${ChatClient.TAG}: onAppActiveNumberReachLimit: `); + private onDataSyncStart(params: any): void { + chatlog.log(`${ChatClient.TAG}: onDataSyncStart: `, params); this._connectionListeners.forEach((element) => { - element.onAppActiveNumberReachLimit?.(); + element.onDataSyncStart?.(params.type as ChatDataSyncType); }); } - private onOfflineMessageSyncStart(): void { - chatlog.log(`${ChatClient.TAG}: onOfflineMessageSyncStart: `); + private onDataSyncFinish(params: any): void { + chatlog.log(`${ChatClient.TAG}: onDataSyncFinish: `, params); this._connectionListeners.forEach((element) => { - element.onOfflineMessageSyncStart?.(); + element.onDataSyncFinish?.( + params.type as ChatDataSyncType, + params.errorCode as number + ); }); } - private onOfflineMessageSyncFinish(): void { - chatlog.log(`${ChatClient.TAG}: onOfflineMessageSyncFinish: `); + private onDatabaseOpened(params: any): void { + chatlog.log(`${ChatClient.TAG}: onDatabaseOpened: `, params); this._connectionListeners.forEach((element) => { - element.onOfflineMessageSyncFinish?.(); + element.onDatabaseOpened?.( + params.username as string, + params.errorCode as number + ); }); } @@ -536,27 +451,6 @@ export class ChatClient extends BaseManager { return this._currentUsername; } - /** - * 检查当前用户是否登录。 - * - * **注意** - * - * 在初始化之后和登录之前调用该方法。 - * - * @returns 当前用户是否登录。 - * - `true`:已登录。自动登录时,成功登录前返回 `true`,其他情况下返回 `false`。 - * - `false`:未登录。非自动登录时,返回 `false`。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async isLoginBefore(): Promise { - chatlog.log(`${ChatClient.TAG}: isLoginBefore: `); - let r: any = await Native._callMethod(MTisLoggedInBefore); - ChatClient.checkErrorFromResult(r); - let _isLoginBefore = r?.[MTisLoggedInBefore] as boolean; - return _isLoginBefore; - } - /** * 获取登录 token。 * @@ -572,88 +466,6 @@ export class ChatClient extends BaseManager { return _token; } - /** - * 注册新用户(开放注册)。 - * - * **注意** - * - * 注册新用户有两种方式: - * - * - 开放注册:客户端直接注册新用户,不建议在正式环境使用。 - * 如调用失败可联系商务申请使用该接口。 - * - * - 授权注册:可调用 REST API 注册新用户,然后保存到服务器或者发送到客户端使用。 - * - * @param userId 用户 ID。 - * 该参数必填。用户 ID 不能超过 64 个字符,支持以下类型的字符: - * - 26 个小写英文字母 a-z - * - 26 个大写英文字母 A-Z - * - 10 个数字 0-9 - * - "_", "-", "." - * - * 用户 ID 不区分大小写,大写字母会自动转换为小写字母。 - * - * 用户的电子邮件地址和 UUID 不能作为用户 ID。 - * - * 可通过以下格式的正则表达式设置用户 ID:^[a-zA-Z0-9_-]+$。 - * - * @param password 密码,长度不超过 64 个字符。该参数必填。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async createAccount(userId: string, password: string): Promise { - chatlog.log(`${ChatClient.TAG}: createAccount: `, userId, '******'); - let r: any = await Native._callMethod(MTcreateAccount, { - [MTcreateAccount]: { - username: userId, - password: password, - }, - }); - ChatClient.checkErrorFromResult(r); - } - - /** - * 通过密码或环信 token 登录 Chat 服务器。 - * - * **注意** - * - * 如果利用环信 token 登录,可通过以下方式获取 token: - * - SDK 接口:详见 {@link createAccount} 或 {@link getAccessToken}。 - * - 环信 console 后台:{@url https://console.easemob.com/app/applicationOverview/userManagement}。 - * - * Token 过期提醒通过 {@link ChatConnectEventListener.onTokenWillExpire} 和 {@link ChatConnectEventListener.onTokenDidExpire} 通知。 - * - * @param userId 用户 ID。详见 {@link createAccount}。 - * @param pwdOrToken 密码或环信 token,详见 {@link createAccount} 或者 {@link getAccessToken}。 - * @param isPassword 是否通过 密码 登录。 - * - (默认)`true`: 通过 密码 登录。 - * - `false`: 通过 token 登录。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - * - * @deprecated 请用 {@link loginWithToken} 替代. - */ - public async login( - userId: string, - pwdOrToken: string, - isPassword: boolean = true - ): Promise { - chatlog.log(`${ChatClient.TAG}: login: `, userId, '******', isPassword); - let r: any = await Native._callMethod(MTlogin, { - [MTlogin]: { - username: userId, - pwdOrToken: pwdOrToken, - isPassword: isPassword, - }, - }); - ChatClient.checkErrorFromResult(r); - const rr = r?.[MTlogin]; - if (rr && rr.username) { - this._currentUsername = rr.username; - chatlog.log(`${ChatClient.TAG}: login: ${rr?.username}, ${rr?.token}`); - } - } - /** * 通过用户ID和token登录。 * @@ -663,7 +475,7 @@ export class ChatClient extends BaseManager { * @param userId 用户ID. * @param token 用户token. * - * @throws A description of the exception. See {@link ChatError}. + * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ public async loginWithToken(userId: string, token: string): Promise { chatlog.log(`${ChatClient.TAG}: loginWithToken: `, userId, '******', false); @@ -681,55 +493,23 @@ export class ChatClient extends BaseManager { chatlog.log(`${ChatClient.TAG}: login: ${rr?.username}, ${rr?.token}`); } } - /* - * @deprecated 2023-11-17 使用 {@link login} 代替。 - * - * 使用用户 ID 和声网 token 登录。 - * - * **注意** - * - * 该方法支持自动登录。 - * - * @param userId 用户 ID,详见 {@link createAccount}。 - * @param agoraToken 声网 token。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async loginWithAgoraToken( - userId: string, - agoraToken: string - ): Promise { - chatlog.log(`${ChatClient.TAG}: loginWithAgoraToken: `, userId, '******'); - let r: any = await Native._callMethod(MTloginWithAgoraToken, { - [MTloginWithAgoraToken]: { - username: userId, - agoratoken: agoraToken, - }, - }); - ChatClient.checkErrorFromResult(r); - const rr = r?.[MTloginWithAgoraToken]; - if (rr && rr.username) { - this._currentUsername = rr.username; - chatlog.log(`${ChatClient.TAG}: loginA: ${rr?.username}, ${rr?.token}`); - } - } /** - * 更新声网 token。 + * 更新 token。 * * **注意** * - * 当用户利用声网 token 登录的情况下在 {@link ChatConnectEventListener} 实现类中收到 token 即将过期事件的回调通知时,可以调用该方法更新 token,避免因 token 失效产生的未知问题。 + * 使用 token 登录时,如果通过 {@link ChatConnectEventListener} 回调收到 token 即将过期的通知,可以调用该方法更新 token,避免 token 失效导致异常。 * - * @param agoraToken 新的声网 token。 + * @param token 新 token。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async renewAgoraToken(agoraToken: string): Promise { - chatlog.log(`${ChatClient.TAG}: renewAgoraToken: `, '******'); + public async renewToken(token: string): Promise { + chatlog.log(`${ChatClient.TAG}: renewToken: `, '******'); let r: any = await Native._callMethod(MTrenewToken, { [MTrenewToken]: { - agora_token: agoraToken, + token: token, }, }); ChatClient.checkErrorFromResult(r); @@ -790,11 +570,11 @@ export class ChatClient extends BaseManager { /** * 更新 app iD。 * - * **Note** + * **注意** * * - 需要不登录的状态下使用,如果登录请使用 {@link logout} 退出. * - * @param 新的 app ID。 + * @param newAppId 新的 App ID。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ @@ -833,15 +613,14 @@ export class ChatClient extends BaseManager { * 获取指定账号下登录的在线设备列表。 * * @param userId 用户 ID。 - * @param password 密码。 + * @param token 用户 token,详见 {@link getAccessToken}。 * @returns 登录的在线设备列表。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ public async getLoggedInDevicesFromServer( userId: string, - pwdOrToken: string, - isPassword?: boolean + token: string ): Promise> { chatlog.log( `${ChatClient.TAG}: getLoggedInDevicesFromServer: `, @@ -851,8 +630,7 @@ export class ChatClient extends BaseManager { let r: any = await Native._callMethod(MTgetLoggedInDevicesFromServer, { [MTgetLoggedInDevicesFromServer]: { username: userId, - password: pwdOrToken, - isPassword: isPassword ?? true, + token: token, }, }); ChatClient.checkErrorFromResult(r); @@ -872,27 +650,22 @@ export class ChatClient extends BaseManager { * 关于如何获取设备 ID,详见 {@link ChatDeviceInfo#resource}。 * * @param userId 用户 ID。 - * @param pwdOrToken 密码或 token。 + * @param token 用户 token,详见 {@link getAccessToken}。 * @param resource 设备 ID,详见 {@link ChatDeviceInfo#resource}。 - * @param isPassword 是否使用密码登录设备: - * - (默认)`true`:使用密码; - * - `false`:使用用户 token。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ public async kickDevice( userId: string, - pwdOrToken: string, - resource: string, - isPassword?: boolean + token: string, + resource: string ): Promise { chatlog.log(`${ChatClient.TAG}: kickDevice: `, userId, '******', resource); let r: any = await Native._callMethod(MTkickDevice, { [MTkickDevice]: { username: userId, - password: pwdOrToken, + token: token, resource: resource, - isPassword: isPassword ?? true, }, }); ChatClient.checkErrorFromResult(r); @@ -902,58 +675,49 @@ export class ChatClient extends BaseManager { * 将指定账号登录的所有设备都踢下线。 * * @param userId 用户 ID。 - * @param password 密码。 + * @param token 用户 token,详见 {@link getAccessToken}。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async kickAllDevices( - userId: string, - pwdOrToken: string, - isPassword?: boolean - ): Promise { + public async kickAllDevices(userId: string, token: string): Promise { chatlog.log(`${ChatClient.TAG}: kickAllDevices: `, userId, '******'); let r: any = await Native._callMethod(MTkickAllDevices, { [MTkickAllDevices]: { username: userId, - password: pwdOrToken, - isPassword: isPassword ?? true, + token: token, }, }); ChatClient.checkErrorFromResult(r); } /** - * 更新推送设置。 - * - * **注意** - * 对于 iOS 设备需在初始化 SDK 时录入设备 ID,否则设置不会成功,详见 {@link ChatClient#init}。 + * 绑定设备的推送 token。 * - * @param config 推送设置,详见 {@link ChatPushConfig}。 + * @param params 参数。 + * - `deviceToken`:平台推送服务上报的推送 token。 + * - `notifierName`:平台的推送证书名称。Android 平台必填,iOS 平台忽略该参数。 + * - Android:厂商推送证书名称,例如 FCM Sender ID、华为/荣耀 App ID、小米/魅族 App ID、OPPO App Key 或 vivo `appId#appKey`。该参数不能为空,否则原生 SDK 会返回参数无效错误。 + * - iOS:不使用该参数。请在 SDK 初始化时通过 {@link ChatOptions.apnsCertName} 设置 APNs 证书名称,因为 App 运行期间无法修改该证书名称。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async updatePushConfig(config: ChatPushConfig): Promise { + public async bindDeviceToken(params: { + deviceToken: string; + notifierName?: string; + }): Promise { chatlog.log( - `${ChatClient.TAG}: updatePushConfig: ${JSON.stringify(config)}` + `${ChatClient.TAG}: ${this.bindDeviceToken.name}: `, + params.notifierName ); - if (this._options) { - const newPushConfig = new ChatPushConfig(this._options.pushConfig); // deep copy - if (config.deviceId) { - newPushConfig.deviceId = config.deviceId; - } - if (config.deviceToken) { - newPushConfig.deviceToken = config.deviceToken; - } - const newOptions = { ...this._options }; - newOptions.pushConfig = { ...newPushConfig }; - this._options = newOptions; - } - let r: any = await Native._callMethod(MTupdatePushConfig, { - [MTupdatePushConfig]: { - config: config, + let r: any = await Native._callMethod(MTbindDeviceToken, { + [MTbindDeviceToken]: { + // Keep the key on every platform: iOS ignores it, and Android reports the + // native invalid-parameter error instead of a missing-key error when it is empty. + notifierName: params.notifierName ?? '', + deviceToken: params.deviceToken, }, }); - ChatPushManager.checkErrorFromResult(r); + ChatClient.checkErrorFromResult(r); } /** diff --git a/src/ChatContactManager.ts b/src/ChatContactManager.ts index ba2148bf..7d43ec2d 100644 --- a/src/ChatContactManager.ts +++ b/src/ChatContactManager.ts @@ -7,11 +7,8 @@ import { MTaddUserToBlockList, MTdeclineInvitation, MTdeleteContact, - MTfetchAllContacts, - MTfetchContacts, MTgetAllContacts, MTgetAllContactsFromDB, - MTgetAllContactsFromServer, MTgetBlockListFromDB, MTgetBlockListFromServer, MTgetContact, @@ -25,8 +22,7 @@ import { Native } from './__internal__/Native'; import type { ChatContactEventListener } from './ChatEvents'; import { chatlog } from './common/ChatConst'; import { ChatContact } from './common/ChatContact'; -import { ChatCursorResult } from './common/ChatCursorResult'; -import { ChatError, ChatException } from './common/ChatError'; +import { ChatException } from './common/ChatError'; /** * 联系人管理类,用于添加、查询和删除联系人。 @@ -75,14 +71,6 @@ export class ChatContactManager extends BaseManager { case 'onFriendRequestDeclined': listener.onFriendRequestDeclined?.(params.username); break; - case 'onContactSyncStart': - listener.onContactSyncStart?.(); - break; - case 'onContactSyncFinish': - listener.onContactSyncFinish?.( - params.error ? new ChatError(params.error) : undefined - ); - break; case 'onContactInfoUpdate': listener.onContactInfoUpdate?.(new ChatContact(params.contact)); break; @@ -170,21 +158,6 @@ export class ChatContactManager extends BaseManager { ChatContactManager.checkErrorFromResult(r); } - /** - * 从服务器获取联系人列表。 - * - * @returns 联系人列表。 - * - * @throws 如果有方法调用的异常会在这里抛出,可以看到具体错误原因。参见 {@link ChatError}。 - */ - public async getAllContactsFromServer(): Promise> { - chatlog.log(`${ChatContactManager.TAG}: getAllContactsFromServer: `); - let r: any = await Native._callMethod(MTgetAllContactsFromServer); - ChatContactManager.checkErrorFromResult(r); - const ret: string[] = r?.[MTgetAllContactsFromServer]; - return ret; - } - /** * 从本地数据库获取联系人列表。 * @@ -353,60 +326,6 @@ export class ChatContactManager extends BaseManager { return undefined; } - /** - * 从服务器获取所有联系人。 - * - * @returns 联系人列表。 - * - * @throws 如果有方法调用的异常会在这里抛出,可以看到具体错误原因。参见 {@link ChatError}。 - */ - public async fetchAllContacts(): Promise { - chatlog.log(`${ChatContactManager.TAG}: fetchAllContacts: `); - let r: any = await Native._callMethod(MTfetchAllContacts); - ChatContactManager.checkErrorFromResult(r); - const list: any[] = r?.[MTfetchAllContacts]; - const ret: ChatContact[] = []; - for (const i of list) { - ret.push(new ChatContact(i)); - } - return ret; - } - - /** - * 从服务器分页获取联系人 - * @params params - - * - cursor: 分页游标,默认为空,可以请求第一页。 - * - pageSize: 每页最大数目。默认20。 [1-50] - * @returns 联系人列表。 - * - * @throws 如果有方法调用的异常会在这里抛出,可以看到具体错误原因。参见 {@link ChatError}。 - */ - public async fetchContacts(params: { - cursor?: string; - pageSize?: number; - }): Promise> { - chatlog.log( - `${ChatContactManager.TAG}: fetchContacts: ${params.cursor}, ${params.pageSize}` - ); - let r: any = await Native._callMethod(MTfetchContacts, { - [MTfetchContacts]: { - cursor: params.cursor, - pageSize: params.pageSize ?? 20, - }, - }); - ChatContactManager.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTfetchContacts].cursor, - list: r?.[MTfetchContacts].list, - opt: { - map: (param: any) => { - return new ChatContact(param); - }, - }, - }); - return ret; - } - /** * 设置联系人备注。 * diff --git a/src/ChatEvents.ts b/src/ChatEvents.ts index 61f9d100..46826819 100644 --- a/src/ChatEvents.ts +++ b/src/ChatEvents.ts @@ -1,14 +1,16 @@ import { ExceptionHandler } from './__internal__/ErrorHandler'; import type { ChatContact } from './common/ChatContact'; import type { ChatConversationType } from './common/ChatConversation'; -import { ChatError, ChatException } from './common/ChatError'; -import type { ChatGroup, ChatGroupMessageAck } from './common/ChatGroup'; +import { ChatException } from './common/ChatError'; +import type { ChatDataSyncType } from './common/ChatOptions'; +import type { ChatGroup } from './common/ChatGroup'; import type { ChatMessage, ChatMessagePinInfo, ChatRecalledMessageInfo, } from './common/ChatMessage'; import type { ChatMessageReactionEvent } from './common/ChatMessageReaction'; +import type { ChatMessageReadReceipt } from './common/ChatMessageReadReceipt'; import type { ChatMessageThreadEvent } from './common/ChatMessageThread'; import type { ChatPresence } from './common/ChatPresence'; import type { ChatRoom } from './common/ChatRoom'; @@ -138,7 +140,14 @@ export enum ChatMultiDeviceEvent { */ GROUP_REMOVE_ALL_BAN, /** - * 用户 A 在设备 A1 上创建了子区,则设备 A2 上会收到该事件。 + * 用户 A 在设备 A1 上更新群组信息时,设备 A2 上会触发该事件。 + * + * 仅 iOS SDK 会上报该事件(原生值为 34,即 `EMMultiDevicesEventGroupUpdate`);Android SDK + * 使用原生值 52 上报群组元数据变更。 + */ + GROUP_UPDATE, + /** + * 用户 A 在设备 A1 上创建子区时,设备 A2 上会触发该事件。 */ THREAD_CREATE = 40, /** @@ -187,6 +196,18 @@ export enum ChatMultiDeviceEvent { * 系统内部会更新会话的推送提醒方式。如果需要则需要手动更新会话列表。 */ CONVERSATION_MUTE_CHANGED = 64, + /** + * 用户 A 在设备 A1 上清除某个会话的未读消息数时,设备 A2 上会触发该事件。 + * + * 详见 {@link ChatManager.clearConversationUnreadMessageCount}。 + */ + CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED = 65, + /** + * 用户 A 在设备 A1 上清除所有会话的未读消息数时,设备 A2 上会触发该事件。 + * + * 详见 {@link ChatManager.clearAllConversationUnreadMessageCount}。 + */ + ALL_CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED = 66, } /** @@ -258,6 +279,8 @@ export function ChatMultiDeviceEventFromNumber( return ChatMultiDeviceEvent.GROUP_ALL_BAN; case 33: return ChatMultiDeviceEvent.GROUP_REMOVE_ALL_BAN; + case 34: + return ChatMultiDeviceEvent.GROUP_UPDATE; case 40: return ChatMultiDeviceEvent.THREAD_CREATE; @@ -285,6 +308,10 @@ export function ChatMultiDeviceEventFromNumber( return ChatMultiDeviceEvent.CONVERSATION_UPDATE_MARK; case 64: return ChatMultiDeviceEvent.CONVERSATION_MUTE_CHANGED; + case 65: + return ChatMultiDeviceEvent.CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED; + case 66: + return ChatMultiDeviceEvent.ALL_CONVERSATION_UNREAD_MESSAGE_COUNT_CLEARED; default: const ret = params as ChatMultiDeviceEvent; @@ -300,17 +327,142 @@ export function ChatMultiDeviceEventFromNumber( } /** - * 网络连接状态监听器。 + * {@link ChatConnectEventListener.onDisconnected} 携带的断开连接原因码。 + * + * 这些数值与底层原生 SDK 的 `EMError` 错误码一致。SDK 会原样传递这些错误码且不进行过滤, + * 因此仍可能收到此处未列出的错误码(包括未来原生 SDK 版本新增的错误码);该枚举仅用于提高可读性和便于比较。 + * + * 断开连接原因分为两类: + * - 登出原因:本地登录状态不再有效,用户必须重新登录。 + * - 连接原因:用户仍保持登录状态,但连接已断开,SDK 会自动重连。 + * + * 除非原生 SDK 文档另有说明,否则应将收到的未列出错误码视为连接原因。 + * + * 注意:token 过期不会触发 `onDisconnected`,而是通过 {@link ChatConnectEventListener.onTokenDidExpire} 上报。 + */ +export enum ChatDisconnectErrorCode { + // 登出原因:本地登录状态无效,用户必须重新登录。 + + /** + * 应用的日活跃用户数(DAU)或月活跃用户数(MAU)已达到上限。 + */ + APP_ACTIVE_NUMBER_REACH_LIMIT = 8, + + /** + * token 与登录信息不匹配。 + */ + INVALID_TOKEN = 104, + + /** + * 用于建立连接的参数无效。 + */ + INVALID_PARAM = 110, + + /** + * 用户鉴权失败。 + * + * 仅使用 token 登录时,鉴权失败通常由登录调用上报;如果 SDK 在重连期间刷新 token 失败,仍可能出现此错误码。 + */ + USER_AUTHENTICATION_FAILED = 202, + + /** + * 用户不存在。 + */ + USER_NOT_FOUND = 204, + + /** + * 用户已从其他设备登录。 + * + * 回调的 `info` 参数携带该设备的名称和扩展信息。 + */ + USER_LOGIN_FROM_OTHER_DEVICE = 206, + + /** + * 用户已从服务器移除。 + */ + USER_REMOVED_FROM_SERVER = 207, + + /** + * 用户已绑定其他设备,无法在当前设备上保持登录状态。 + */ + USER_BIND_ANOTHER_DEVICE = 213, + + /** + * 用户登录的设备数量过多。 + */ + USER_LOGIN_TOO_MANY_DEVICES = 214, + + /** + * 用户已修改密码。 + */ + USER_KICKED_BY_CHANGE_PASSWORD = 216, + + /** + * 用户被其他设备踢下线。 + */ + USER_KICKED_BY_OTHER_DEVICE = 217, + + /** + * 本次登录设备与上次登录设备不同,用户需要重新登录。 + * + * 仅当服务器启用“不将其他设备踢下线”选项时,该原因才会生效。 + */ + USER_DEVICE_CHANGED = 220, + + /** + * 无法获取可用的服务器地址。 + */ + SERVER_GET_DNSLIST_FAILED = 304, + + /** + * 应用被服务器禁用。 + */ + SERVER_SERVICE_RESTRICTED = 305, + + // 连接原因:用户保持登录状态,SDK 会自动重连。 + + /** + * 发生网络错误。 + * + * 该错误码仅由 Android 上报;在 iOS 上,网络断开时不会上报错误码。 + */ + NETWORK_ERROR = 2, + + /** + * 应用的用户数量或服务配额超过限制。 + */ + EXCEED_SERVICE_LIMIT = 4, + + /** + * 无法连接服务器,例如连接超时、DNS 解析失败或连接被拒绝。 + * + * 这是网络不稳定时最常见的原因。 + */ + SERVER_NOT_REACHABLE = 300, + + /** + * 发生未知服务器错误,例如 IO 错误或连接流意外关闭。 + */ + SERVER_UNKNOWN_ERROR = 303, + + /** + * 传输数据解密失败。 + */ + SERVER_DECRYPTION_FAILED = 306, +} + +/** + * 连接事件监听器。 * - * 在不稳定的网络环境断网时,SDK 可以接收到已断开连接的回调。 + * 在不稳定的网络环境中断开连接时,使用该 SDK 的应用会收到 `onDisconnected` 回调。 * - * SDK 会自动尝试重连,不需要你手动操作。 + * 无需手动重连,Chat SDK 会自动处理重连。 * - * 以下为两种连接状态回调: - * - `onConnected`: 当网络连接成功时提示已连接。 - * - `onDisconnected`: 当网络连接断开时提示已断开连接。 + * 包含以下两个连接相关回调: + * - `onConnected`:连接建立时触发。 + * - `onDisconnected`:连接断开时触发,并携带表示断开原因的错误码,详见 {@link ChatDisconnectErrorCode}。 * - * Adds a connection event listener: + * 添加连接事件监听器: * * ```typescript * let listener = new (class s implements ChatConnectEventListener { @@ -323,14 +475,14 @@ export function ChatMultiDeviceEventFromNumber( * onConnected(): void { * chatlog.log('ConnectScreen.onConnected'); * } - * onDisconnected(errorCode?: number): void { - * chatlog.log('ConnectScreen.onDisconnected', errorCode); + * onDisconnected(errorCode?: number, info?: { deviceName?: string; ext?: string }): void { + * chatlog.log('ConnectScreen.onDisconnected', errorCode, info); * } * })(); * ChatClient.getInstance().addConnectionListener(listener); * ``` * - * 移除连接事件监听器: + * 移除连接事件监听器: * * ```typescript * ChatClient.getInstance().removeConnectionListener(listener); @@ -343,13 +495,27 @@ export interface ChatConnectEventListener { onConnected?(): void; /** - * 和 chat 服务器断开连接时触发的回调。 + * SDK 与 chat 服务器断开连接时触发。 + * + * `errorCode` 由原生 SDK 原样传递,分为两类(详见 {@link ChatDisconnectErrorCode}): + * - 登出原因(例如 `USER_LOGIN_FROM_OTHER_DEVICE`):本地登录状态不再有效,应返回登录页面并重新登录。 + * - 连接原因(例如 `NETWORK_ERROR`、`SERVER_NOT_REACHABLE`):用户仍保持登录状态,但连接已断开,SDK 会自动重连;收到这些错误码时不要返回登录页面。 * - * 断开连接时底层不一定会登出。 + * 未在 {@link ChatDisconnectErrorCode} 中列出的错误码(包括未来原生 SDK 版本新增的错误码)也会原样传递; + * 除非原生 SDK 文档另有说明,否则应将其视为连接原因。 * - * @param errorCode 错误码,详见 {@link ChatError}。 + * 如果没有 `errorCode`,表示平台未提供断开原因(例如 iOS 上的网络断开);也应将其视为连接事件。 + * + * 注意:token 过期导致的登出不会触发该回调,详见 {@link ChatConnectEventListener.onTokenDidExpire}。 + * 需要同时处理这两个回调,才能覆盖所有登出场景。 + * + * @param errorCode 断开连接的原因,详见 {@link ChatDisconnectErrorCode}。 + * @param info 仅当 `errorCode` 为 {@link ChatDisconnectErrorCode.USER_LOGIN_FROM_OTHER_DEVICE} 时存在:`deviceName` 为发起登录的设备名称,`ext` 为该设备的扩展信息。详见 {@link ChatOptions.loginExtraInfo}。 */ - onDisconnected?(): void; + onDisconnected?( + errorCode?: number, + info?: { deviceName?: string; ext?: string } + ): void; /** * Agora token 即将过期时触发。 @@ -361,13 +527,6 @@ export interface ChatConnectEventListener { */ onTokenDidExpire?(): void; - /** - * 应用程序的日活跃用户数量(DAU)或月活跃用户数量(MAU)达到上限时回调。 - * - * 服务器主动断开连接。 - */ - onAppActiveNumberReachLimit?(): void; - /** * 开始接收离线消息的时候触发。 */ @@ -379,70 +538,29 @@ export interface ChatConnectEventListener { onOfflineMessageSyncFinish?(): void; /** - * 其他设备登录通知。 - * - * 服务器主动断开连接。 - * - * @deprecated 2024-08-15 请使用 {@link onUserDidLoginFromOtherDeviceWithInfo} 替换 - */ - onUserDidLoginFromOtherDevice?(deviceName?: string): void; - - /** - * 用户在其它设备登录。 - * - * 当前用户被服务器断开。 - * - * @params - - * - Param [deviceName] 设备名称。 - * - Param [ext] 设备扩展信息。 详见 {@link ChatOptions.loginExtraInfo}. - * - */ - onUserDidLoginFromOtherDeviceWithInfo?(params: { - deviceName: string; - ext?: string; - }): void; - - /** - * 用户被移除通知。 + * SDK 开始从服务器同步指定类型的数据时触发。 * - * 服务器主动断开连接。 + * @param type 要同步的数据类型,是 {@link ChatDataSyncType} 的位掩码。 */ - onUserDidRemoveFromServer?(): void; + onDataSyncStart?(type: ChatDataSyncType): void; /** - * 被服务器禁止连接通知。 + * SDK 完成从服务器同步指定类型的数据时触发。 * - * 服务器主动断开连接。 + * @param type 已同步的数据类型,是 {@link ChatDataSyncType} 的位掩码。 + * @param errorCode 同步结果的错误码。`0` 表示成功。 */ - onUserDidForbidByServer?(): void; + onDataSyncFinish?(type: ChatDataSyncType, errorCode: number): void; /** - * 用户密码变更通知。 + * 本地数据库打开时触发。 * - * 服务器主动断开连接。 - */ - onUserDidChangePassword?(): void; - - /** - * 登录设备数量超限通知。 + * 数据库的打开过程独立于登录流程。依赖本地数据库的 SDK 方法只能在该回调触发后调用。 * - * 服务器主动断开连接。 + * @param username 已打开数据库所绑定的用户 ID。 + * @param errorCode 数据库打开结果的错误码。`0` 表示成功。 */ - onUserDidLoginTooManyDevice?(): void; - - /** - * 被其他设备踢掉通知。 - * - * 服务器主动断开连接。 - */ - onUserKickedByOtherDevice?(): void; - - /** - * 鉴权失败通知。 典型触发通知场景:token 过期、token 验证失败。 - * - * 服务器主动断开连接。 - */ - onUserAuthenticationFailed?(): void; + onDatabaseOpened?(username: string, errorCode: number): void; } /** @@ -526,7 +644,7 @@ export interface ChatCustomEventListener { * * - 消息成功发送到对方后,发送方会收到送达回执(需开启送达回执功能,详见 {@link ChatOptions#requireDeliveryAck(boolean)})。 * - * - 对方阅读了这条消息,发送方会收到已读回执(需开启已读回执功能,详见 {@link ChatOptions#requireAck(boolean)})。 + * - 对方阅读了这条消息,发送方会收到已读回执(发送消息时需将 {@link ChatMessage.isNeedReadReceipt} 设置为 `true`)。 * * 发送消息过程中,消息 ID 会从最初本地生成的 uuid 变更为服务器端生成的全局唯一 ID,该 ID 在使用 SDK 的所有设备上均唯一。 * @@ -595,18 +713,16 @@ export interface ChatMessageEventListener { onCmdMessagesReceived?(messages: Array): void; /** - * 收到单聊消息已读回执的回调。 + * 收到消息已读回执时触发。 * - * @param messages 消息的已读回执。 - */ - onMessagesRead?(messages: Array): void; - - /** - * 收到群组消息的已读回执的回调。 + * 以下任一场景会触发该回调: + * + * - 接收方阅读消息并发送已读回执,详见 {@link ChatManager.sendMessageReadReceipts}。 + * - 多端多设备登录场景下,当前用户在其他设备上阅读消息或清除会话未读消息数。 * - * @param groupMessageAcks 群组消息已读回执。 + * @param receipts 消息已读回执,详见 {@link ChatMessageReadReceipt}。 */ - onGroupMessageRead?(groupMessageAcks: Array): void; + onMessageReadReceipts?(receipts: Array): void; /** * 收到消息已送达回执的回调。 @@ -623,23 +739,11 @@ export interface ChatMessageEventListener { onMessagesRecalledInfo?(info: Array): void; /** - * 会话更新事件回调。 - */ - onConversationsUpdate?(): void; - - /** - * 收到会话已读回执的回调。 - * - * 回调此方法的场景: - * (1)消息被接收方阅读,即接收方发送了会话已读回执。 - * SDK 在接收到此事件时,会将本地数据库中该会话中消息的 `isAcked` 属性置为 `true`。 - * (2)多端多设备登录场景下,一端发送会话已读回执,服务器端会将会话的未读消息数置为 0, - * 同时其他端会回调此方法,并将本地数据库中该会话中消息的 `isRead` 属性置为 `true`。 + * 会话更新时触发。 * - * @param from 发送已读回执的用户 ID。 - * @param to 收到已读回执的用户 ID。 + * 仅在添加或删除会话时触发该回调。 */ - onConversationRead?(from: string, to?: string): void; + onConversationsUpdate?(): void; /** * 消息表情回复(Reaction)变化监听器。 @@ -662,16 +766,12 @@ export interface ChatMessageEventListener { * 子区所属群组的所有成员均可调用该方法。 * * @param event 子区更新事件。 - * - * @param event The message thread update event. */ onChatMessageThreadUpdated?(event: ChatMessageThreadEvent): void; /** * 子区移除回调。 * * @param event 子区移除事件。 - * - * @param event The message thread destruction event. */ onChatMessageThreadDestroyed?(event: ChatMessageThreadEvent): void; /** @@ -899,14 +999,6 @@ export interface ChatGroupEventListener { newOwner: string; oldOwner: string; }): void; - /** - * 新成员加入群组的回调。 - * - * @params 参数组。 - * - Param [groupId] 群组 ID。 - * - Param [member] 新成员的用户 ID。 - */ - onMemberJoined?(params: { groupId: string; member: string }): void; /** * 多个群成员加入群组的回调。 * @@ -917,14 +1009,6 @@ export interface ChatGroupEventListener { * - Param [members] 新成员的用户 ID 数组。 */ onMembersJoined?(params: { groupId: string; members: Array }): void; - /** - * 群组成员主动退出回调。 - * - * @params 参数组。 - * - Param [groupId] 群组 ID。 - * - Param [member] 退群的成员的用户 ID。 - */ - onMemberExited?(params: { groupId: string; member: string }): void; /** * 多个群组成员主动退出回调。 * @@ -1089,16 +1173,6 @@ export interface ChatContactEventListener { * @param userName 发起好友请求的用户 ID。 */ onFriendRequestDeclined?(userName: string): void; - /** - * 开始从服务器同步联系人列表时触发。 - */ - onContactSyncStart?(): void; - /** - * 从服务器同步联系人列表完成时触发。 - * - * @param error 同步失败时的错误信息。详见 {@link ChatError}。 - */ - onContactSyncFinish?(error?: ChatError): void; /** * 联系人信息更新时触发。 * @@ -1178,20 +1252,6 @@ export interface ChatRoomEventListener { roomName?: string; reason?: string; }): void; - /** - * 有成员被禁言回调。 - * - * @params 参数组。 - * - Param [roomId] 聊天室 ID。 - * - Param [mutes] 被禁言成员的用户 ID。 - * - Param [expireTime] 预留参数,禁言过期时间戳。 - */ - onMuteListAdded?(params: { - roomId: string; - mutes: Array; - expireTime?: string; - }): void; - /** * 增加禁言成员时候回调。 * diff --git a/src/ChatGroupManager.ts b/src/ChatGroupManager.ts index 54a850d8..169b29fd 100644 --- a/src/ChatGroupManager.ts +++ b/src/ChatGroupManager.ts @@ -28,8 +28,6 @@ import { MTgetGroupSpecificationFromServer, MTgetGroupWithId, MTgetJoinedGroups, - MTgetJoinedGroupsFromServer, - MTgetPublicGroupsFromServer, MTinviterUser, MTisMemberInAllowListFromServer, MTjoinPublicGroup, @@ -50,6 +48,7 @@ import { MTupdateDescription, MTupdateGroupAnnouncement, MTupdateGroupAvatar, + MTupdateGroupConfigs, MTupdateGroupExt, MTupdateGroupNamecard, MTupdateGroupOwner, @@ -64,10 +63,10 @@ import { ChatCursorResult } from './common/ChatCursorResult'; import { ChatException } from './common/ChatError'; import { ChatGroup, + type ChatGroupConfigs, + type ChatGroupConfigsType, type ChatGroupFileStatusCallback, - ChatGroupInfo, ChatGroupMember, - ChatGroupOptions, ChatGroupSharedFile, } from './common/ChatGroup'; @@ -186,13 +185,13 @@ export class ChatGroupManager extends BaseManager { case 'onAdminAdded': listener.onAdminAdded?.({ groupId: params.groupId, - admin: params.admin, + admin: params.administrator, }); break; case 'onAdminRemoved': listener.onAdminRemoved?.({ groupId: params.groupId, - admin: params.admin, + admin: params.administrator, }); break; case 'onOwnerChanged': @@ -202,24 +201,12 @@ export class ChatGroupManager extends BaseManager { oldOwner: params.oldOwner, }); break; - case 'onMemberJoined': - listener.onMemberJoined?.({ - groupId: params.groupId, - member: params.member, - }); - break; case 'onMembersJoined': listener.onMembersJoined?.({ groupId: params.groupId, members: params.members, }); break; - case 'onMemberExited': - listener.onMemberExited?.({ - groupId: params.groupId, - member: params.member, - }); - break; case 'onMembersExited': listener.onMembersExited?.({ groupId: params.groupId, @@ -259,7 +246,7 @@ export class ChatGroupManager extends BaseManager { case 'onAllMemberMuteStateChanged': listener.onAllGroupMemberMuteStateChanged?.({ groupId: params.groupId, - isAllMuted: params.isAllMuted, + isAllMuted: params.isMuted, }); break; case 'onStateChanged': @@ -366,118 +353,6 @@ export class ChatGroupManager extends BaseManager { return ret; } - /** - * 以分页方式从服务器获取当前用户已加入的群组。 - * - * 此操作只返回群组列表,不包含群组的所有成员信息。如果要更新某个群组包括成员的全部信息,需要再调用 {@link #fetchMemberListFromServer}。 - * - * @param pageSize 每页期望返回的群组数。[1, 20] - * @param pageNum 当前页码,从 0 开始。 - * @returns 当前用户已加入的群组列表 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async fetchJoinedGroupsFromServer( - pageSize: number, - pageNum: number - ): Promise> { - chatlog.log( - `${ChatGroupManager.TAG}: fetchJoinedGroupsFromServer: `, - pageSize, - pageNum - ); - let r: any = await Native._callMethod(MTgetJoinedGroupsFromServer, { - [MTgetJoinedGroupsFromServer]: { - pageSize, - pageNum, - needRole: true, - needMemberCount: true, - }, - }); - ChatGroupManager.checkErrorFromResult(r); - const ret: ChatGroup[] = []; - Object.entries(r?.[MTgetJoinedGroupsFromServer]).forEach( - (value: [string, any]) => { - ret.push(new ChatGroup(value[1])); - } - ); - return ret; - } - - /** - * 分页从服务器获取公开群组。 - * - * @param pageSize 每页期望返回的群组数。 - * @param cursor 开始取数据的游标位置。首次调用时传 `null`,按群组创建时间的倒序获取。 - * @returns 群组列表以及用于下次查询的 cursor。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async fetchPublicGroupsFromServer( - pageSize: number, - cursor?: string - ): Promise> { - chatlog.log( - `${ChatGroupManager.TAG}: fetchPublicGroupsFromServer: `, - pageSize, - cursor - ); - let r: any = await Native._callMethod(MTgetPublicGroupsFromServer, { - [MTgetPublicGroupsFromServer]: { - pageSize, - cursor, - }, - }); - ChatGroupManager.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTgetPublicGroupsFromServer].cursor, - list: r?.[MTgetPublicGroupsFromServer].list, - opt: { - map: (param: any) => { - return new ChatGroupInfo(param); - }, - }, - }); - return ret; - } - - /** - * 创建群组。 - * - * 群组创建成功后,会更新内存及数据库中的数据,多端多设备会收到相应的通知事件,然后将群组更新到内存及数据库中。 - * - * 可通过设置 {@link ChatGroupEventListener} 监听相关事件。 - * - * @param options 群组创建时需设置的选项。该参数可选,不可为 `null`。详见 {@link ChatGroupOptions}. - * 群组的其他选项如下: - * - 群组最大成员数,默认值为 200; - * - 群组类型,详见 {@link ChatGroupStyle}。默认值为 {@link ChatGroupStyle.PrivateOnlyOwnerInvite},即私有群,仅群主可邀请用户入群; - * - 邀请入群是否需要对方同意,默认为 `false`,即邀请后直接入群; - * - 群详情扩展。 - * @param groupName 群组名称。该参数可选,不设置传 `null`。 - * @param desc 群组描述。该参数可选,不设置传 `null`。 - * @param inviteMembers 群成员列表。可选参数。该参数不可为 `null`。 - * @param inviteReason 成员入群的邀请信息。该参数可选,不设置传 `null`。 - * @returns 创建成功的群组实例。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async createGroup( - options: ChatGroupOptions, - groupName: string, - desc?: string, - inviteMembers?: Array, - inviteReason?: string - ): Promise { - return this.createGroupEx({ - options, - groupName, - desc, - inviteMembers, - inviteReason, - }); - } - /** * 创建群组对象。支持设置头像。 * @@ -485,12 +360,14 @@ export class ChatGroupManager extends BaseManager { * * 可通过设置 {@link ChatGroupEventListener} 监听相关事件。 * - * @param options 群组创建时需设置的选项。该参数可选,不可为 `null`。详见 {@link ChatGroupOptions}. - * 群组的其他选项如下: - * - 群组最大成员数,默认值为 200; - * - 群组类型,详见 {@link ChatGroupStyle}。默认值为 {@link ChatGroupStyle.PrivateOnlyOwnerInvite},即私有群,仅群主可邀请用户入群; - * - 邀请入群是否需要对方同意,默认为 `false`,即邀请后直接入群; - * - 群详情扩展。 + * @param configs 创建群组时使用的配置,详见 {@link ChatGroupConfigs}。 + * 配置项如下: + * - 群组允许的最大成员数,默认值为 200。 + * - 群组是否为公开群,默认值为 `false`。 + * - 加入群组是否需要群主或管理员批准,默认值为 `false`。 + * - 是否允许群成员邀请其他用户加入群组,默认值为 `false`。 + * - 邀请用户加入群组时是否需要征得对方同意,默认值为 `false`,表示无需对方同意即可自动入群。 + * - 群详情扩展信息。 * @param groupName 群组名称。 * @param groupAvatar 群组头像。 * @param desc 群组描述。 @@ -501,7 +378,7 @@ export class ChatGroupManager extends BaseManager { * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ public async createGroupEx(params: { - options: ChatGroupOptions; + configs: ChatGroupConfigs; groupName: string; groupAvatar?: string; desc?: string; @@ -509,7 +386,7 @@ export class ChatGroupManager extends BaseManager { inviteReason?: string; }): Promise { const { - options, + configs, groupName, groupAvatar, desc, @@ -518,7 +395,7 @@ export class ChatGroupManager extends BaseManager { } = params; chatlog.log( `${ChatGroupManager.TAG}: createGroupEx: `, - options, + configs, groupName, groupAvatar, desc, @@ -532,7 +409,7 @@ export class ChatGroupManager extends BaseManager { desc, inviteMembers, inviteReason, - options, + configs, }, }); ChatGroupManager.checkErrorFromResult(r); @@ -540,37 +417,37 @@ export class ChatGroupManager extends BaseManager { } /** - * 从服务器获取群组详情。 + * 更新群组配置。 + * + * 仅群主和群管理员可以调用该方法。 * * @param groupId 群组 ID。 - * @param isFetchMembers 是否获取群组成员信息: - * - `true`:是;该方法最多可获取 200 个成员的信息。如需获取所有群组成员的信息,可调用 {@link fetchMemberListFromServer}。 - * - `false`:否。 - * @returns 群组实例。如果群组不存在,返回 `undefined`。 + * @param types 要更新的配置类型,是 {@link ChatGroupConfigsType} 的位掩码。仅更新该参数指定的配置。 + * @param configs 新的群组配置,详见 {@link ChatGroupConfigs}。 + * @returns 更新后的群组实例。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async fetchGroupInfoFromServer( + public async updateGroupConfigs( groupId: string, - isFetchMembers: boolean = false - ): Promise { + types: ChatGroupConfigsType, + configs: ChatGroupConfigs + ): Promise { chatlog.log( - `${ChatGroupManager.TAG}: fetchGroupInfoFromServer: `, + `${ChatGroupManager.TAG}: updateGroupConfigs: `, groupId, - isFetchMembers + types, + configs ); - let r: any = await Native._callMethod(MTgetGroupSpecificationFromServer, { - [MTgetGroupSpecificationFromServer]: { - groupId: groupId, - fetchMembers: isFetchMembers, + let r: any = await Native._callMethod(MTupdateGroupConfigs, { + [MTupdateGroupConfigs]: { + group_id: groupId, + types: types, + configs: configs, }, }); ChatGroupManager.checkErrorFromResult(r); - const g = r?.[MTgetGroupSpecificationFromServer]; - if (g) { - return new ChatGroup(g); - } - return undefined; + return new ChatGroup(r?.[MTupdateGroupConfigs]); } /** @@ -1581,7 +1458,7 @@ export class ChatGroupManager extends BaseManager { * * 若群组无需群主或管理员验证,可直接加入。 * - * 若群组需要群主或管理员验证,用户需等待请求批准后才能加入。群组类型详见 {@link ChatGroupStyle}。 + * 若群组需要群主或管理员验证,用户需等待请求批准后才能加入,详见 {@link ChatGroupConfigs.joinApprovalRequired}。 * * @param groupId 群组 ID。 * @@ -1600,7 +1477,7 @@ export class ChatGroupManager extends BaseManager { /** * 申请加入群组。 * - * 该方法仅适用于群组类型为 {@link ChatGroupStyle.PublicJoinNeedApproval} 的公开群组。 + * 该方法仅适用于将 {@link ChatGroupConfigs.joinApprovalRequired} 设置为 `true` 的公开群组。 * * @param groupId 群组 ID。 * @param reason 申请加入的理由。 @@ -1631,7 +1508,7 @@ export class ChatGroupManager extends BaseManager { * 仅群主和管理员可调用此方法。 * * @param groupId 群组 ID。 - * @param username 申请入群的用户 ID。 + * @param userId 申请入群的用户 ID。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ diff --git a/src/ChatManager.ts b/src/ChatManager.ts index 3e6ef9e9..e09aa57e 100644 --- a/src/ChatManager.ts +++ b/src/ChatManager.ts @@ -2,16 +2,15 @@ import type { NativeEventEmitter } from 'react-native'; import { BaseManager } from './__internal__/Base'; import { - MTackConversationRead, - MTackGroupMessageRead, - MTackMessageRead, MTaddReaction, MTaddRemoteAndLocalConversationsMark, - MTasyncFetchGroupAcks, + MTclearAllConversationUnreadMessageCount, MTclearAllMessages, + MTclearConversationUnreadMessageCount, MTcreateChatThread, MTdeleteAllMessageAndConversation, MTdeleteConversation, + MTdeleteConversations, MTdeleteMessagesBeforeTimestamp, MTdeleteMessagesWithTs, MTdeleteRemoteAndLocalConversationsMark, @@ -26,9 +25,7 @@ import { MTfetchChatThreadDetail, MTfetchChatThreadMember, MTfetchChatThreadsWithParentId, - MTfetchConversationsByOptions, - MTfetchConversationsFromServerWithPage, - MTfetchHistoryMessages, + MTfetchGroupMessageReadReceipts, MTfetchHistoryMessagesByOptions, MTfetchJoinedChatThreads, MTfetchJoinedChatThreadsWithParentId, @@ -37,11 +34,9 @@ import { MTfetchReactionDetail, MTfetchReactionList, MTfetchSupportLanguages, - MTgetConvsMsgsWithKeyword, MTgetConversation, - MTgetConversationsFromDBWithCursor, - MTgetConversationsFromServer, - MTgetConversationsFromServerWithCursor, + MTgetConvsMsgsWithKeyword, + MTgetGroupMessageReadReceipts, MTgetLatestMessage, MTgetLatestMessageFromOthers, MTgetMessage, @@ -50,12 +45,10 @@ import { MTgetMessageThread, MTgetMsgCount, MTgetPinInfo, - MTgetPinnedConversationsFromServerWithCursor, MTgetReactionList, MTgetThreadConversation, MTgetUnreadMessageCount, MTgetUnreadMsgCount, - MTgroupAckCount, MTimportMessages, MTinsertMessage, MTjoinChatThread, @@ -65,27 +58,20 @@ import { MTloadMsgWithMsgType, MTloadMsgWithStartId, MTloadMsgWithTime, - MTmarkAllChatMsgAsRead, - MTmarkAllMessagesAsRead, - MTmarkMessageAsRead, MTmessageReactionDidChange, - MTmodifyMessage, MTmodifyMsgBody, MTonChatThreadCreated, MTonChatThreadDestroyed, MTonChatThreadUpdated, MTonChatThreadUserRemoved, MTonCmdMessagesReceived, - MTonConversationHasRead, MTonConversationUpdate, - MTonGroupMessageRead, MTonMessageContentChanged, MTonMessagePinChanged, + MTonMessageReadReceipts, MTonMessagesDelivered, - MTonMessagesRead, MTonMessagesRecalledInfo, MTonMessagesReceived, - MTonReadAckForGroupMessageUpdated, MTpinConversation, MTpinMessage, MTpinnedMessages, @@ -96,13 +82,13 @@ import { MTremoveMessagesFromServerWithTs, MTremoveMessagesWithTimestamp, MTremoveReaction, - MTreportMessage, MTresendMessage, MTsearchChatMsgFromDB, MTsearchMessages, MTsearchMessagesFromServer, MTsearchMessagesInConversation, MTsendMessage, + MTsendMessageReadReceipts, MTsyncConversationExt, MTtranslateMessage, MTunpinMessage, @@ -119,14 +105,13 @@ import type { ChatMessageEventListener } from './ChatEvents'; import { chatlog } from './common/ChatConst'; import { ChatConversation, - ChatConversationFetchOptions, ChatConversationMarkType, ChatConversationType, ChatSearchDirection, } from './common/ChatConversation'; import { ChatCursorResult } from './common/ChatCursorResult'; import { ChatError } from './common/ChatError'; -import { ChatGroupMessageAck } from './common/ChatGroup'; +import { ChatGroupReadReceipt } from './common/ChatGroup'; import { type ChatFetchMessageOptions, ChatMessage, @@ -139,6 +124,7 @@ import { ChatMessageType, ChatRecalledMessageInfo, } from './common/ChatMessage'; +import { ChatMessageReadReceipt } from './common/ChatMessageReadReceipt'; import { ChatMessageReaction, ChatMessageReactionEvent, @@ -216,10 +202,11 @@ export class ChatManager extends BaseManager { MTonCmdMessagesReceived, this.onCmdMessagesReceived.bind(this) ); - event.removeAllListeners(MTonMessagesRead); - event.addListener(MTonMessagesRead, this.onMessagesRead.bind(this)); - event.removeAllListeners(MTonGroupMessageRead); - event.addListener(MTonGroupMessageRead, this.onGroupMessageRead.bind(this)); + event.removeAllListeners(MTonMessageReadReceipts); + event.addListener( + MTonMessageReadReceipts, + this.onMessageReadReceipts.bind(this) + ); event.removeAllListeners(MTonMessagesDelivered); event.addListener( MTonMessagesDelivered, @@ -235,16 +222,6 @@ export class ChatManager extends BaseManager { MTonConversationUpdate, this.onConversationsUpdate.bind(this) ); - event.removeAllListeners(MTonConversationHasRead); - event.addListener( - MTonConversationHasRead, - this.onConversationHasRead.bind(this) - ); - event.removeAllListeners(MTonReadAckForGroupMessageUpdated); - event.addListener( - MTonReadAckForGroupMessageUpdated, - this.onReadAckForGroupMessageUpdated.bind(this) - ); event.removeAllListeners(MTmessageReactionDidChange); event.addListener( MTmessageReactionDidChange, @@ -313,6 +290,7 @@ export class ChatManager extends BaseManager { recalledBy: param.recalledBy, recalledExt: param.recalledExt, recalledMessageId: param.recalledMessageId, + recalledConvId: param.recalledConvId, }); list.push(m); }); @@ -339,28 +317,18 @@ export class ChatManager extends BaseManager { listener.onCmdMessagesReceived?.(list); }); } - private onMessagesRead(messages: any): void { - chatlog.log(`${ChatManager.TAG}: onMessagesRead: `, messages); + private onMessageReadReceipts(params: any): void { + chatlog.log(`${ChatManager.TAG}: onMessageReadReceipts: `, params); if (this._messageListeners.size === 0) { return; } - let list: Array = this.createReceiveMessage(messages); - this._messageListeners.forEach((listener: ChatMessageEventListener) => { - listener.onMessagesRead?.(list); - }); - } - private onGroupMessageRead(messages: any): void { - chatlog.log(`${ChatManager.TAG}: onGroupMessageRead: `, messages); - if (this._messageListeners.size === 0) { - return; - } - let list: Array = []; - messages.forEach((message: any) => { - let m = new ChatGroupMessageAck(message); - list.push(m); + let list: Array = []; + const receipts: Array = params?.receipts ?? []; + receipts.forEach((receipt: any) => { + list.push(new ChatMessageReadReceipt(receipt)); }); this._messageListeners.forEach((listener: ChatMessageEventListener) => { - listener.onGroupMessageRead?.(messages); + listener.onMessageReadReceipts?.(list); }); } private onMessagesDelivered(messages: any): void { @@ -390,21 +358,6 @@ export class ChatManager extends BaseManager { listener.onConversationsUpdate?.(); }); } - private onConversationHasRead(params: any): void { - chatlog.log(`${ChatManager.TAG}: onConversationHasRead: `, params); - this._messageListeners.forEach((listener: ChatMessageEventListener) => { - let from = params?.from; - let to = params?.to; - listener.onConversationRead?.(from, to); - }); - } - - private onReadAckForGroupMessageUpdated(params: any): void { - chatlog.log( - `${ChatManager.TAG}: onReadAckForGroupMessageUpdated: `, - params - ); - } private onMessageReactionDidChange(params: any): void { chatlog.log( @@ -662,94 +615,75 @@ export class ChatManager extends BaseManager { } /** - * 发送消息的已读回执。 - * - * 该方法只针对单聊会话。 + * 向服务器发送消息已读回执。 * * **注意** * - * 该方法只有在 {@link ChatOptions.requireAck} 为 `true` 时才生效。 - * - * 若发送群消息已读回执,详见 {@link sendGroupMessageReadAck}。 + * - 该方法适用于单聊和群聊。 + * - 每次最多可发送 50 条消息的已读回执,且这些消息必须属于同一个会话。 + * - 仅当发送消息时将 {@link ChatMessage.isNeedReadReceipt} 设置为 `true`,该方法才会生效。 * - * 推荐进入会话页面时调用 {@link sendConversationReadAck},其他情况下调用该方法以减少调用频率。 - * - * @param message 需要发送已读回执的消息。 + * @param msgIds 要发送已读回执的消息 ID 列表。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async sendMessageReadAck(message: ChatMessage): Promise { - chatlog.log( - `${ChatManager.TAG}: sendMessageReadAck: ${message.msgId}, ${message.localTime}`, - message - ); - let r: any = await Native._callMethod(MTackMessageRead, { - [MTackMessageRead]: { - to: message.from, - msg_id: message.msgId, + public async sendMessageReadReceipts(msgIds: string[]): Promise { + chatlog.log(`${ChatManager.TAG}: sendMessageReadReceipts: `, msgIds); + let r: any = await Native._callMethod(MTsendMessageReadReceipts, { + [MTsendMessageReadReceipts]: { + msg_ids: msgIds, }, }); Native.checkErrorFromResult(r); } /** - * 发送群消息已读回执。 + * 清除指定会话的未读消息数。 * - * **Warning** + * **注意** * - * - 该方法只有在 {@link ChatOptions.requireAck} 和 {@link ChatMessage.needGroupAck} 设置为 `true` 时才会生效。 - * - 该方法仅对群组消息有效。发送单聊消息已读回执,详见 {@link sendMessageReadAck};发送会话已读回执,详见 {@link sendConversationReadAck}。 + * 该方法仅清除会话在本地的未读消息数,并在多端多设备登录场景下将该操作同步到当前用户的其他设备。 + * 该方法不会为会话中的消息发送已读回执。 * - * @param msgId 消息 ID。 - * @param groupId 群组 ID。 - * @param opt 扩展信息。用户通过定义关键字指定动作或命令。 + * @param convId 会话 ID。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async sendGroupMessageReadAck( - msgId: string, - groupId: string, - opt?: { content: string } + public async clearConversationUnreadMessageCount( + convId: string ): Promise { chatlog.log( - `${ChatManager.TAG}: sendGroupMessageReadAck: ${msgId}, ${groupId}` + `${ChatManager.TAG}: clearConversationUnreadMessageCount: ${convId}` + ); + let r: any = await Native._callMethod( + MTclearConversationUnreadMessageCount, + { + [MTclearConversationUnreadMessageCount]: { + convId: convId, + }, + } ); - let s = opt?.content - ? { - msg_id: msgId, - group_id: groupId, - content: opt?.content, - } - : { - msg_id: msgId, - group_id: groupId, - }; - let r: any = await Native._callMethod(MTackGroupMessageRead, { - [MTackGroupMessageRead]: s, - }); Native.checkErrorFromResult(r); } /** - * 发送会话的已读回执。 + * 清除所有会话的未读消息数。 * * **注意** * - * - 该方法仅适用于单聊会话。 - * - * - 该方法通知服务器将此会话未读数设置为 `0`,消息发送方(包含多端多设备)将会收到 {@link ChatMessageEventListener.onConversationRead} 回调。 - * - * @param convId 会话 ID。 + * 该方法仅清除所有会话在本地的未读消息数,并在多端多设备登录场景下将该操作同步到当前用户的其他设备。 + * 该方法不会为这些会话中的消息发送已读回执。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async sendConversationReadAck(convId: string): Promise { - chatlog.log(`${ChatManager.TAG}: sendConversationReadAck: ${convId}`); - let r: any = await Native._callMethod(MTackConversationRead, { - [MTackConversationRead]: { - convId: convId, - }, - }); + public async clearAllConversationUnreadMessageCount(): Promise { + chatlog.log(`${ChatManager.TAG}: clearAllConversationUnreadMessageCount: `); + let r: any = await Native._callMethod( + MTclearAllConversationUnreadMessageCount, + { + [MTclearAllConversationUnreadMessageCount]: {}, + } + ); Native.checkErrorFromResult(r); } @@ -830,19 +764,6 @@ export class ChatManager extends BaseManager { return rr.map((msg: any) => new ChatMessage(msg)); } - /** - * 将所有的会话都设成已读。 - * - * 该方法仅对本地会话有效。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async markAllConversationsAsRead(): Promise { - chatlog.log(`${ChatManager.TAG}: markAllConversationsAsRead: `); - let r: any = await Native._callMethod(MTmarkAllChatMsgAsRead); - Native.checkErrorFromResult(r); - } - /** * 获取未读消息数。 * @@ -1113,67 +1034,13 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 分页获取指定会话的历史消息。 - * - * **注意** 调用该方法,如果会话对象不存在则创建。 - * - * @param convId 会话 ID。 - * @param convType 会话类型。详见 {@link ChatConversationType}。 - * @params params - - * - pageSize: 每页期望返回的消息数量。取值范围为 [1,50]。 - * - startMsgId: 开始消息 ID。设置该参数后,SDK 从指定消息开始,按服务器接收消息的时间倒序获取。如果该参数为空字符串,SDK 从最新消息开始,按服务器接收消息的时间倒序获取。 - * - direction: 消息搜索方向,详见 {@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.Up`:按消息中包含的 Unix 时间戳降序获取。 - * - `ChatSearchDirection.Down`:按消息中包含的 Unix 时间戳升序获取。 - * @returns 获取到的消息列表(不包含起始 ID 的消息)和下次查询的 cursor。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - * - * @deprecated 2024-08-15 请使用 {@link fetchHistoryMessagesByOptions} 替代。 - */ - public async fetchHistoryMessages( - convId: string, - convType: ChatConversationType, - params: { - pageSize?: number; - startMsgId?: string; - direction?: ChatSearchDirection; - } - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: fetchHistoryMessages: ${convId}, ${convType}, ${params}` - ); - let r: any = await Native._callMethod(MTfetchHistoryMessages, { - [MTfetchHistoryMessages]: { - convId: convId, - convType: convType as number, - pageSize: params.pageSize ?? 20, - startMsgId: params.startMsgId ?? '', - direction: - params.direction === ChatSearchDirection.DOWN ? 'down' : 'up', - }, - }); - Native.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTfetchHistoryMessages].cursor, - list: r?.[MTfetchHistoryMessages].list, - opt: { - map: (param: any) => { - return new ChatMessage(param); - }, - }, - }); - return ret; - } - /** * 根据消息拉取参数配置从服务器分页获取指定会话的历史消息。 * * **注意** 调用该方法,如果会话对象不存在则创建。 * * @param convId 会话 ID。 - * @param chatType 会话类型。详见 {@link ChatConversationType}. + * @param convType 会话类型。详见 {@link ChatConversationType}。 * @param params - * - options: 查询历史消息的参数配置类。详见 {@link ChatFetchMessageOptions}. * - cursor: 查询的起始游标位置。 @@ -1217,52 +1084,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 从本地数据库获取指定会话中包含特定关键字的消息。 - * - * @param keywords 查询关键字。 - * @param timestamp 查询的起始消息 Unix 时间戳,单位为毫秒。该参数设置后,SDK 从指定时间戳的消息开始,按消息搜索方向获取。 如果该参数设置为负数,SDK 从当前时间开始搜索。 - * @param maxCount 每次获取的最大消息数。取值范围为 [1,400]。 - * @param from 单聊或群聊中的消息发送方的用户 ID。若设置为 `null` 或空字符串,SDK 将在整个会话中搜索消息。 - * @param direction 消息搜索方向。详见 {@link ChatSearchDirection}. - * - (Default) `ChatSearchDirection.UP`: 按照消息中的时间戳的逆序查询 - * - `ChatSearchDirection.DOWN`: 按照消息中的时间戳的正序查询。 - * @returns 消息列表(不包含查询起始时间戳对应的消息)。若未查找到任何消息,返回空列表。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - * - * @deprecated 2024-04-22. Use {@link getMsgsWithKeyword} instead. - */ - public async searchMsgFromDB( - keywords: string, - timestamp: number = -1, - maxCount: number = 20, - from: string = '', - direction: ChatSearchDirection = ChatSearchDirection.UP - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: searchMsgFromDB: ${keywords}, ${timestamp}, ${maxCount}, ${from}` - ); - let r: any = await Native._callMethod(MTsearchChatMsgFromDB, { - [MTsearchChatMsgFromDB]: { - keywords: keywords, - timestamp: timestamp, - maxCount: maxCount, - from: from, - direction: direction === ChatSearchDirection.UP ? 'up' : 'down', - }, - }); - Native.checkErrorFromResult(r); - let ret = new Array(0); - const rr: Array = r?.[MTsearchChatMsgFromDB]; - if (rr) { - rr.forEach((element) => { - ret.push(new ChatMessage(element)); - }); - } - return ret; - } - /** * 获取所有会话在一定时间内的会话中发送的消息。 * @@ -1372,41 +1193,78 @@ export class ChatManager extends BaseManager { } /** - * Uses the pagination to get read receipts for group messages from the server. + * 从本地数据库获取群组消息的已读回执。 + * + * **注意** + * + * - 该方法仅适用于群组消息。 + * - 每次调用最多可传入 20 条消息,且这些消息必须属于同一会话。 + * + * @param msgIds 消息 ID 列表。 + * @returns 消息已读回执列表,详见 {@link ChatMessageReadReceipt}。 + * + * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 + */ + public async getGroupMessageReadReceipts( + msgIds: string[] + ): Promise> { + chatlog.log(`${ChatManager.TAG}: getGroupMessageReadReceipts: `, msgIds); + let r: any = await Native._callMethod(MTgetGroupMessageReadReceipts, { + [MTgetGroupMessageReadReceipts]: { + msg_ids: msgIds, + }, + }); + Native.checkErrorFromResult(r); + let ret = new Array(0); + const rr: Array = r?.[MTgetGroupMessageReadReceipts]; + if (rr) { + rr.forEach((element) => { + ret.push(new ChatMessageReadReceipt(element)); + }); + } + return ret; + } + + /** + * 分页从服务端获取群组消息的已读回执。 * - * 发送群组消息已读回执见 {@link {@link #sendConversationReadAck(String)}。 + * 有关如何发送群组消息的已读回执,详见 {@link sendMessageReadReceipts}。 * * @param msgId 消息 ID。 - * @param startAckId 开始的已读回执 ID。如果该参数设置为空字符串或 `null`,从服务器接收已读回执时间的倒序开始获取。 - * @param pageSize 每页期望获取群消息已读回执的条数。 - * @returns 已读回执列表和用于下次查询的 cursor。 + * @param groupId 群组 ID。 + * @param cursor 查询起始的已读回执 ID。如果该参数为 `undefined` 或空字符串,SDK 按服务端收到已读回执时间的倒序,从最新一条开始获取。 + * @param pageSize 每页期望获取的群组消息已读回执数量。 + * @returns 获取到的已读回执列表(不包含起始 ID 对应的回执)以及下次查询的游标,详见 {@link ChatCursorResult}。 + * + * **注意** 返回结果中的 `totalCount` 字段仅在 iOS 平台可用。 * * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ - public async fetchGroupAcks( + public async fetchGroupMessageReadReceipts( msgId: string, groupId: string, - startAckId: string, - pageSize: number = 0 - ): Promise> { + cursor?: string, + pageSize?: number + ): Promise> { chatlog.log( - `${ChatManager.TAG}: asyncFetchGroupAcks: ${msgId}, ${startAckId}, ${pageSize}` + `${ChatManager.TAG}: fetchGroupMessageReadReceipts: ${msgId}, ${groupId}, ${cursor}, ${pageSize}` ); - let r: any = await Native._callMethod(MTasyncFetchGroupAcks, { - [MTasyncFetchGroupAcks]: { + let r: any = await Native._callMethod(MTfetchGroupMessageReadReceipts, { + [MTfetchGroupMessageReadReceipts]: { msg_id: msgId, - ack_id: startAckId, - pageSize: pageSize, group_id: groupId, + receipt_id: cursor ?? '', + pageSize: pageSize ?? 20, }, }); Native.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTasyncFetchGroupAcks].cursor, - list: r?.[MTasyncFetchGroupAcks].list, + let ret = new ChatCursorResult({ + cursor: r?.[MTfetchGroupMessageReadReceipts].cursor, + list: r?.[MTfetchGroupMessageReadReceipts].list, + totalCount: r?.[MTfetchGroupMessageReadReceipts].totalCount, opt: { map: (param: any) => { - return new ChatGroupMessageAck(param); + return new ChatGroupReadReceipt(param); }, }, }); @@ -1523,27 +1381,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 从服务器获取会话列表。 - * - * @returns 会话列表。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async fetchAllConversations(): Promise> { - chatlog.log(`${ChatManager.TAG}: fetchAllConversations:`); - let r: any = await Native._callMethod(MTgetConversationsFromServer); - Native.checkErrorFromResult(r); - let ret = new Array(0); - const rr: Array = r?.[MTgetConversationsFromServer]; - if (rr) { - rr.forEach((element) => { - ret.push(new ChatConversation(element)); - }); - } - return ret; - } - /** * 删除指定会话及其本地历史消息。 * @@ -1574,22 +1411,48 @@ export class ChatManager extends BaseManager { } /** - * 从会话中获取最新消息。 + * 从本地数据库批量删除会话。 + * + * @param convIds 要删除的会话 ID 列表。不存在的会话 ID 会被忽略。 + * @param deleteMessages 是否删除会话中的本地历史消息。 + * - (默认)`true`:是。 + * - `false`:否。 + * + * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 + */ + public async deleteConversations( + convIds: Array, + deleteMessages: boolean = true + ): Promise { + chatlog.log( + `${ChatManager.TAG}: deleteConversations: ${convIds}, ${deleteMessages}` + ); + let r: any = await Native._callMethod(MTdeleteConversations, { + [MTdeleteConversations]: { + convIds: convIds, + deleteMessages: deleteMessages, + }, + }); + Native.checkErrorFromResult(r); + } + + /** + * 获取会话中的最新消息。 * * **注意** * * 该操作不会改变未读消息数。 - * 如果会话对象不存在,此方法将创建它。 + * 如果会话对象不存在,此方法会创建该对象。 * - * SDK首先从内存中获取最新消息。 如果没有找到消息,SDK会从本地数据库中加载消息,然后放入内存中。 + * SDK 首先从内存中获取最新消息。如果未找到消息,SDK 会从本地数据库加载消息并将其放入内存。 * * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param isChatThread 会话是否为子区会话。 + * @param convType 会话类型,详见 {@link ChatConversationType}。 + * @param isChatThread 是否为子区会话。 * - * @returns 消息实例。 如果消息不存在,SDK 将返回 `undefined`。 + * @returns 消息实例。如果消息不存在,SDK 返回 `undefined`。 * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 + * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 */ public async getLatestMessage( convId: string, @@ -1728,75 +1591,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 将消息标记为已读。 - * - * **注意** 如果会话对象不存在,此方法将创建它。 - * - * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param isChatThread 会话是否为子区会话。 - * - * @param msgId 消息 ID。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async markMessageAsRead( - convId: string, - convType: ChatConversationType, - msgId: string, - isChatThread: boolean = false - ): Promise { - chatlog.log( - `${ChatManager.TAG}: markMessageAsRead: `, - convId, - convType, - msgId, - isChatThread - ); - let r: any = await Native._callMethod(MTmarkMessageAsRead, { - [MTmarkMessageAsRead]: { - convId: convId, - convType: convType, - msg_id: msgId, - isChatThread: isChatThread, - }, - }); - ChatManager.checkErrorFromResult(r); - } - - /** - * 将所有消息标记为已读。 - * - * **注意** 如果会话对象不存在,此方法将创建它。 - * - * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param isChatThread 会话是否为子区会话。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async markAllMessagesAsRead( - convId: string, - convType: ChatConversationType, - isChatThread: boolean = false - ): Promise { - chatlog.log( - `${ChatManager.TAG}: markAllMessagesAsRead: `, - convId, - convType, - isChatThread - ); - let r: any = await Native._callMethod(MTmarkAllMessagesAsRead, { - [MTmarkAllMessagesAsRead]: { - convId: convId, - convType: convType, - isChatThread: isChatThread, - }, - }); - ChatManager.checkErrorFromResult(r); - } - /** * 更新本地数据库中的消息。 * @@ -1968,81 +1762,13 @@ export class ChatManager extends BaseManager { * @param convId 会话 ID。 * @param convType 会话类型。 请参阅{@link ChatConversationType}。 * @param msgType 消息类型。 请参阅{@link ChatMessageType}。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 + * @param direction 消息搜索方向,详见 {@link ChatSearchDirection}。 * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 * @param timestamp 用于查询的消息中的起始 Unix 时间戳。 单位是毫秒。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 * 如果将此参数设置为负值,则SDK从当前时间开始,按照消息中时间戳的降序顺序检索消息。 * @param count 每次检索的最大消息数。 取值范围为[1,400]。 - * @param sender 用于检索的用户 ID 或组 ID。 通常,它是会话 ID。 - * @param isChatThread 会话是否为子区会话。 - * - * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。 如果没有获取到消息,则返回空列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - * - * @2023 年 7 月 24 日已弃用。 请改用 {@link getMsgsWithMsgType}。 - */ - public async getMessagesWithMsgType( - convId: string, - convType: ChatConversationType, - msgType: ChatMessageType, - direction: ChatSearchDirection = ChatSearchDirection.UP, - timestamp: number = -1, - count: number = 20, - sender?: string, - isChatThread: boolean = false - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: getMessagesWithMsgType: `, - convId, - convType, - msgType, - direction, - timestamp, - count, - sender, - isChatThread - ); - let r: any = await Native._callMethod(MTloadMsgWithMsgType, { - [MTloadMsgWithMsgType]: { - convId: convId, - convType: convType, - msg_type: msgType, - direction: direction === ChatSearchDirection.UP ? 'up' : 'down', - timestamp: timestamp, - count: count, - sender: sender ?? '', - isChatThread: isChatThread, - }, - }); - ChatManager.checkErrorFromResult(r); - const ret: ChatMessage[] = []; - const rr = r?.[MTloadMsgWithMsgType]; - if (rr) { - Object.entries(rr).forEach((value: [string, any]) => { - ret.push(new ChatMessage(value[1])); - }); - } - return ret; - } - - /** - * 从本地数据库中检索会话中某种类型的消息。 - * - * **注意** 如果会话对象不存在,此方法将创建它。 - * - * @params - - * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param msgType 消息类型。 请参阅{@link ChatMessageType}。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 - * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 - * @param timestamp 用于查询的消息中的起始 Unix 时间戳。 单位是毫秒。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 - * 如果将此参数设置为负值,则SDK从当前时间开始,按照消息中时间戳的降序顺序检索消息。 - * @param count 每次检索的最大消息数。 取值范围为[1,400]。 - * @param sender 用于检索的用户 ID 或组 ID。 通常,它是会话 ID。 + * @param sender 消息发送者的用户 ID。若不设置该参数,SDK 在检索消息时忽略该条件。 * @param isChatThread 会话是否为子区会话。 * * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。 如果没有获取到消息,则返回空列表。 @@ -2111,80 +1837,17 @@ export class ChatManager extends BaseManager { * **注意** 如果会话对象不存在,此方法将创建它。 * * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param startMsgId 查询的起始消息ID。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 - * 如果该参数设置为空字符串,则SDK按照消息搜索方向检索消息,而忽略该参数。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 + * @param convType 会话类型,详见 {@link ChatConversationType}。 + * @param startMsgId 查询的起始消息 ID。设置该参数后,SDK 按消息搜索方向从指定消息开始检索;若设置为空字符串,则忽略该参数。 + * @param direction 消息搜索方向,详见 {@link ChatSearchDirection}。 * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 - * @param loadCount 每次检索的最大消息数。 取值范围为[1,50]。 + * @param loadCount 每次检索的最大消息数,取值范围为 [1,50]。 * @param isChatThread 会话是否为子区会话。 * * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。 如果没有获取到消息,则返回空列表。 * * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - * - * @2023 年 7 月 24 日已弃用。 请改用 {@link getMsgs}。 - */ - public async getMessages( - convId: string, - convType: ChatConversationType, - startMsgId: string, - direction: ChatSearchDirection = ChatSearchDirection.UP, - loadCount: number = 20, - isChatThread: boolean = false - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: getMessages: `, - convId, - convType, - startMsgId, - direction, - loadCount, - isChatThread - ); - let r: any = await Native._callMethod(MTloadMsgWithStartId, { - [MTloadMsgWithStartId]: { - convId: convId, - convType: convType, - direction: direction === ChatSearchDirection.UP ? 'up' : 'down', - startId: startMsgId, - count: loadCount, - isChatThread: isChatThread, - }, - }); - ChatManager.checkErrorFromResult(r); - const ret: ChatMessage[] = []; - const rr = r?.[MTloadMsgWithStartId]; - if (rr) { - Object.entries(rr).forEach((value: [string, any]) => { - ret.push(new ChatMessage(value[1])); - }); - } - return ret; - } - - /** - * 从本地数据库中检索会话中指定数量的消息。 - * - * 检索到的消息也会根据其中包含的时间戳放入内存中的会话中。 - * - * **注意** 如果会话对象不存在,此方法将创建它。 - * - * @params - - * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param startMsgId 查询的起始消息ID。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 - * 如果该参数设置为空字符串,则SDK按照消息搜索方向检索消息,而忽略该参数。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 - * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 - * @param loadCount 每次检索的最大消息数。 取值范围为[1,50]。 - * @param isChatThread 会话是否为子区会话。 - * - * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。如果没有获取到消息,则返回空列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 */ public async getMsgs(params: { convId: string; @@ -2232,75 +1895,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 获取指定用户在一定时间内的会话中发送的消息。 - * - * 该方法从本地数据库获取数据。 - * - * **注意** 如果会话对象不存在,此方法将创建它。 - * - * @param convId 会话 ID。 - * @param convType 会话类型。请参阅 {@link ChatConversationType}。 - * @param keywords 查询的关键字。 - * @param Direction 消息搜索方向。请参阅 {@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 - * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 - * @param timestamp 用于查询的消息中的起始 Unix 时间戳。单位是毫秒。设置该参数后,SDK 按照消息搜索方向,从指定的消息开始检索消息。 - * 如果将此参数设置为负值,则 SDK 从当前时间开始,按照消息中时间戳的降序顺序检索消息。 - * @param count 每次检索的最大消息数。取值范围为 [1,400]。 - * @param sender 用于检索的用户 ID 或群组 ID。 - * @param isChatThread 会话是否是子区会话。 - * - * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。如果没有获取到消息,则返回空列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - * - * @deprecated 2023-07-24 此方法已弃用。 请改用 {@link getMsgsWithKeyword}。 - */ - public async getMessagesWithKeyword( - convId: string, - convType: ChatConversationType, - keywords: string, - direction: ChatSearchDirection = ChatSearchDirection.UP, - timestamp: number = -1, - count: number = 20, - sender?: string, - isChatThread: boolean = false - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: getMessagesWithKeyword: `, - convId, - convType, - keywords, - direction, - timestamp, - count, - sender, - isChatThread - ); - let r: any = await Native._callMethod(MTloadMsgWithKeywords, { - [MTloadMsgWithKeywords]: { - convId: convId, - convType: convType, - keywords: keywords, - direction: direction === ChatSearchDirection.UP ? 'up' : 'down', - timestamp: timestamp, - count: count, - sender: sender, - isChatThread: isChatThread, - }, - }); - ChatManager.checkErrorFromResult(r); - const ret: ChatMessage[] = []; - const rr = r?.[MTloadMsgWithKeywords]; - if (rr) { - Object.entries(rr).forEach((value: [string, any]) => { - ret.push(new ChatMessage(value[1])); - }); - } - return ret; - } - /** * 获取指定用户在一定时间内的会话中发送的消息。 * @@ -2333,7 +1927,6 @@ export class ChatManager extends BaseManager { direction?: ChatSearchDirection; timestamp?: number; count?: number; - sender?: string; senders?: Array; searchScope?: ChatMessageSearchScope; isChatThread?: boolean; @@ -2345,7 +1938,6 @@ export class ChatManager extends BaseManager { direction = ChatSearchDirection.UP, timestamp = -1, count = 20, - sender, senders, searchScope = ChatMessageSearchScope.All, isChatThread = false, @@ -2359,7 +1951,6 @@ export class ChatManager extends BaseManager { timestamp, count, searchScope, - sender, senders, isChatThread ); @@ -2371,7 +1962,6 @@ export class ChatManager extends BaseManager { direction: direction === ChatSearchDirection.UP ? 'up' : 'down', timestamp: timestamp, count: count, - sender: sender, senders: senders, searchScope: searchScope, isChatThread: isChatThread, @@ -2388,68 +1978,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 检索本地数据库中某个会话在一定时间内发送和接收的消息。 - * - * **注意** 如果会话对象不存在,此方法将创建它。 - * - * @param convId 会话 ID。 - * @param convType 会话类型。 请参阅{@link ChatConversationType}。 - * @param startTime 查询的起始 Unix 时间戳,以毫秒为单位。 - * @param endTime 查询的结束 Unix 时间戳,以毫秒为单位。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 - * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 - * @param count 每次检索的最大消息数。 取值范围为[1,400]。 - * @param isChatThread 会话是否为子区会话。 - * - * @returns 检索到的消息列表(不包括具有开始或结束时间戳的消息)。 如果没有获取到消息,则返回空列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - * - * @deprecated 2023-07-24 此方法已弃用。 请改用 {@link getMsgWithTimestamp}。 - */ - public async getMessageWithTimestamp( - convId: string, - convType: ChatConversationType, - startTime: number, - endTime: number, - direction: ChatSearchDirection = ChatSearchDirection.UP, - count: number = 20, - isChatThread: boolean = false - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: getMessageWithTimestamp: `, - convId, - convType, - startTime, - endTime, - direction, - count, - isChatThread - ); - let r: any = await Native._callMethod(MTloadMsgWithTime, { - [MTloadMsgWithTime]: { - convId: convId, - convType: convType, - startTime: startTime, - endTime: endTime, - direction: direction === ChatSearchDirection.UP ? 'up' : 'down', - count: count, - isChatThread: isChatThread, - }, - }); - ChatManager.checkErrorFromResult(r); - const ret: ChatMessage[] = []; - const rr = r?.[MTloadMsgWithTime]; - if (rr) { - Object.entries(rr).forEach((value: [string, any]) => { - ret.push(new ChatMessage(value[1])); - }); - } - return ret; - } - /** * 检索本地数据库中某个会话在一定时间内发送和接收的消息。 * @@ -2460,7 +1988,7 @@ export class ChatManager extends BaseManager { * @param convType 会话类型。 请参阅{@link ChatConversationType}。 * @param startTime 查询的起始 Unix 时间戳,以毫秒为单位。 * @param endTime 查询的结束 Unix 时间戳,以毫秒为单位。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 + * @param direction 消息搜索方向。请参阅 {@link ChatSearchDirection}。 * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 * @param count 每次检索的最大消息数。 取值范围为[1,400]。 @@ -2729,31 +2257,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 举报消息。 - * - * @param msgId 要举报的消息 ID。 - * @param tag 非法消息的标签。你需要填写自定义标签,例如`涉政`或`广告`。 - * @param reason 举报原因。你需要自行填写举报原因。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async reportMessage( - msgId: string, - tag: string, - reason: string - ): Promise { - chatlog.log(`${ChatManager.TAG}: reportMessage: `, msgId, tag, reason); - let r: any = await Native._callMethod(MTreportMessage, { - [MTreportMessage]: { - msgId, - tag, - reason, - }, - }); - ChatManager.checkErrorFromResult(r); - } - /** * 获取指定消息的 Reaction 列表。 * @@ -2781,28 +2284,6 @@ export class ChatManager extends BaseManager { return ret; } - /** - * 获取群组消息的已读人数。 - * - * @param msgId 消息 ID。 - * @returns 若调用成功,返回群组消息的已读人数;失败则抛出异常。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async groupAckCount(msgId: string): Promise { - chatlog.log(`${ChatManager.TAG}: groupAckCount: `, msgId); - let r: any = await Native._callMethod(MTgroupAckCount, { - [MTgroupAckCount]: { - msgId, - }, - }); - ChatManager.checkErrorFromResult(r); - if (r?.[MTgroupAckCount] !== undefined) { - return r?.[MTgroupAckCount] as number; - } - return undefined; - } - /** * 创建子区。 * @@ -3273,44 +2754,6 @@ export class ChatManager extends BaseManager { return undefined; } - /** - * 从服务器分页获取会话列表。 - * - * @param pageSize 当前页码,从 1 开始。 - * @param pageNum 每页获取的会话数量,取值范围为 [1,20]。 - * @returns 当前用户的会话列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async fetchConversationsFromServerWithPage( - pageSize: number, - pageNum: number - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: fetchConversationsFromServerWithPage: `, - pageSize, - pageNum - ); - let r: any = await Native._callMethod( - MTfetchConversationsFromServerWithPage, - { - [MTfetchConversationsFromServerWithPage]: { - pageSize: pageSize, - pageNum: pageNum, - }, - } - ); - Native.checkErrorFromResult(r); - let ret = [] as ChatConversation[]; - const rr: Array = r?.[MTfetchConversationsFromServerWithPage]; - if (rr) { - rr.forEach((element) => { - ret.push(new ChatConversation(element)); - }); - } - return ret; - } - /** * 从会话中删除消息(从本地存储和服务器)。 * @@ -3344,7 +2787,7 @@ export class ChatManager extends BaseManager { ); throw new ChatError({ code: 1, description: 'msgIds count is 0' }); } - if ((await Factory.getChatClient().isLoginBefore()) === false) { + if ((await Factory.getChatClient().isConnected()) === false) { // todo: temp fix native console.log( `${ChatManager.TAG}: removeMessagesFromServerWithMsgIds: not logged in yet.` @@ -3398,7 +2841,7 @@ export class ChatManager extends BaseManager { ); throw new ChatError({ code: 1, description: 'timestamp <= 0' }); } - if ((await Factory.getChatClient().isLoginBefore()) === false) { + if ((await Factory.getChatClient().isConnected()) === false) { // todo: temp fix native console.log( `${ChatManager.TAG}: removeMessagesFromServerWithTimestamp: not logged in yet.` @@ -3416,132 +2859,6 @@ export class ChatManager extends BaseManager { Native.checkErrorFromResult(r); } - /** - * 分页从服务器获取会话列表。 - * - * SDK 按照会话活跃时间(会话的最后一条消息的时间戳)倒序返回会话列表。 - * - * 若会话中没有消息,则 SDK 按照会话创建时间的倒序返回会话列表。 - * - * @param cursor: 查询数据起始位置。如果为空字符串或者 `undefined`,SDK 从最新活跃的会话开始获取。 - * - * @param pageSize: 每页期望返回的会话数量。取值范围为 [1,50]。 - * - * @returns 会话列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async fetchConversationsFromServerWithCursor( - cursor?: string, - pageSize?: number - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: fetchConversationsFromServerWithCursor: ${cursor}, ${pageSize}` - ); - let r: any = await Native._callMethod( - MTgetConversationsFromServerWithCursor, - { - [MTgetConversationsFromServerWithCursor]: { - cursor: cursor ?? '', - pageSize: pageSize ?? 20, - }, - } - ); - Native.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTgetConversationsFromServerWithCursor].cursor, - list: r?.[MTgetConversationsFromServerWithCursor].list, - opt: { - map: (param: any) => { - return new ChatConversation(param); - }, - }, - }); - return ret; - } - - /** - * 分页从服务器获取置顶会话。 - * - * SDK 按照会话置顶时间倒序返回。 - * - * @param cursor: 查询数据起始位置。如果为空字符串或者 `undefined`,SDK 从最新置顶的会话开始查询。 - * @param pageSize: 请求最大会话数量。范围 [1,50]。 - * - * @returns 置顶的会话列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async fetchPinnedConversationsFromServerWithCursor( - cursor?: string, - pageSize?: number - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: fetchPinnedConversationsFromServerWithCursor: ${cursor}, ${pageSize}` - ); - let r: any = await Native._callMethod( - MTgetPinnedConversationsFromServerWithCursor, - { - [MTgetPinnedConversationsFromServerWithCursor]: { - cursor: cursor ?? '', - pageSize: pageSize ?? 20, - }, - } - ); - Native.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTgetPinnedConversationsFromServerWithCursor].cursor, - list: r?.[MTgetPinnedConversationsFromServerWithCursor].list, - opt: { - map: (param: any) => { - return new ChatConversation(param); - }, - }, - }); - return ret; - } - - /** - * 从本地数据库分页获取会话列表。 - * - * 调用该方法前,你需要将 {@link ChatOptions.autoLoadConversations} 设置为 `false`,因为开启自动加载会话后,SDK 会在初始化时把所有会话加载到内存,此时分页加载会话没有意义。 - * - * SDK 按以下顺序获取会话列表:先按置顶时间的倒序返回置顶会话,再按会话最新一条消息的服务器时间戳的倒序返回会话,时间戳相同的会话按会话 ID 的字母倒序(不区分大小写)返回。 - * - * @param cursor 查询数据起始位置。如果为空字符串或者 `undefined`,SDK 从最新的会话开始获取。 - * - * @param pageSize 每页期望返回的会话数量。取值范围为 [1,100]。 - * - * @returns 获取到的会话列表。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息。如果传入的 cursor 无效,SDK 会抛出错误码为 `INVALID_PARAM` 的 {@link ChatError}。详见 {@link ChatError}。 - */ - public async fetchConversationsFromDB( - cursor?: string, - pageSize?: number - ): Promise> { - chatlog.log( - `${ChatManager.TAG}: fetchConversationsFromDB: ${cursor}, ${pageSize}` - ); - let r: any = await Native._callMethod(MTgetConversationsFromDBWithCursor, { - [MTgetConversationsFromDBWithCursor]: { - cursor: cursor ?? '', - pageSize: pageSize ?? 20, - }, - }); - Native.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTgetConversationsFromDBWithCursor].cursor, - list: r?.[MTgetConversationsFromDBWithCursor].list, - opt: { - map: (param: any) => { - return new ChatConversation(param); - }, - }, - }); - return ret; - } - /** * 设置会话是否置顶。 * @@ -3566,45 +2883,6 @@ export class ChatManager extends BaseManager { Native.checkErrorFromResult(r); } - /** - * 修改文本消息。 - * - * 调用该方法修改消息内容后,本地和服务端的消息均会修改。 - * - * 调用该方法只能修改单聊和群聊中的文本消息,不能修改聊天室消息。 - * - * @param msgId 修改消息 ID。 - * @param body 文本消息 body。详见 {@link ChatTextMessageBody}。 - * - * @returns 修改后的消息。 详见 {@link ChatMessageBody}。 - * - * @throws 如果有异常会在此抛出,包括错误码和错误信息,详见 {@link ChatError}。 - */ - public async modifyMessageBody( - msgId: string, - body: ChatMessageBody - ): Promise { - chatlog.log( - `${ChatManager.TAG}: modifyMessageBody: ${msgId}, ${body.type}` - ); - if (body.type !== ChatMessageType.TXT) { - throw new ChatError({ - code: 1, - description: - 'Currently only text message content modification is supported.', - }); - } - let r: any = await Native._callMethod(MTmodifyMessage, { - [MTmodifyMessage]: { - msgId, - body, - }, - }); - Native.checkErrorFromResult(r); - const rr = r?.[MTmodifyMessage]; - return new ChatMessage(rr); - } - /** * 更新消息内容。本地和服务器都更新。 * @@ -3741,35 +3019,6 @@ export class ChatManager extends BaseManager { Native.checkErrorFromResult(r); } - /** - * 按会话过滤选项从服务端获取会话。 - * - * @param option 会话过滤选项。请参阅 {@link ChatConversationFetchOptions}。 - * - * @returns 获取的会话列表。请参阅 {@link ChatCursorResult}。 - */ - public async fetchConversationsByOptions( - option: ChatConversationFetchOptions - ): Promise> { - chatlog.log(`${ChatManager.TAG}: fetchConversationsByOptions: ${option}`); - let r: any = await Native._callMethod(MTfetchConversationsByOptions, { - [MTfetchConversationsByOptions]: { - ...option, - }, - }); - Native.checkErrorFromResult(r); - let ret = new ChatCursorResult({ - cursor: r?.[MTfetchConversationsByOptions].cursor, - list: r?.[MTfetchConversationsByOptions].list, - opt: { - map: (param: any) => { - return new ChatConversation(param); - }, - }, - }); - return ret; - } - /** * 清空所有会话及其消息。 * diff --git a/src/ChatPushManager.ts b/src/ChatPushManager.ts index 1581d57c..1e9af747 100644 --- a/src/ChatPushManager.ts +++ b/src/ChatPushManager.ts @@ -74,7 +74,7 @@ export class ChatPushManager extends Native { /** * 清除指定会话的消息推送设置。 * - * 清除消息推送设置后,该会话采用 app 的消息推送模式,详见 {@link EMPushManager.setSilentModeForAll(ChatSilentModeParam)}。 + * 清除消息推送设置后,该会话采用 app 的消息推送模式,详见 {@link ChatPushManager.setSilentModeForAll}。 * * @params 参数组。 * - convId: 会话 ID。 diff --git a/src/ChatRoomManager.ts b/src/ChatRoomManager.ts index 5de94604..21e7c5f8 100644 --- a/src/ChatRoomManager.ts +++ b/src/ChatRoomManager.ts @@ -8,8 +8,6 @@ import { MTchangeChatRoomOwner, MTchangeChatRoomSubject, MTchatRoomChange, - MTcreateChatRoom, - MTdestroyChatRoom, MTfetchChatRoomAllowListFromServer, MTfetchChatRoomAnnouncement, MTfetchChatRoomAttributes, @@ -105,21 +103,10 @@ export class ChatRoomManager extends Native { }); break; case 'onMuteListAdded': - { - const hasKV = params.muteKVs; - if (hasKV) { - listener.onMuteListAddedV2?.({ - roomId: params.roomId, - mutes: params.muteKVs, - }); - } else { - listener.onMuteListAdded?.({ - roomId: params.roomId, - mutes: params.mutes, - expireTime: params.expireTime, - }); - } - } + listener.onMuteListAddedV2?.({ + roomId: params.roomId, + mutes: params.muteKVs, + }); break; case 'onMuteListRemoved': listener.onMuteListRemoved?.({ @@ -167,7 +154,7 @@ export class ChatRoomManager extends Native { case 'onAllMemberMuteStateChanged': listener.onAllChatRoomMemberMuteStateChanged?.({ roomId: params.roomId, - isAllMuted: params.isAllMuted, + isAllMuted: params.isMuted, }); break; case 'onSpecificationChanged': @@ -232,25 +219,6 @@ export class ChatRoomManager extends Native { this._roomListeners.clear(); } - /** - * 加入聊天室。 - * - * 退出聊天室见: {@link leaveChatRoom}。 - * - * @param roomId 要加入的聊天室的 ID。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async joinChatRoom(roomId: string): Promise { - chatlog.log(`${ChatRoomManager.TAG}: joinChatRoom: ${roomId}`); - let r: any = await Native._callMethod(MTjoinChatRoom, { - [MTjoinChatRoom]: { - roomId: roomId, - }, - }); - ChatRoomManager.checkErrorFromResult(r); - } - /** * 加入聊天室。 * @@ -385,66 +353,6 @@ export class ChatRoomManager extends Native { return undefined; } - /** - * 创建聊天室。 - * - * @param subject 聊天室名称。 - * @param description 聊天室描述。 - * @param welcome 新成员加入时的欢迎消息。 - * @param members 被邀请加入的成员用户 ID 列表。 - * @param maxCount 聊天室最大人数。 - * @returns 聊天室实例。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async createChatRoom( - subject: string, - description?: string, - welcome?: string, - members?: Array, - maxCount: number = 300 - ): Promise { - chatlog.log( - `${ChatRoomManager.TAG}: createChatRoom: `, - subject, - description, - welcome, - members, - maxCount - ); - let r: any = await Native._callMethod(MTcreateChatRoom, { - [MTcreateChatRoom]: { - subject: subject, - desc: description, - welcomeMsg: welcome, - members: members, - maxUserCount: maxCount, - }, - }); - ChatRoomManager.checkErrorFromResult(r); - let ret: ChatRoom = new ChatRoom(r?.[MTcreateChatRoom]); - return ret; - } - - /** - * 解散聊天室。 - * - * 仅聊天室所有者有权限调用该方法。 - * - * @param roomId 聊天室 ID。 - * - * @throws 如果有异常会在这里抛出,包含错误码和错误描述,详见 {@link ChatError}。 - */ - public async destroyChatRoom(roomId: string): Promise { - chatlog.log(`${ChatRoomManager.TAG}: destroyChatRoom: `, roomId); - let r: any = await Native._callMethod(MTdestroyChatRoom, { - [MTdestroyChatRoom]: { - roomId, - }, - }); - ChatRoomManager.checkErrorFromResult(r); - } - /** * 修改聊天室名称。 * @@ -1090,11 +998,11 @@ export class ChatRoomManager extends Native { * - 10 个数字 0-9; * - “_”, “-”, “.”。 * - deleteWhenLeft: 当前成员退出聊天室是否自动删除该自定义属性。 - * - (Default)`true`:是 + * - (默认)`true`:是 * - `false`:否 * - overwrite: 是否覆盖其他成员设置的属性 key 相同的属性。 * - `true`:是 - * - (Default)`false`:否 + * - (默认)`false`:否 * * @returns 若某些属性设置失败,SDK 返回键值对(key-value)结构的属性集合,在每个键值对中 key 为属性 key,value 为失败原因。 * diff --git a/src/__internal__/Consts.ts b/src/__internal__/Consts.ts index b8757d2b..4fd891c3 100644 --- a/src/__internal__/Consts.ts +++ b/src/__internal__/Consts.ts @@ -4,23 +4,19 @@ /// ChatClient methods export const MTinit = 'init'; -export const MTcreateAccount = 'createAccount'; export const MTlogin = 'login'; export const MTlogout = 'logout'; export const MTchangeAppKey = 'changeAppKey'; export const MTchangeAppId = 'changeAppId'; -export const MTisLoggedInBefore = 'isLoggedInBefore'; -export const MTupdateCurrentUserNick = 'updateCurrentUserNick'; // deprecated 2026-05-21 -export const MTuploadLog = 'uploadLog'; export const MTcompressLogs = 'compressLogs'; export const MTkickDevice = 'kickDevice'; export const MTkickAllDevices = 'kickAllDevices'; export const MTgetLoggedInDevicesFromServer = 'getLoggedInDevicesFromServer'; export const MTgetCurrentUser = 'getCurrentUser'; export const MTgetToken = 'getToken'; -export const MTloginWithAgoraToken = 'loginWithAgoraToken'; export const MTisConnected = 'isConnected'; export const MTrenewToken = 'renewToken'; +export const MTbindDeviceToken = 'bindDeviceToken'; export const MTbindPushKitToken = 'bindPushKitToken'; export const MTunbindPushKitToken = 'unbindPushKitToken'; @@ -40,21 +36,13 @@ export const MTonMultiDeviceEventRemoveMessage = export const MTonMultiDeviceEventConversation = 'onMultiDeviceEventConversation'; -export const MTonUserDidLoginFromOtherDevice = 'onUserDidLoginFromOtherDevice'; // deprecated 2024-08-15 use onUserDidLoginFromOtherDeviceWithInfo instead -export const MTonUserDidLoginFromOtherDeviceWithInfo = - 'onUserDidLoginFromOtherDeviceWithInfo'; -export const MTonUserDidRemoveFromServer = 'onUserDidRemoveFromServer'; -export const MTonUserDidForbidByServer = 'onUserDidForbidByServer'; -export const MTonUserDidChangePassword = 'onUserDidChangePassword'; -export const MTonUserDidLoginTooManyDevice = 'onUserDidLoginTooManyDevice'; -export const MTonUserKickedByOtherDevice = 'onUserKickedByOtherDevice'; -export const MTonUserAuthenticationFailed = 'onUserAuthenticationFailed'; -export const MTonAppActiveNumberReachLimit = 'onAppActiveNumberReachLimit'; +export const MTonDataSyncStart = 'onDataSyncStart'; +export const MTonDataSyncFinish = 'onDataSyncFinish'; +export const MTonDatabaseOpened = 'onDatabaseOpened'; /// ChatContactManager methods export const MTaddContact = 'addContact'; export const MTdeleteContact = 'deleteContact'; -export const MTgetAllContactsFromServer = 'getAllContactsFromServer'; export const MTgetAllContactsFromDB = 'getAllContactsFromDB'; export const MTaddUserToBlockList = 'addUserToBlockList'; export const MTremoveUserFromBlockList = 'removeUserFromBlockList'; @@ -70,12 +58,15 @@ export const MTonContactChanged = 'onContactChanged'; /// ChatManager methods export const MTsendMessage = 'sendMessage'; export const MTresendMessage = 'resendMessage'; -export const MTackMessageRead = 'ackMessageRead'; -export const MTackGroupMessageRead = 'ackGroupMessageRead'; -export const MTackConversationRead = 'ackConversationRead'; +export const MTsendMessageReadReceipts = 'sendMessageReadReceipts'; +export const MTclearConversationUnreadMessageCount = + 'clearConversationUnreadMessageCount'; +export const MTclearAllConversationUnreadMessageCount = + 'clearAllConversationUnreadMessageCount'; +export const MTgetGroupMessageReadReceipts = 'getGroupMessageReadReceipts'; +export const MTfetchGroupMessageReadReceipts = 'fetchGroupMessageReadReceipts'; export const MTrecallMessage = 'recallMessage'; export const MTgetConversation = 'getConversation'; -export const MTmarkAllChatMsgAsRead = 'markAllChatMsgAsRead'; export const MTgetUnreadMessageCount = 'getUnreadMessageCount'; export const MTupdateChatMessage = 'updateChatMessage'; export const MTdownloadAttachmentInCombine = 'downloadAttachmentInCombine'; @@ -87,15 +78,13 @@ export const MTvoiceMessageToText = 'voiceMessageToText'; export const MTvoiceFileToText = 'voiceFileToText'; export const MTimportMessages = 'importMessages'; export const MTloadAllConversations = 'loadAllConversations'; -export const MTgetConversationsFromServer = 'getConversationsFromServer'; // deprecated. 2023-07-24 export const MTdeleteConversation = 'deleteConversation'; -export const MTfetchHistoryMessages = 'fetchHistoryMessages'; +export const MTdeleteConversations = 'deleteConversations'; export const MTfetchHistoryMessagesByOptions = 'fetchHistoryMessagesByOptions'; export const MTsearchChatMsgFromDB = 'searchChatMsgFromDB'; export const MTgetConvsMsgsWithKeyword = 'getConvsMsgsWithKeyword'; export const MTgetMessage = 'getMessage'; export const MTgetMessagesWithIds = 'getMessagesWithIds'; -export const MTasyncFetchGroupAcks = 'asyncFetchGroupAcks'; export const MTdeleteRemoteConversation = 'deleteRemoteConversation'; export const MTdeleteMessagesBeforeTimestamp = 'deleteMessagesBeforeTimestamp'; @@ -106,23 +95,13 @@ export const MTaddReaction = 'addReaction'; export const MTremoveReaction = 'removeReaction'; export const MTfetchReactionList = 'fetchReactionList'; export const MTfetchReactionDetail = 'fetchReactionDetail'; -export const MTreportMessage = 'reportMessage'; -export const MTfetchConversationsFromServerWithPage = - 'fetchConversationsFromServerWithPage'; export const MTremoveMessagesFromServerWithMsgIds = 'removeMessagesFromServerWithMsgIds'; export const MTremoveMessagesFromServerWithTs = 'removeMessagesFromServerWithTs'; -export const MTgetConversationsFromServerWithCursor = - 'getConversationsFromServerWithCursor'; -export const MTgetPinnedConversationsFromServerWithCursor = - 'getPinnedConversationsFromServerWithCursor'; -export const MTgetConversationsFromDBWithCursor = - 'getConversationsFromDBWithCursor'; export const MTpinConversation = 'pinConversation'; -export const MTmodifyMessage = 'modifyMessage'; export const MTmodifyMsgBody = 'modifyMsgBody'; export const MTdownloadAndParseCombineMessage = 'downloadAndParseCombineMessage'; @@ -130,26 +109,18 @@ export const MTdownloadAndParseCombineMessage = /// ChatManager listener export const MTonMessagesReceived = 'onMessagesReceived'; export const MTonCmdMessagesReceived = 'onCmdMessagesReceived'; -export const MTonMessagesRead = 'onMessagesRead'; -export const MTonGroupMessageRead = 'onGroupMessageRead'; +export const MTonMessageReadReceipts = 'onMessageReadReceipts'; export const MTonMessagesDelivered = 'onMessagesDelivered'; -export const MTonMessagesRecalled = 'onMessagesRecalled'; // deprecated 2024-05-23 export const MTonMessagesRecalledInfo = 'onMessagesRecalledInfo'; export const MTonConversationUpdate = 'onConversationUpdate'; -export const MTonConversationHasRead = 'onConversationHasRead'; -export const MTonReadAckForGroupMessageUpdated = - 'onReadAckForGroupMessageUpdated'; export const MTmessageReactionDidChange = 'messageReactionDidChange'; export const MTonMessageContentChanged = 'onMessageContentChanged'; export const MTonMessageProgressUpdate = 'onMessageProgressUpdate'; export const MTonMessageError = 'onMessageError'; export const MTonMessageSuccess = 'onMessageSuccess'; -export const MTonMessageReadAck = 'onMessageReadAck'; // deprecated 2026-08-20 use onMessagesRead instead -export const MTonMessageDeliveryAck = 'onMessageDeliveryAck'; // deprecated 2026-08-20 use onMessagesDelivered instead -export const MTonMessageStatusChanged = 'onMessageStatusChanged'; // deprecated 2022.05.05 export const MTonChatThreadCreated = 'onChatThreadCreated'; export const MTonChatThreadUpdated = 'onChatThreadUpdated'; @@ -159,20 +130,15 @@ export const MTonChatThreadUserRemoved = 'onChatThreadUserRemoved'; /// ChatConversionManager methods export const MTgetUnreadMsgCount = 'getUnreadMsgCount'; export const MTgetMsgCount = 'getMsgCount'; -export const MTmarkAllMessagesAsRead = 'markAllMessagesAsRead'; -export const MTmarkMessageAsRead = 'markMessageAsRead'; export const MTsyncConversationExt = 'syncConversationExt'; -export const MTsyncConversationName = 'syncConversationName'; // deprecated 2022.05.05 export const MTremoveMessage = 'removeMessage'; export const MTgetLatestMessage = 'getLatestMessage'; export const MTgetLatestMessageFromOthers = 'getLatestMessageFromOthers'; export const MTclearAllMessages = 'clearAllMessages'; export const MTdeleteMessagesWithTs = 'deleteMessagesWithTs'; export const MTinsertMessage = 'insertMessage'; -export const MTappendMessage = 'appendMessage'; export const MTupdateConversationMessage = 'updateConversationMessage'; -export const MTloadMsgWithId = 'loadMsgWithId'; export const MTloadMsgWithStartId = 'loadMsgWithStartId'; export const MTloadMsgWithKeywords = 'loadMsgWithKeywords'; export const MTloadMsgWithMsgType = 'loadMsgWithMsgType'; @@ -180,7 +146,6 @@ export const MTloadMsgWithTime = 'loadMsgWithTime'; // ChatMessage methods export const MTgetReactionList = 'getReactionList'; -export const MTgroupAckCount = 'groupAckCount'; // ChatRoomManager methods export const MTjoinChatRoom = 'joinChatRoom'; @@ -189,9 +154,6 @@ export const MTfetchPublicChatRoomsFromServer = 'fetchPublicChatRoomsFromServer'; export const MTfetchChatRoomInfoFromServer = 'fetchChatRoomInfoFromServer'; export const MTgetChatRoom = 'getChatRoom'; -export const MTgetAllChatRooms = 'getAllChatRooms'; -export const MTcreateChatRoom = 'createChatRoom'; -export const MTdestroyChatRoom = 'destroyChatRoom'; export const MTchangeChatRoomSubject = 'changeChatRoomSubject'; export const MTchangeChatRoomDescription = 'changeChatRoomDescription'; export const MTfetchChatRoomMembers = 'fetchChatRoomMembers'; @@ -222,7 +184,6 @@ export const MTmuteAllChatRoomMembers = 'muteAllChatRoomMembers'; export const MTunMuteAllChatRoomMembers = 'unMuteAllChatRoomMembers'; export const MTfetchChatRoomAttributes = 'fetchChatRoomAttributes'; -export const MTfetchChatRoomAllAttributes = 'fetchChatRoomAllAttributes'; export const MTsetChatRoomAttributes = 'setChatRoomAttributes'; export const MTremoveChatRoomAttributes = 'removeChatRoomAttributes'; @@ -232,11 +193,8 @@ export const MTchatRoomChange = 'onChatRoomChanged'; /// ChatGroupManager methods export const MTgetGroupWithId = 'getGroupWithId'; export const MTgetJoinedGroups = 'getJoinedGroups'; -export const MTgetGroupsWithoutPushNotification = - 'getGroupsWithoutPushNotification'; -export const MTgetJoinedGroupsFromServer = 'getJoinedGroupsFromServer'; -export const MTgetPublicGroupsFromServer = 'getPublicGroupsFromServer'; export const MTcreateGroup = 'createGroup'; +export const MTupdateGroupConfigs = 'updateGroupConfigs'; export const MTgetGroupSpecificationFromServer = 'getGroupSpecificationFromServer'; export const MTgetGroupMemberListFromServer = 'getGroupMemberListFromServer'; @@ -292,29 +250,10 @@ export const MTgetGroupNamecard = 'getGroupNamecard'; export const MTonGroupChanged = 'onGroupChanged'; /// ChatPushManager methods -export const MTgetImPushConfig = 'getImPushConfig'; export const MTgetImPushConfigFromServer = 'getImPushConfigFromServer'; export const MTupdatePushNickname = 'updatePushNickname'; -export const MTenableOfflinePush = 'enableOfflinePush'; -export const MTdisableOfflinePush = 'disableOfflinePush'; -export const MTgetNoPushGroups = 'getNoPushGroups'; -export const MTsetNoDisturbUsers = 'setNoDisturbUsers'; // deprecated 2022.05.04 -export const MTgetNoDisturbUsersFromServer = 'getNoDisturbUsersFromServer'; // deprecated 2022.05.04 - -export const MTupdateHMSPushToken = 'updateHMSPushToken'; // deprecated 2026-05-21 -export const MTupdateFCMPushToken = 'updateFCMPushToken'; // deprecated 2026-05-21 -export const MTupdateAPNsPushToken = 'updateAPNsPushToken'; // deprecated 2026-05-21 - -export const MTimPushNoDisturb = 'imPushNoDisturb'; // deprecated 2022.05.04 export const MTupdateImPushStyle = 'updateImPushStyle'; -export const MTupdateGroupPushService = 'updateGroupPushService'; -export const MTgetNoDisturbGroups = 'getNoDisturbGroups'; // deprecated 2022.05.04 -export const MTupdateUserPushService = 'updateUserPushService'; -export const MTgetNoPushUsers = 'getNoPushUsers'; - -export const MTupdatePushConfig = 'updatePushConfig'; -export const MTreportPushAction = 'reportPushAction'; export const MTsetConversationSilentMode = 'setConversationSilentMode'; export const MTremoveConversationSilentMode = 'removeConversationSilentMode'; export const MTfetchConversationSilentMode = 'fetchConversationSilentMode'; @@ -331,9 +270,7 @@ export const MTgetPushTemplate = 'getPushTemplate'; /// ChatUserInfoManager methods export const MTupdateOwnUserInfo = 'updateOwnUserInfo'; -export const MTupdateOwnUserInfoWithType = 'updateOwnUserInfoWithType'; export const MTfetchUserInfoById = 'fetchUserInfoById'; -export const MTfetchUserInfoByIdWithType = 'fetchUserInfoByIdWithType'; export const MTgetLocalUserInfoByIds = 'getLocalUserInfoByIds'; export const MTsubscribeUsersInfo = 'subscribeUsersInfo'; export const MTunsubscribeUsersInfo = 'unsubscribeUsersInfo'; @@ -375,8 +312,6 @@ export const MTgetThreadConversation = 'getThreadConversation'; export const MTgetAllContacts = 'getAllContacts'; export const MTsetContactRemark = 'setContactRemark'; export const MTgetContact = 'getContact'; -export const MTfetchAllContacts = 'fetchAllContacts'; -export const MTfetchContacts = 'fetchContacts'; export const MTfetchJoinedGroupCount = 'fetchJoinedGroupCount'; // 2024-04-17 added @@ -387,7 +322,6 @@ export const MTaddRemoteAndLocalConversationsMark = 'addRemoteAndLocalConversationsMark'; export const MTdeleteRemoteAndLocalConversationsMark = 'deleteRemoteAndLocalConversationsMark'; -export const MTfetchConversationsByOptions = 'fetchConversationsByOptions'; export const MTdeleteAllMessageAndConversation = 'deleteAllMessageAndConversation'; export const MTpinMessage = 'pinMessage'; diff --git a/src/__tests__/contract/eventWiring.test.ts b/src/__tests__/contract/eventWiring.test.ts index 245c3501..25e36dc0 100644 --- a/src/__tests__/contract/eventWiring.test.ts +++ b/src/__tests__/contract/eventWiring.test.ts @@ -19,10 +19,9 @@ import { /** * Events that are intentionally defined as consts but not wired in TS. * Currently empty: the legacy ack events `MTonMessageReadAck` / - * `MTonMessageDeliveryAck` used to live here and were marked deprecated on - * 2026-08-20 in all three languages (TS / Java / ObjC), which excludes them - * from this check. Prefer deprecating an event over allowlisting it; extend - * this list only with a documented reason. + * `MTonMessageDeliveryAck` used to live here and were removed in 5.0.0 + * together with the other deprecated event consts. Prefer deprecating an + * event over allowlisting it; extend this list only with a documented reason. */ const UNWIRED_EVENT_ALLOWLIST = new Set([]); diff --git a/src/__tests__/contract/methodNames.test.ts b/src/__tests__/contract/methodNames.test.ts index a2d7778f..e43327de 100644 --- a/src/__tests__/contract/methodNames.test.ts +++ b/src/__tests__/contract/methodNames.test.ts @@ -97,25 +97,6 @@ describe('contract: method-name parity (TS ↔ Java ↔ ObjC)', () => { } }); - test('every TS MTon* event value appears in RN supportedEvents resolved list', () => { - const eventSet = new Set(rnEventValues); - const tsEvents = tsConsts.filter((c) => c.symbol.startsWith('on')); - const missing: { symbol: string; value: string }[] = []; - for (const c of tsEvents) { - if (!eventSet.has(c.value)) { - missing.push({ symbol: c.symbol, value: c.value }); - } - } - if (missing.length > 0) { - const lines = missing - .map((m) => ` MT${m.symbol} = '${m.value}'`) - .join('\n'); - throw new Error( - `TS MTon* events missing from RN supportedEvents (${missing.length} of ${tsEvents.length}):\n${lines}` - ); - } - }); - test('every non-event ObjC methodMap entry has a dispatch case', () => { // Keys whose value starts with `on` are events emitted by native code: // they travel through onReceive: and are never invoked from TS, so they @@ -140,4 +121,23 @@ describe('contract: method-name parity (TS ↔ Java ↔ ObjC)', () => { ); } }); + + test('every TS MTon* event value appears in RN supportedEvents resolved list', () => { + const eventSet = new Set(rnEventValues); + const tsEvents = tsConsts.filter((c) => c.symbol.startsWith('on')); + const missing: { symbol: string; value: string }[] = []; + for (const c of tsEvents) { + if (!eventSet.has(c.value)) { + missing.push({ symbol: c.symbol, value: c.value }); + } + } + if (missing.length > 0) { + const lines = missing + .map((m) => ` MT${m.symbol} = '${m.value}'`) + .join('\n'); + throw new Error( + `TS MTon* events missing from RN supportedEvents (${missing.length} of ${tsEvents.length}):\n${lines}` + ); + } + }); }); diff --git a/src/__tests__/unit/ChatClient.events.test.ts b/src/__tests__/unit/ChatClient.events.test.ts index b7e33a25..c9713e67 100644 --- a/src/__tests__/unit/ChatClient.events.test.ts +++ b/src/__tests__/unit/ChatClient.events.test.ts @@ -73,6 +73,49 @@ describe('ChatClient event dispatch', () => { expect(listener.onDisconnected).not.toHaveBeenCalled(); }); + test('onDisconnected passes the error code through to the listeners', () => { + const listener: ChatConnectEventListener = { onDisconnected: jest.fn() }; + client.addConnectionListener(listener); + + emitNativeEvent(MTonDisconnected, { errorCode: 217 }); + + expect(listener.onDisconnected).toHaveBeenCalledWith(217, undefined); + }); + + test('onDisconnected delivers codes that are not listed in ChatDisconnectErrorCode unchanged', () => { + const listener: ChatConnectEventListener = { onDisconnected: jest.fn() }; + client.addConnectionListener(listener); + + emitNativeEvent(MTonDisconnected, { errorCode: 999 }); + + expect(listener.onDisconnected).toHaveBeenCalledWith(999, undefined); + }); + + test('onDisconnected wraps the device info only when present', () => { + const listener: ChatConnectEventListener = { onDisconnected: jest.fn() }; + client.addConnectionListener(listener); + + emitNativeEvent(MTonDisconnected, { + errorCode: 206, + deviceName: 'Pixel 8', + ext: '{"from":"other"}', + }); + + expect(listener.onDisconnected).toHaveBeenCalledWith(206, { + deviceName: 'Pixel 8', + ext: '{"from":"other"}', + }); + }); + + test('a network disconnection arrives without an error code', () => { + const listener: ChatConnectEventListener = { onDisconnected: jest.fn() }; + client.addConnectionListener(listener); + + emitNativeEvent(MTonDisconnected); + + expect(listener.onDisconnected).toHaveBeenCalledWith(undefined, undefined); + }); + test('removeAllConnectionListener clears every connection listener', () => { const l1: ChatConnectEventListener = { onTokenWillExpire: jest.fn() }; const l2: ChatConnectEventListener = { onTokenWillExpire: jest.fn() }; @@ -140,6 +183,41 @@ describe('ChatClient event dispatch', () => { ); }); + test('multi-device group update event routes to onGroupEvent', () => { + const listener: ChatMultiDeviceEventListener = { + onGroupEvent: jest.fn(), + }; + client.addMultiDeviceListener(listener); + + // 34 is iOS-only (`EMMultiDevicesEventGroupUpdate`). + emitNativeEvent(MTonMultiDeviceEvent, { + type: MTonMultiDeviceEventGroup, + event: 34, + target: 'g1', + ext: '', + }); + + expect(listener.onGroupEvent).toHaveBeenCalledWith( + ChatMultiDeviceEvent.GROUP_UPDATE, + 'g1', + '' + ); + }); + + test('multi-device group event values match the native enumerations', () => { + // The enum numbers its members implicitly, so inserting or reordering a + // member would silently shift the values the native bridge sends. + expect(ChatMultiDeviceEvent.GROUP_ADD_USER_ALLOW_LIST).toBe(30); + expect(ChatMultiDeviceEvent.GROUP_REMOVE_USER_ALLOW_LIST).toBe(31); + expect(ChatMultiDeviceEvent.GROUP_ALL_BAN).toBe(32); + expect(ChatMultiDeviceEvent.GROUP_REMOVE_ALL_BAN).toBe(33); + expect(ChatMultiDeviceEvent.GROUP_UPDATE).toBe(34); + expect(ChatMultiDeviceEvent.THREAD_CREATE).toBe(40); + expect(ChatMultiDeviceEvent.THREAD_UPDATE).toBe(44); + expect(ChatMultiDeviceEvent.THREAD_KICK).toBe(45); + expect(ChatMultiDeviceEvent.GROUP_METADATA_CHANGED).toBe(52); + }); + test('multi-device conversation event routes to onConversationEvent', () => { const listener: ChatMultiDeviceEventListener = { onConversationEvent: jest.fn(), diff --git a/src/__tests__/unit/ChatClient.test.ts b/src/__tests__/unit/ChatClient.test.ts index 087bf7cb..fedbb39a 100644 --- a/src/__tests__/unit/ChatClient.test.ts +++ b/src/__tests__/unit/ChatClient.test.ts @@ -29,7 +29,6 @@ describe('ChatClient call contracts', () => { const options = ChatOptions.withAppKey({ appKey: 'easemob#demo', - autoLogin: false, debugModel: true, }); @@ -51,34 +50,22 @@ describe('ChatClient call contracts', () => { }); }); - describe('login', () => { - test('calls MTlogin with { [MTlogin]: { username, pwdOrToken, isPassword } }', async () => { + describe('loginWithToken', () => { + test('calls MTlogin with { [MTlogin]: { username, pwdOrToken, isPassword: false } }', async () => { mockCallMethodOnce({}); - await client.login('alice', 'secret', true); + await client.loginWithToken('alice', 'token-1'); const last = getLastCall(); expect(last?.method).toBe(MTlogin); expect(last?.args).toEqual({ [MTlogin]: { username: 'alice', - pwdOrToken: 'secret', - isPassword: true, + pwdOrToken: 'token-1', + isPassword: false, }, }); }); - - test('defaults isPassword to true when omitted', async () => { - mockCallMethodOnce({}); - - await client.login('bob', 'pw'); - - const last = getLastCall(); - const inner = (last?.args as Record)[MTlogin]; - expect(inner.isPassword).toBe(true); - expect(inner.username).toBe('bob'); - expect(inner.pwdOrToken).toBe('pw'); - }); }); describe('logout', () => { diff --git a/src/__tests__/unit/ChatGroupManager.test.ts b/src/__tests__/unit/ChatGroupManager.test.ts index caa49fff..783e6fca 100644 --- a/src/__tests__/unit/ChatGroupManager.test.ts +++ b/src/__tests__/unit/ChatGroupManager.test.ts @@ -1,7 +1,7 @@ /** - * Contract test for `ChatGroupManager.fetchGroupInfoFromServer`: verifies - * the MT constant, the nested `{ groupId, fetchMembers }` payload shape, - * and that the native payload is decoded into a `ChatGroup` instance. + * Contract test for `ChatGroupManager.fetchGroupInfoWithoutMembersFromServer`: + * verifies the MT constant, the nested `{ groupId }` payload shape, and that + * the native payload is decoded into a `ChatGroup` instance. */ import { ChatGroupManager } from '../../ChatGroupManager'; @@ -13,7 +13,7 @@ import { getLastCall, mockCallMethodOnce } from '../helpers/nativeMock'; describe('ChatGroupManager call-method contract', () => { const manager = new ChatGroupManager(); - test('fetchGroupInfoFromServer calls MTgetGroupSpecificationFromServer with { groupId, fetchMembers } and decodes ChatGroup', async () => { + test('fetchGroupInfoWithoutMembersFromServer calls MTgetGroupSpecificationFromServer with { groupId } and decodes ChatGroup', async () => { mockCallMethodOnce({ [MTgetGroupSpecificationFromServer]: { groupId: 'g1', @@ -23,14 +23,13 @@ describe('ChatGroupManager call-method contract', () => { }, }); - const group = await manager.fetchGroupInfoFromServer('g1', true); + const group = await manager.fetchGroupInfoWithoutMembersFromServer('g1'); const last = getLastCall(); expect(last?.method).toBe(MTgetGroupSpecificationFromServer); expect(last?.args).toMatchObject({ [MTgetGroupSpecificationFromServer]: { groupId: 'g1', - fetchMembers: true, }, }); expect(group).toBeInstanceOf(ChatGroup); @@ -39,12 +38,12 @@ describe('ChatGroupManager call-method contract', () => { expect(group?.owner).toBe('alice'); }); - test('fetchGroupInfoFromServer rejects with ChatError when native returns { error }', async () => { + test('fetchGroupInfoWithoutMembersFromServer rejects with ChatError when native returns { error }', async () => { mockCallMethodOnce({ error: { code: 404, description: 'group not found' }, }); - const p = manager.fetchGroupInfoFromServer('g1', true); + const p = manager.fetchGroupInfoWithoutMembersFromServer('g1'); await expect(p).rejects.toBeInstanceOf(ChatError); await expect(p).rejects.toMatchObject({ diff --git a/src/__tests__/unit/ChatManager.events.test.ts b/src/__tests__/unit/ChatManager.events.test.ts index 87331af3..f0f9a9dc 100644 --- a/src/__tests__/unit/ChatManager.events.test.ts +++ b/src/__tests__/unit/ChatManager.events.test.ts @@ -9,10 +9,11 @@ import { eventEmitter } from '../../__specs__'; import { ChatManager } from '../../ChatManager'; import type { ChatMessageEventListener } from '../../ChatEvents'; import { ChatMessage, ChatTextMessageBody } from '../../common/ChatMessage'; +import { ChatMessageReadReceipt } from '../../common/ChatMessageReadReceipt'; import type { NativeEventEmitter } from 'react-native'; import { - MTonConversationHasRead, - MTonMessagesRead, + MTonMessageReadReceipts, + MTonMessagesDelivered, MTonMessagesReceived, MTonMessagesRecalledInfo, } from '../../__internal__/Consts'; @@ -83,25 +84,42 @@ describe('ChatManager event dispatch', () => { test('a removed message listener is no longer notified', () => { const listener: ChatMessageEventListener = { - onMessagesRead: jest.fn(), + onMessagesDelivered: jest.fn(), }; manager.addMessageListener(listener); manager.removeMessageListener(listener); - emitNativeEvent(MTonMessagesRead, [NATIVE_TEXT_MESSAGE]); + emitNativeEvent(MTonMessagesDelivered, [NATIVE_TEXT_MESSAGE]); - expect(listener.onMessagesRead).not.toHaveBeenCalled(); + expect(listener.onMessagesDelivered).not.toHaveBeenCalled(); }); - test('onConversationHasRead forwards from and to to onConversationRead', () => { + test('onMessageReadReceipts decodes receipts and fans out ChatMessageReadReceipt models', () => { const listener: ChatMessageEventListener = { - onConversationRead: jest.fn(), + onMessageReadReceipts: jest.fn(), }; manager.addMessageListener(listener); - emitNativeEvent(MTonConversationHasRead, { from: 'peer1', to: 'me' }); - - expect(listener.onConversationRead).toHaveBeenCalledWith('peer1', 'me'); + emitNativeEvent(MTonMessageReadReceipts, { + receipts: [ + { + msg_id: 'm1', + conv_id: 'peer1', + isPeerReceipt: true, + readCount: 3, + }, + ], + }); + + expect(listener.onMessageReadReceipts).toHaveBeenCalledTimes(1); + const list = (listener.onMessageReadReceipts as jest.Mock).mock + .calls[0]![0] as ChatMessageReadReceipt[]; + expect(list).toHaveLength(1); + expect(list[0]).toBeInstanceOf(ChatMessageReadReceipt); + expect(list[0]!.msgId).toBe('m1'); + expect(list[0]!.convId).toBe('peer1'); + expect(list[0]!.isPeerReceipt).toBe(true); + expect(list[0]!.readCount).toBe(3); }); test('onMessagesRecalledInfo decodes recalled info entries', () => { diff --git a/src/__tests__/unit/ChatRoomManager.test.ts b/src/__tests__/unit/ChatRoomManager.test.ts index a7823fcb..a2b8b985 100644 --- a/src/__tests__/unit/ChatRoomManager.test.ts +++ b/src/__tests__/unit/ChatRoomManager.test.ts @@ -1,7 +1,7 @@ /** - * Contract test for `ChatRoomManager.joinChatRoom`: verifies the MT - * constant, the nested `{ roomId }` payload shape, and that a - * void-returning method resolves on success without throwing. + * Contract test for `ChatRoomManager.joinChatRoomEx`: verifies the MT + * constant, the nested `{ roomId, exitOtherRoom, ext }` payload shape, and + * that a void-returning method resolves on success without throwing. */ import { ChatRoomManager } from '../../ChatRoomManager'; @@ -12,10 +12,10 @@ import { getLastCall, mockCallMethodOnce } from '../helpers/nativeMock'; describe('ChatRoomManager call-method contract', () => { const manager = new ChatRoomManager(); - test('joinChatRoom calls MTjoinChatRoom with { roomId } and resolves void', async () => { + test('joinChatRoomEx calls MTjoinChatRoom with { roomId } and resolves void', async () => { mockCallMethodOnce({}); - const result = await manager.joinChatRoom('room-42'); + const result = await manager.joinChatRoomEx({ roomId: 'room-42' }); const last = getLastCall(); expect(last?.method).toBe(MTjoinChatRoom); @@ -27,12 +27,12 @@ describe('ChatRoomManager call-method contract', () => { expect(result).toBeUndefined(); }); - test('joinChatRoom rejects with ChatError when native returns { error }', async () => { + test('joinChatRoomEx rejects with ChatError when native returns { error }', async () => { mockCallMethodOnce({ error: { code: 404, description: 'room not found' }, }); - const p = manager.joinChatRoom('room-42'); + const p = manager.joinChatRoomEx({ roomId: 'room-42' }); await expect(p).rejects.toBeInstanceOf(ChatError); await expect(p).rejects.toMatchObject({ diff --git a/src/__tests__/unit/models.test.ts b/src/__tests__/unit/models.test.ts index e791c482..ef212251 100644 --- a/src/__tests__/unit/models.test.ts +++ b/src/__tests__/unit/models.test.ts @@ -1,17 +1,14 @@ import { ChatConversation, - ChatConversationFetchOptions, ChatConversationMarkType, ChatConversationType, ChatConversationTypeFromNumber, } from '../../common/ChatConversation'; import { ChatGroup, - ChatGroupOptions, + ChatGroupConfigs, ChatGroupPermissionType, ChatGroupPermissionTypeFromNumber, - ChatGroupStyle, - ChatGroupStyleFromNumber, } from '../../common/ChatGroup'; import { ChatRoom, @@ -81,33 +78,6 @@ describe('ChatConversation constructor', () => { }); }); -describe('ChatConversationFetchOptions factories', () => { - test('default() fetches 20 unpinned conversations', () => { - const opt = ChatConversationFetchOptions.default(); - - expect(opt.pageSize).toBe(20); - expect(opt.pinned).toBe(false); - expect(opt.cursor).toBeUndefined(); - expect(opt.mark).toBeUndefined(); - }); - - test('pinned() fetches 20 pinned conversations', () => { - const opt = ChatConversationFetchOptions.pinned(); - - expect(opt.pageSize).toBe(20); - expect(opt.pinned).toBe(true); - }); - - test('withMark() sets the mark filter', () => { - const opt = ChatConversationFetchOptions.withMark( - ChatConversationMarkType.Type5 - ); - - expect(opt.pageSize).toBe(20); - expect(opt.mark).toBe(ChatConversationMarkType.Type5); - }); -}); - describe('ChatRoomPermissionTypeFromNumber', () => { test.each([ [-1, ChatRoomPermissionType.None], @@ -166,21 +136,6 @@ describe('ChatRoom constructor', () => { }); }); -describe('ChatGroupStyleFromNumber', () => { - test.each([ - [0, ChatGroupStyle.PrivateOnlyOwnerInvite], - [1, ChatGroupStyle.PrivateMemberCanInvite], - [2, ChatGroupStyle.PublicJoinNeedApproval], - [3, ChatGroupStyle.PublicOpenJoin], - ])('maps native int %i to %s', (input, expected) => { - expect(ChatGroupStyleFromNumber(input)).toBe(expected); - }); - - test('passes unknown values through unchanged', () => { - expect(ChatGroupStyleFromNumber(7)).toBe(7); - }); -}); - describe('ChatGroupPermissionTypeFromNumber', () => { test.each([ [-1, ChatGroupPermissionType.None], @@ -217,49 +172,48 @@ describe('ChatGroup constructor', () => { expect(group.messageBlocked).toBe(false); expect(group.isAllMemberMuted).toBe(false); expect(group.permissionType).toBe(ChatGroupPermissionType.Owner); - expect(group.options).toBeUndefined(); + expect(group.configs).toBeUndefined(); + expect(group.isDisabled).toBe(false); }); - test('builds nested ChatGroupOptions only when options are present', () => { + test('builds nested ChatGroupConfigs only when configs are present', () => { const group = new ChatGroup({ groupId: 'g1', owner: 'user1', permissionType: 0, - options: { style: 3, maxCount: 500, inviteNeedConfirm: true }, + isDisabled: true, + configs: { + maxCount: 500, + inviteNeedConfirm: true, + isPublic: true, + joinApprovalRequired: true, + allowInvites: true, + ext: 'ext-content', + }, }); expect(group.permissionType).toBe(ChatGroupPermissionType.Member); - expect(group.options).toBeInstanceOf(ChatGroupOptions); - expect(group.options?.style).toBe(ChatGroupStyle.PublicOpenJoin); - expect(group.options?.maxCount).toBe(500); - expect(group.options?.inviteNeedConfirm).toBe(true); + expect(group.isDisabled).toBe(true); + expect(group.configs).toBeInstanceOf(ChatGroupConfigs); + expect(group.configs?.maxCount).toBe(500); + expect(group.configs?.inviteNeedConfirm).toBe(true); + expect(group.configs?.isPublic).toBe(true); + expect(group.configs?.joinApprovalRequired).toBe(true); + expect(group.configs?.allowInvites).toBe(true); + expect(group.configs?.ext).toBe('ext-content'); }); }); -describe('ChatGroupOptions constructor', () => { +describe('ChatGroupConfigs constructor', () => { test('applies defaults when fields are missing', () => { - const opt = new ChatGroupOptions({}); - - expect(opt.style).toBe(ChatGroupStyle.PublicJoinNeedApproval); - expect(opt.maxCount).toBe(200); - expect(opt.inviteNeedConfirm).toBe(false); - expect(opt.isDisabled).toBe(false); - expect(opt.ext).toBeUndefined(); - }); - - test('converts style from native int', () => { - const opt = new ChatGroupOptions({ style: 1 }); - - expect(opt.style).toBe(ChatGroupStyle.PrivateMemberCanInvite); - }); - - // Pins current behavior: the constructor checks `params.style` for - // truthiness, so a native style of 0 (PrivateOnlyOwnerInvite) falls back - // to the default instead of being converted. - test('style 0 currently falls back to the default style', () => { - const opt = new ChatGroupOptions({ style: 0 }); - - expect(opt.style).toBe(ChatGroupStyle.PublicJoinNeedApproval); + const configs = new ChatGroupConfigs({}); + + expect(configs.maxCount).toBe(200); + expect(configs.inviteNeedConfirm).toBe(false); + expect(configs.isPublic).toBe(false); + expect(configs.joinApprovalRequired).toBe(false); + expect(configs.allowInvites).toBe(false); + expect(configs.ext).toBeUndefined(); }); }); diff --git a/src/common/ChatAreaCode.ts b/src/common/ChatAreaCode.ts deleted file mode 100644 index c3f7d14d..00000000 --- a/src/common/ChatAreaCode.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum ChatAreaCode { - GLOB = -1, - CN = 1, - NA = 2, - EU = 4, - AS = 8, - JP = 16, - IN = 32, -} diff --git a/src/common/ChatConversation.ts b/src/common/ChatConversation.ts index 9745ea9d..f1f20605 100644 --- a/src/common/ChatConversation.ts +++ b/src/common/ChatConversation.ts @@ -108,8 +108,8 @@ export function ChatConversationTypeToString( * * 关于会话名称,请根据会话类型获取: * 单聊:详见 {@link ChatUserInfoManager.fetchUserInfoById}; - * 群聊:详见 {@link ChatGroup.getGroupWithId}; - * 聊天室:详见 {@link ChatRoom.fetchChatRoomInfoFromServer}。 + * 群聊:详见 {@link ChatGroupManager.fetchGroupInfoWithoutMembersFromServer}; + * 聊天室:详见 {@link ChatRoomManager.fetchChatRoomInfoFromServer}。 */ export class ChatConversation { /** @@ -156,6 +156,21 @@ export class ChatConversation { */ remindType?: ChatPushRemindType; + /** + * 会话的显示名称。 + * + * **注意** 该字段由原生 SDK 返回。要在 RN 层根据会话类型获取会话名称, + * 详见 {@link ChatConversation.name}。 + */ + readonly displayName?: string; + + /** + * 会话的显示头像。 + * + * **注意** 该字段由原生 SDK 返回。 + */ + readonly displayAvatar?: string; + constructor(params: { convId: string; convType: ChatConversationType; @@ -165,6 +180,8 @@ export class ChatConversation { pinnedTime?: number; marks?: ChatConversationMarkType[]; remindType?: ChatPushRemindType; + name?: string; + avatar?: string; }) { this.convId = params.convId; this.convType = params.convType; @@ -174,6 +191,8 @@ export class ChatConversation { this.pinnedTime = params.pinnedTime ?? 0; this.marks = params.marks; this.remindType = params.remindType ?? ChatPushRemindType.ALL; + this.displayName = params.name; + this.displayAvatar = params.avatar; } /** @@ -313,35 +332,6 @@ export class ChatConversation { this.ext = ext; } - /** - * 标记指定消息为已读。 - * - * @param msgId 消息 ID。 - * - * @throws 如果有方法调用的异常会在这里抛出,可以看到具体错误原因。参见 {@link ChatError}。 - */ - public async markMessageAsRead(msgId: string): Promise { - return Factory.getChatClient().chatManager.markMessageAsRead( - this.convId, - this.convType, - msgId, - this.isChatThread - ); - } - - /** - * 标记所有消息为已读。 - * - * @throws 如果有方法调用的异常会在这里抛出,可以看到具体错误原因。参见 {@link ChatError}。 - */ - public async markAllMessagesAsRead(): Promise { - return Factory.getChatClient().chatManager.markAllMessagesAsRead( - this.convId, - this.convType, - this.isChatThread - ); - } - /** * 更新本地数据库的指定消息。 * @@ -421,38 +411,6 @@ export class ChatConversation { ); } - /** - * 从本地数据库获取会话中的指定用户发送的某些类型的消息。 - * - * @param msgType 消息类型。详见 {@link ChatMessageType}。 - * @param direction 消息加载方向。默认按消息中的时间戳({@link SortMessageByServerTime})的倒序加载,详见 {@link ChatSearchDirection}。 - * @param timestamp 搜索的起始时间戳。单位为毫秒。 - * @param count 获取的最大消息数量。 - * @param sender 消息发送方。该参数也可以在搜索群组消息或聊天室消息时使用。 - * - * @returns 消息列表。若未获取到,返回空列表。 - * - * @throws 如果有方法调用的异常会在这里抛出,可以看到具体错误原因。参见 {@link ChatError}。 - */ - public async getMessagesWithMsgType( - msgType: ChatMessageType, - direction: ChatSearchDirection = ChatSearchDirection.UP, - timestamp: number = -1, - count: number = 20, - sender?: string - ): Promise> { - return Factory.getChatClient().chatManager.getMessagesWithMsgType( - this.convId, - this.convType, - msgType, - direction, - timestamp, - count, - sender, - this.isChatThread - ); - } - /** * 从本地数据库中检索会话中某种类型的消息。 * @@ -460,13 +418,14 @@ export class ChatConversation { * * @params - * @param msgType 消息类型。 请参阅{@link ChatMessageType}。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 + * @param direction 消息搜索方向。请参阅 {@link ChatSearchDirection}。 * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 * @param timestamp 用于查询的消息中的起始 Unix 时间戳。 单位是毫秒。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 * 如果将此参数设置为负值,则SDK从当前时间开始,按照消息中时间戳的降序顺序检索消息。 * @param count 每次检索的最大消息数。 取值范围为[1,400]。 * @param sender 用于检索的用户 ID 或组 ID。 通常,它是会话 ID。 + * @param senders 群聊消息发送者的用户 ID 列表。 * * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。 如果没有获取到消息,则返回空列表。 * @@ -488,42 +447,6 @@ export class ChatConversation { }); } - /** - * 从本地数据库中检索会话中一定数量的消息。 - * - * **注意** - * - * 获取的消息也会加入到内存中存储的会话的现有消息中。 - * - * @param startMsgId 查询的起始消息ID。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 - * 如果该参数设置为空字符串,则SDK按照消息搜索方向检索消息,而忽略该参数。 - * - 如果“direction”设置为“ChatSearchDirection.UP”,则 SDK 会按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序顺序从最新消息开始检索消息。 - * - 如果“direction”设置为“ChatSearchDirection.DOWN”,则 SDK 会按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的升序从最旧的消息开始检索消息。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序检索消息。 - * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的升序检索消息。 - * @param loadCount 每次检索的最大消息数。 取值范围为[1,400]。 - * @returns 消息列表(不包括具有开始或结束时间戳的消息列表)。 如果没有获取到消息,则返回空列表。 - * - * @throws 异常的描述。 请参阅{@link ChatError}。 - * - * @deprecated 2024-04-17 此方法已弃用。 请改用 {@link getMsgs}。 - */ - public async getMessages( - startMsgId: string, - direction: ChatSearchDirection = ChatSearchDirection.UP, - loadCount: number = 20 - ): Promise> { - return Factory.getChatClient().chatManager.getMessages( - this.convId, - this.convType, - startMsgId, - direction, - loadCount, - this.isChatThread - ); - } - /** * 从本地数据库中检索会话中指定数量的消息。 * @@ -534,7 +457,7 @@ export class ChatConversation { * @params - * @param startMsgId 查询的起始消息ID。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 * 如果该参数设置为空字符串,则SDK按照消息搜索方向检索消息,而忽略该参数。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 + * @param direction 消息搜索方向。请参阅 {@link ChatSearchDirection}。 * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 * @param loadCount 每次检索的最大消息数。 取值范围为[1,50]。 @@ -575,42 +498,6 @@ export class ChatConversation { }); } - /** - * 检索本地数据库中会话中带有关键字的消息。 - * - * @param keywords 查询的关键字。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 - * - (默认)“ChatSearchDirection.Up”:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序顺序检索消息。 - * - `ChatSearchDirection.Down`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的升序检索消息。 - * @param timestamp 用于查询的消息中的起始 Unix 时间戳。 单位是毫秒。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 - * 如果将此参数设置为负值,则 SDK 从当前时间开始,按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序顺序检索消息。 - * @param count 每次检索的最大消息数。取值范围为[1,400]。 - * @param sender 用于检索的用户 ID 或组 ID。 - * @returns 检索到的消息列表(不包括具有起始时间戳的消息)。如果没有获取到消息,则返回空列表。 - * - * @throws 异常的描述。请参阅{@link ChatError}。 - * - * @deprecated 2024-04-17 此方法已弃用。 请改用 {@link getMsgsWithKeyword}。 - */ - public async getMessagesWithKeyword( - keywords: string, - direction: ChatSearchDirection = ChatSearchDirection.UP, - timestamp: number = -1, - count: number = 20, - sender?: string - ): Promise> { - return Factory.getChatClient().chatManager.getMessagesWithKeyword( - this.convId, - this.convType, - keywords, - direction, - timestamp, - count, - sender, - this.isChatThread - ); - } - /** * 获取指定用户在一定时间段内在会话中发送的消息。 * @@ -621,7 +508,7 @@ export class ChatConversation { * @params - * - keywords 查询的关键字。 * - direction 消息搜索方向。请参阅 {@link ChatSearchDirection}。 - * - (Default) `ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序顺序检索消息。 + * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序顺序检索消息。 * - `ChatSearchDirection.DOWN`: 按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的升序检索消息。 * - timestamp 用于查询的消息中的起始 Unix 时间戳。 单位是毫秒。 设置该参数后,SDK按照消息搜索方向,从指定的消息开始检索消息。 * - searchScope 搜索范围,请参阅 {@link ChatMessageSearchScope}。 @@ -638,7 +525,6 @@ export class ChatConversation { direction?: ChatSearchDirection; timestamp?: number; count?: number; - sender?: string; senders?: Array; searchScope?: ChatMessageSearchScope; }): Promise> { @@ -650,38 +536,6 @@ export class ChatConversation { }); } - /** - * 获取本地数据库中某个会话在一定时间内发送和接收的消息。 - * - * @param startTime 搜索的起始 Unix 时间戳。 单位是毫秒。 - * @param endTime 搜索的结束 Unix 时间戳。 单位是毫秒。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 - * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的降序检索消息。 - * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳 ({@link ChatOptions.sortMessageByServerTime}) 的升序检索消息。 - * @param count 每次检索的最大消息数。 取值范围为[1,400]。 - * @returns 检索到的消息列表(不包括具有开始或结束时间戳的消息)。 如果没有获取到消息,则返回空列表。 - * - * @throws 异常的描述。 请参阅{@link ChatError}。 - * - * @deprecated 2024-04-17 此方法已弃用。 请改用 {@link getMsgWithTimestamp}。 - */ - public async getMessageWithTimestamp( - startTime: number, - endTime: number, - direction: ChatSearchDirection = ChatSearchDirection.UP, - count: number = 20 - ): Promise> { - return Factory.getChatClient().chatManager.getMessageWithTimestamp( - this.convId, - this.convType, - startTime, - endTime, - direction, - count, - this.isChatThread - ); - } - /** * 检索本地数据库中某个会话在一定时间内发送和接收的消息。 * @@ -690,7 +544,7 @@ export class ChatConversation { * @params - * @param startTime 查询的起始 Unix 时间戳,以毫秒为单位。 * @param endTime 查询的结束 Unix 时间戳,以毫秒为单位。 - * @param Direction 消息搜索方向。 请参阅{@link ChatSearchDirection}。 + * @param direction 消息搜索方向。请参阅 {@link ChatSearchDirection}。 * - (默认)`ChatSearchDirection.UP`:按照消息中包含的 Unix 时间戳的降序检索消息。 * - `ChatSearchDirection.DOWN`:按照消息中包含的 Unix 时间戳的升序检索消息。 * @param count 每次检索的最大消息数。 取值范围为[1,400]。 @@ -826,56 +680,3 @@ export class ChatConversation { }); } } - -/** - * 会话对象过滤选项。 - */ -export class ChatConversationFetchOptions { - /** - * 每页查询的会话数量。查询标记的会话时,取值范围为 [1,10],默认为 10。否则,取值范围为[1,50]。 - */ - pageSize?: number; - /** - * 查询游标,即会话查询的起始位置。 - */ - cursor?: string; - /** - * 是否获取置顶的会话。 - */ - pinned?: boolean; - /** - * 是否获取添加标记的会话。 - */ - mark?: ChatConversationMarkType; - constructor(params: { - pageSize?: number; - cursor?: string; - pinned?: boolean; - mark?: ChatConversationMarkType; - }) { - this.pageSize = params.pageSize; - this.cursor = params.cursor; - this.pinned = params.pinned; - this.mark = params.mark; - } - static default(): ChatConversationFetchOptions { - return new ChatConversationFetchOptions({ - pageSize: 20, - pinned: false, - }); - } - static pinned(): ChatConversationFetchOptions { - return new ChatConversationFetchOptions({ - pageSize: 20, - pinned: true, - }); - } - static withMark( - mark: ChatConversationMarkType - ): ChatConversationFetchOptions { - return new ChatConversationFetchOptions({ - pageSize: 20, - mark: mark, - }); - } -} diff --git a/src/common/ChatCursorResult.ts b/src/common/ChatCursorResult.ts index 2a7346a8..46245b4c 100644 --- a/src/common/ChatCursorResult.ts +++ b/src/common/ChatCursorResult.ts @@ -12,12 +12,21 @@ export class ChatCursorResult { * 请求结果。 */ list?: Array; + /** + * 服务端结果总数。 + * + * **注意** 仅部分平台的部分 API 会返回该字段。 + * 原生 SDK 未提供该字段时,其值为 `undefined`。 + */ + totalCount?: number; constructor(params: { cursor: string; list?: Array; + totalCount?: number; opt?: { map: (obj: any) => any }; }) { this.cursor = params.cursor; + this.totalCount = params.totalCount; let data: Array = []; params.list?.forEach((value) => { data.push(params.opt ? params.opt.map(value) : value); diff --git a/src/common/ChatGroup.ts b/src/common/ChatGroup.ts index 9690c165..e8dc1399 100644 --- a/src/common/ChatGroup.ts +++ b/src/common/ChatGroup.ts @@ -1,26 +1,5 @@ import type { ChatError } from './ChatError'; - -/** - * 群组类型枚举。 - */ -export enum ChatGroupStyle { - /** - * 私有群组,创建完成后,只允许群主邀请用户加入。 - */ - PrivateOnlyOwnerInvite = 0, - /** - * 私有群组,创建完成后,只允许群主和群成员邀请用户加入。 - */ - PrivateMemberCanInvite = 1, - /** - * 公开群组,创建完成后,只允许群主邀请用户加入;非群成员用户需发送入群申请,群主同意后才能入群。 - */ - PublicJoinNeedApproval = 2, - /** - * 公开群组,创建完成后,允许非群组成员加入,无需群主同意。 - */ - PublicOpenJoin = 3, -} +import { ChatGroupMemberInfo } from './ChatGroupMemberInfo'; /** * 群组角色类型枚举。 @@ -44,37 +23,6 @@ export enum ChatGroupPermissionType { Owner = 2, } -/** - * 将群组类型由整型转换为枚举类型。 - * - * @param params 整型的群组类型。 - * @returns 枚举类型的群组类型。 - */ -export function ChatGroupStyleFromNumber(params: number): ChatGroupStyle { - switch (params) { - case 0: - return ChatGroupStyle.PrivateOnlyOwnerInvite; - case 1: - return ChatGroupStyle.PrivateMemberCanInvite; - case 2: - return ChatGroupStyle.PublicJoinNeedApproval; - case 3: - return ChatGroupStyle.PublicOpenJoin; - default: - return params; - } -} - -/** - * 群组类型由枚举转换为字符串类型。 - * - * @param params 枚举类型的群组类型。 - * @returns 字符串类型的群组类型。 - */ -export function ChatGroupStyleToString(params: ChatGroupStyle): string { - return ChatGroupStyle[params]!; -} - /** * 群成员角色由数字整型转换为枚举类型。 * @@ -113,19 +61,19 @@ export function ChatGroupPermissionTypeToString( /** * 群组消息已读回执类。 */ -export class ChatGroupMessageAck { +export class ChatGroupReadReceipt { /** * 群组消息 ID。 */ - msg_id: string; + msgId: string; /** * 群组消息已读回执的 ID。 */ - ack_id: string; + ackId: string; /** * 发送已读回执的用户 ID。 */ - from: string; + from: ChatGroupMemberInfo; /** * 群组消息已读回执数量。 */ @@ -134,33 +82,25 @@ export class ChatGroupMessageAck { * 群组消息已读回执发送的 Unix 时间戳。单位为毫秒。 */ timestamp: number; - /** - * 已读回执的扩展信息。 - */ - content?: string; constructor(params: { msg_id: string; ack_id: string; - from: string; + from: any; count: number; timestamp: number; - ext?: { content: string }; }) { - this.msg_id = params.msg_id; - this.ack_id = params.ack_id; - this.from = params.from; + this.msgId = params.msg_id; + this.ackId = params.ack_id; + this.from = new ChatGroupMemberInfo(params.from); this.count = params.count; this.timestamp = params.timestamp; - if (params.ext) { - this.content = params.ext.content; - } } } /** * 群组信息类,包含群组相关的信息。 * - * 可调用 {@link ChatGroupManager.fetchGroupInfoFromServer} 方法获取群组相关信息。 + * 可调用 {@link ChatGroupManager.fetchGroupInfoWithoutMembersFromServer} 方法获取群组相关信息。 */ export class ChatGroup { /** @@ -224,14 +164,20 @@ export class ChatGroup { */ permissionType: ChatGroupPermissionType; /** - * 群组选项。 + * 群组配置,详见 {@link ChatGroupConfigs}。 + */ + configs?: ChatGroupConfigs; + /** + * 群组是否被禁用: + * - `true`:是。 + * - `false`:否。 */ - options?: ChatGroupOptions; + isDisabled: boolean; /** * 获取群组最大成员数量。该参数在创建群组时设定。 */ get maxCount(): number { - return this.options?.maxCount ?? 0; + return this.configs?.maxCount ?? 0; } constructor(params: { @@ -249,7 +195,8 @@ export class ChatGroup { messageBlocked?: boolean; isAllMemberMuted?: boolean; permissionType: number; - options?: any; + configs?: any; + isDisabled?: boolean; }) { this.groupId = params.groupId; this.groupName = params.groupName ?? ''; @@ -267,33 +214,65 @@ export class ChatGroup { this.permissionType = ChatGroupPermissionTypeFromNumber( params.permissionType ); - if (params.options) { - this.options = new ChatGroupOptions(params.options); + if (params.configs) { + this.configs = new ChatGroupConfigs(params.configs); } + this.isDisabled = params.isDisabled ?? false; } } /** - * 群组选项类,包含群组创建时设置的选项。 + * 群组配置类型,用于指定要更新的群组配置。 + * + * 该类型为位掩码。可以使用按位或运算符合并多个类型, + * 例如 `ChatGroupConfigsType.MaxUsers | ChatGroupConfigsType.Ext`。 + * + * 详见 {@link ChatGroupManager.updateGroupConfigs}。 */ -export class ChatGroupOptions { +export enum ChatGroupConfigsType { /** - * 群组类型。 + * 是否允许群组成员邀请其他用户加入群组。 */ - style: ChatGroupStyle; + AllowInvites = 1, // 1 << 0 /** - * 群组最大成员数量。 + * 群组允许的最大成员数。 + */ + MaxUsers = 2, // 1 << 1 + /** + * 邀请用户加入群组时是否需要征得对方同意。 + */ + InviteNeedConfirm = 4, // 1 << 2 + /** + * 加入群组是否需要群主或管理员批准。 + */ + JoinApprovalRequired = 8, // 1 << 3 + /** + * 群组是否为公开群。 + */ + IsPublic = 16, // 1 << 4 + /** + * 群组扩展信息。 + */ + Ext = 32, // 1 << 5 +} + +/** + * 创建或更新群组时要设置的群组配置。 + */ +export class ChatGroupConfigs { + /** + * 群组允许的最大成员数。默认值为 200。 */ maxCount: number; /** * 邀请用户进群是否需要对方同意。 * - * 收到邀请是否自动入群取决于两个设置:创建群组时设置 {@link GroupOptions#inviteNeedConfirm} 以及通过 {@link ChatOptions#autoAcceptGroupInvitation} 确定是否自动接受加群邀请。 + * 收到邀请是否自动入群取决于两个设置:创建群组时设置 {@link ChatGroupConfigs.inviteNeedConfirm},以及通过 {@link ChatOptions.autoAcceptGroupInvitation} 确定是否自动接受加群邀请。 * * (1)如果 `inviteNeedConfirm` 设置为 `false`,在服务端直接加受邀人进群,与受邀人对 {@link ChatOptions.autoAcceptGroupInvitation} 的设置无关。 * (2) 如果 `inviteNeedConfirm` 设置为 `true`,是否自动入群取决于受邀请人对 {@link ChatOptions.autoAcceptGroupInvitation} 的设置。 * - * {@link ChatOptions#autoAcceptGroupInvitation} 为 SDK 级别操作,设置为 `true` 时,受邀人收到入群邀请后,SDK 在内部调用同意入群的 API,自动接受邀请入群; + * {@link ChatOptions.autoAcceptGroupInvitation} 为 SDK 级别操作,设置为 `true` 时,受邀人收到入群邀请后,SDK 在内部调用同意入群的 API,自动接受邀请入群; * 若设置为 `false`,即非自动同意其邀请,用户可以选择接受邀请进群,也可选择拒绝邀请。 */ inviteNeedConfirm: boolean; @@ -302,28 +281,40 @@ export class ChatGroupOptions { */ ext?: string; /** - * 是否禁用群组 - * - `true`: 是. - * - `false`: 否. + * 群组是否为公开群: + * - `true`:是。 + * - (默认)`false`:否。 */ - isDisabled: boolean; + isPublic: boolean; /** - * 构造一个对象。 + * 加入群组是否需要群主或管理员批准: + * - `true`:是。 + * - (默认)`false`:否。 + */ + joinApprovalRequired: boolean; + /** + * 是否允许群组成员邀请其他用户加入群组: + * - `true`:是。 + * - (默认)`false`:否。 + */ + allowInvites: boolean; + /** + * 构造一个群组配置对象。 */ constructor(params: { - style?: number; maxCount?: number; inviteNeedConfirm?: boolean; ext?: string; - isDisabled?: boolean; + isPublic?: boolean; + joinApprovalRequired?: boolean; + allowInvites?: boolean; }) { - this.style = params.style - ? ChatGroupStyleFromNumber(params.style) - : ChatGroupStyle.PublicJoinNeedApproval; this.maxCount = params.maxCount ?? 200; this.inviteNeedConfirm = params.inviteNeedConfirm ?? false; this.ext = params.ext; - this.isDisabled = params.isDisabled ?? false; + this.isPublic = params.isPublic ?? false; + this.joinApprovalRequired = params.joinApprovalRequired ?? false; + this.allowInvites = params.allowInvites ?? false; } } diff --git a/src/common/ChatGroupMemberInfo.ts b/src/common/ChatGroupMemberInfo.ts new file mode 100644 index 00000000..8c3857f1 --- /dev/null +++ b/src/common/ChatGroupMemberInfo.ts @@ -0,0 +1,48 @@ +import type { ChatGroupPermissionType } from './ChatGroup'; + +/** + * 定义群组成员信息的类。 + * + * 详见 {@link ChatGroupReadReceipt.from}。 + */ +export class ChatGroupMemberInfo { + /** + * 群组成员的用户 ID。 + */ + memberId: string; + /** + * 成员加入群组的 Unix 时间戳,单位为毫秒。 + */ + joinedTimestamp: number; + /** + * 群组成员的角色,详见 {@link ChatGroupPermissionType}。 + */ + role: ChatGroupPermissionType; + /** + * 群组成员在群组中的名片。 + */ + namecard?: string; + /** + * 群组成员的昵称。 + */ + nickname?: string; + /** + * 群组成员的头像 URL。 + */ + avatarUrl?: string; + constructor(params: { + memberId: string; + joinedTimestamp: number; + role: ChatGroupPermissionType; + namecard?: string; + nickname?: string; + avatarUrl?: string; + }) { + this.memberId = params.memberId; + this.joinedTimestamp = params.joinedTimestamp; + this.role = params.role; + this.namecard = params.namecard; + this.nickname = params.nickname; + this.avatarUrl = params.avatarUrl; + } +} diff --git a/src/common/ChatMessage.ts b/src/common/ChatMessage.ts index 1e693dcd..3083bb67 100644 --- a/src/common/ChatMessage.ts +++ b/src/common/ChatMessage.ts @@ -396,30 +396,30 @@ export class ChatMessage { */ hasDeliverAck: boolean = false; /** - * 单聊时,发送方是否收到了接收方的已读回执。接收方阅读消息后会调用 {@link ChatManager.sendMessageReadAck} 或者 {@link ChatManager.sendConversationReadAck} 发送已读回执。若需要已读回执,SDK 初始化时需设置 {@link ChatOptions.requireAck} 为 `true`。 + * 单聊时,发送方是否收到了接收方的消息已读回执。接收方阅读消息后,会调用 {@link ChatManager.sendMessageReadReceipts} 向发送方发送已读回执。 * * - `true`:是。 * - (默认)`false`:否。 */ - hasReadAck: boolean = false; + readonly isPeerRead: boolean = false; /** * 群聊时,是否需要消息已读回执。 * * - `true`:是。 * - (默认)`false`:否。 */ - needGroupAck: boolean = false; + isNeedReadReceipt: boolean = false; /** - * 群聊时,已阅读消息的群成员数量。群成员已读消息后调用 {@link ChatManager.sendGroupMessageReadAck} 或者 {@link ChatManager.sendConversationReadAck} 发送已读回执。若需要开启已读回执功能,初始化时需设置 {@link ChatOptions.requireAck} 为 `true`, 并且发送消息时设置 {@link isNeedGroupAck} 为 `true`。 + * 群聊时,已阅读消息的群成员数量。群成员阅读消息后,会调用 {@link ChatManager.sendMessageReadReceipts} 发送已读回执。若要为群消息启用已读回执功能,需要在发送消息时将 {@link isNeedReadReceipt} 设置为 `true`。 */ - groupAckCount: number = 0; + readonly groupReadReceiptCount: number = 0; /** - * 单聊或群聊时,接收方是否已读了消息。该参数的值影响会话的未读消息数。阅读消息后,接收方可以调用 {@link ChatManager.markMessageAsRead} 或者 {@link ChatManager.markAllMessagesAsRead} 将消息设置为已读。 + * 单聊或群聊时,接收方是否已读消息。该参数的值与会话的未读消息数相关。接收方可以调用 {@link ChatManager.clearConversationUnreadMessageCount} 清除会话的未读消息数。 * * - `true`:是。 * - (默认)`false`:否。 */ - hasRead: boolean = false; + readonly isRead: boolean = false; /** * 消息的聊天类型,详见 {@link ChatMessageChatType}。 */ @@ -463,7 +463,7 @@ export class ChatMessage { /** * 聊天室消息的优先级。 - * **Note** 该属性仅适用于聊天室会话。 + * **注意** 该属性仅适用于聊天室会话。 */ // @ts-expect-error private priority?: ChatRoomMessagePriority; @@ -527,10 +527,10 @@ export class ChatMessage { localTime?: number; serverTime?: number; hasDeliverAck?: boolean; - hasReadAck?: boolean; - needGroupAck?: boolean; - groupAckCount?: number; - hasRead?: boolean; + isPeerRead?: boolean; + isNeedReadReceipt?: boolean; + groupReadReceiptCount?: number; + isRead?: boolean; chatType?: number; direction?: string; status?: number; @@ -553,10 +553,10 @@ export class ChatMessage { this.localTime = params.localTime ?? getNowTimestamp(); this.serverTime = params.serverTime ?? getNowTimestamp(); this.hasDeliverAck = params.hasDeliverAck ?? false; - this.hasReadAck = params.hasReadAck ?? false; - this.needGroupAck = params.needGroupAck ?? false; - this.groupAckCount = params.groupAckCount ?? 0; - this.hasRead = params.hasRead ?? false; + this.isPeerRead = params.isPeerRead ?? false; + this.isNeedReadReceipt = params.isNeedReadReceipt ?? false; + this.groupReadReceiptCount = params.groupReadReceiptCount ?? 0; + this.isRead = params.isRead ?? false; this.chatType = ChatMessageChatTypeFromNumber(params.chatType ?? 0); this.direction = ChatMessageDirectionFromString(params.direction ?? 'send'); this.status = ChatMessageStatusFromNumber(params.status ?? 0); @@ -659,7 +659,7 @@ export class ChatMessage { body: params.body, direction: 'send', to: params.targetId, - hasRead: true, + isRead: true, chatType: params.chatType, isChatThread: params.isChatThread, conversationId: params.targetId, @@ -678,7 +678,7 @@ export class ChatMessage { * - 群聊时是群组 ID。 * - 聊天室则是聊天室 ID。 * @param content 文本消息内容。 - * @param chatType 会话类型。详见 {@link ChatType}。 + * @param chatType 会话类型。详见 {@link ChatMessageChatType}。 * @params opt 消息扩展参数。 * - isChatThread: 是否是子区消息。默认不是子区消息。 * - isOnline: 是否为在线时收到的消息。 @@ -720,12 +720,14 @@ export class ChatMessage { * - 群聊时为群组 ID。 * - 聊天室则为聊天室 ID。 * @param filePath 文件路径。 - * @param chatType 会话类型。详见 {@link ChatType}。 - * @param displayName 文件名称。 + * @param chatType 会话类型。详见 {@link ChatMessageChatType}。 * @params opt 消息扩展参数。 + * - displayName: 文件名称。 * - isChatThread: 是否是子区消息。默认不是子区消息。 * - isOnline: 是否为在线时收到的消息。 * - deliverOnlineOnly: 消息是否只投递给在线用户。 + * - fileSize: 文件大小。 + * - receiverList: 定向消息的接收方列表。 * * @returns 消息实例。 */ @@ -936,7 +938,7 @@ export class ChatMessage { * - 群聊时是群组 ID。 * - 聊天室则是聊天室 ID。 * @param messageIdList 合并的消息列表 ID。 - * @param chatType 会话类型。 详见 {@link ChatType}. + * @param chatType 会话类型。详见 {@link ChatMessageChatType}。 * @params opt 消息扩展参数。 * - isChatThread: 是否是子区消息。 * - `true`:是; @@ -952,7 +954,7 @@ export class ChatMessage { * - compatibleText: 合并消息的兼容信息。该字段用于需要兼容不支持合并转发消息的版本。 * - receiverList: 消息接收人列表。 * - * @returns The message instance. + * @returns 消息实例。 */ public static createCombineMessage( targetId: string, @@ -1135,14 +1137,6 @@ export class ChatMessage { return Factory.getChatClient().chatManager.getReactionList(this.msgId); } - /** - * 获取群组消息的已读人数。 - * - */ - public get groupReadCount(): Promise { - return Factory.getChatClient().chatManager.groupAckCount(this.msgId); - } - /** * 获取指定子区的详情。 * @@ -1381,10 +1375,6 @@ export class ChatImageMessageBody extends _ChatFileMessageBody { * 缩略图在服务器的地址。 */ thumbnailRemotePath: string; - /** - * 下载缩略图需要的 token。 - */ - thumbnailSecret: string; /** * 缩略图下载状态,详见 {@link ChatDownloadStatus}。 */ @@ -1429,7 +1419,6 @@ export class ChatImageMessageBody extends _ChatFileMessageBody { sendOriginalImage?: boolean; thumbnailLocalPath?: string; thumbnailRemotePath?: string; - thumbnailSecret?: string; thumbnailStatus?: number; width?: number; height?: number; @@ -1457,7 +1446,6 @@ export class ChatImageMessageBody extends _ChatFileMessageBody { this.sendOriginalImage = params.sendOriginalImage ?? false; this.thumbnailLocalPath = params.thumbnailLocalPath ?? ''; this.thumbnailRemotePath = params.thumbnailRemotePath ?? ''; - this.thumbnailSecret = params.thumbnailSecret ?? ''; this.thumbnailStatus = ChatDownloadStatusFromNumber( params.thumbnailStatus ?? -1 ); @@ -1721,10 +1709,6 @@ export class ChatMessagePinInfo { * 查询消息的选项。 */ export class ChatFetchMessageOptions { - /** - * 消息发送方。 - */ - from?: string; /** * 群组会话中消息发送者的用户 ID 数组。 */ @@ -1748,11 +1732,10 @@ export class ChatFetchMessageOptions { /** * 获取的消息是否保存到本地数据库。 * - `true`: 保存到数据库; - * - `false`(Default):不保存。 + * - (默认)`false`:不保存。 */ needSave: boolean; constructor(params: { - from?: string; senders?: Array; msgTypes?: ChatMessageType[]; startTs: number; @@ -1760,7 +1743,6 @@ export class ChatFetchMessageOptions { direction: ChatSearchDirection; needSave: boolean; }) { - this.from = params.from; this.senders = params.senders; this.startTs = params.startTs; this.endTs = params.endTs; diff --git a/src/common/ChatMessageReaction.ts b/src/common/ChatMessageReaction.ts index 99ef4e0f..4ea9ce4a 100644 --- a/src/common/ChatMessageReaction.ts +++ b/src/common/ChatMessageReaction.ts @@ -1,5 +1,5 @@ /** - * Operation type of reaction. + * Reaction 操作类型。 */ export enum ChatReactionOperate { Remove, diff --git a/src/common/ChatMessageReadReceipt.ts b/src/common/ChatMessageReadReceipt.ts new file mode 100644 index 00000000..e9233dc0 --- /dev/null +++ b/src/common/ChatMessageReadReceipt.ts @@ -0,0 +1,41 @@ +/** + * 消息已读回执类。 + * + * 已读回执记录消息的已读状态:对于单聊消息,记录对端用户是否已读; + * 对于要求已读回执的群组消息,记录已读该消息的群组成员数量。 + * + * 详见 {@link ChatManager.getGroupMessageReadReceipts} 和 + * {@link ChatMessageEventListener.onMessageReadReceipts}。 + */ +export class ChatMessageReadReceipt { + /** + * 消息 ID。 + */ + msgId: string; + /** + * 会话 ID。 + */ + convId: string; + /** + * 已读回执是否来自对端用户: + * - `true`:回执来自对端用户(单聊消息被接收方已读,或群组消息被群组成员已读)。 + * - `false`:在多设备登录场景下,回执来自当前用户的其他设备。 + */ + isPeerReceipt: boolean; + /** + * 群组消息的已读回执数量。该字段仅对群组消息有效, + * 对单聊消息其值为 `0`。 + */ + readCount: number; + constructor(params: { + msg_id: string; + conv_id: string; + isPeerReceipt: boolean; + readCount: number; + }) { + this.msgId = params.msg_id; + this.convId = params.conv_id; + this.isPeerReceipt = params.isPeerReceipt; + this.readCount = params.readCount; + } +} diff --git a/src/common/ChatMessageStreamChunk.ts b/src/common/ChatMessageStreamChunk.ts index dfed0185..ecdbca3e 100644 --- a/src/common/ChatMessageStreamChunk.ts +++ b/src/common/ChatMessageStreamChunk.ts @@ -48,6 +48,4 @@ export interface ChatStreamChunk { * 流分片的自定义类型。 */ customType?: string; - - // sequenceNumber: number; // deprecated } diff --git a/src/common/ChatOptions.ts b/src/common/ChatOptions.ts index 2322114f..7f097527 100644 --- a/src/common/ChatOptions.ts +++ b/src/common/ChatOptions.ts @@ -1,9 +1,44 @@ -import { ChatAreaCode } from './ChatAreaCode'; import { ChatError } from './ChatError'; -import type { ChatPushConfig } from './ChatPushConfig'; + +export enum ChatAreaCode { + GLOB = -1, + CN = 1, + NA = 2, + EU = 4, + AS = 8, + JP = 16, + IN = 32, +} + +/** + * 数据同步类型。 + * + * 该类型为位掩码。可以使用按位或运算符合并多个类型, + * 例如 `ChatDataSyncType.Conversations | ChatDataSyncType.Contacts`。 + * + * 详见 {@link ChatOptions.dataSyncType}。 + */ +export enum ChatDataSyncType { + /** + * 不同步任何数据。 + */ + None = 0, + /** + * 同步会话数据。 + */ + Conversations = 1, // 1 << 0 + /** + * 同步联系人数据。 + */ + Contacts = 2, // 1 << 1 + /** + * 同步已加入群组的数据。 + */ + JoinedGroups = 4, // 1 << 2 +} /** - * 聊天设置类,用于定义 SDK 的各种参数和选项,例如,是否发送前加密信息、是否自动接受加好友邀请等。 + * 聊天设置类,用于定义 SDK 的各种参数和选项,例如是否在发送消息前加密,以及是否自动接受好友邀请。 */ export class ChatOptions { /** @@ -14,12 +49,6 @@ export class ChatOptions { * 创建 App 时在 console 后台上注册的 App 唯一识别符,即 App ID。 */ appId: string; - /** - * 是否开启自动登录。 - * -(默认) `true`:开启; - * - `false`:关闭。 - */ - autoLogin: boolean; /** * 是否输出调试信息。 * - `true`: SDK 会在日志里输出调试信息。 @@ -38,7 +67,7 @@ export class ChatOptions { * 是否自动接受好友邀请。 * * - `true`: 是. - * - (Default) `false`: 否. + * - (默认)`false`: 否. */ acceptInvitationAlways: boolean; /** @@ -47,15 +76,6 @@ export class ChatOptions { * - `false`:否。 */ autoAcceptGroupInvitation: boolean; - /** - * 是否需要接收方阅读消息后发送已读回执。 - * - (默认) `true`:是; - * - `false`:否。 - * - * 该设置对于 {@link ChatManager.sendConversationReadAck} 无效。 - * - */ - requireAck: boolean; /** * 是否需要接收方发送送达回执。 * -(默认)`true`:是; @@ -118,10 +138,6 @@ export class ChatOptions { * **注意** 该属性仅用于 iOS 平台。设置为在环信控制台配置的 PushKit 证书名称,并通过 {@link ChatClient.bindPushKitToken} 绑定 `PKPushRegistry` 上报的 token。只能在调用 {@link ChatClient.init} 时设置,App 运行期间不可更改。 */ pushKitCertName?: string; - /** - * 推送设置。 - */ - pushConfig?: ChatPushConfig; /** * 设置是否开启 DNS。 * - (默认) `true`:开启。 @@ -217,7 +233,7 @@ export class ChatOptions { /** * 工作目录下的内容是否可以拷贝 * - * **Note** 仅ios平台生效。 + * **注意** 仅 iOS 平台生效。 * * 默认是 `false` */ @@ -226,7 +242,7 @@ export class ChatOptions { /** * UIKit 版本。 * - * - (Default) undefined. + * - (默认)`undefined`。 * */ uikitVersion?: string; @@ -263,32 +279,13 @@ export class ChatOptions { enableUserInfo: boolean; /** - * 是否在登录后自动同步联系人列表。 + * 登录后从服务器同步的数据类型,详见 {@link ChatDataSyncType}。 * - * - `true`:是。 - * - (默认)`false`:否。 - */ - enableAutoSyncContacts: boolean; - - /** - * 获取本地会话列表时是否包含聊天室会话。 + * 该值为位掩码,例如 `ChatDataSyncType.Conversations | ChatDataSyncType.Contacts`。 * - * - `true`:是。本地会话列表中包含聊天室会话。 - * - (默认)`false`:否。本地会话列表中不包含聊天室会话。 - * - * 该配置不控制聊天室会话的创建或存储,也不影响聊天室消息的正常收发。 - */ - enableChatroomConversation: boolean; - - /** - * 是否在 SDK 初始化时自动将所有会话加载到内存。 - * - * - (默认)`true`:是。 - * - `false`:否。你需要调用 {@link ChatManager.fetchConversationsFromDB} 分页加载会话。 - * - * 开启自动加载会话后,分页加载会话没有意义。 + * 如果未设置该属性,则使用原生 SDK 的默认值。 */ - autoLoadConversations: boolean; + dataSyncType?: ChatDataSyncType; /** * 自定义 NTP 服务器地址列表。 @@ -301,17 +298,12 @@ export class ChatOptions { */ ntpServers?: string[]; - /** - * @deprecated 请使用 {@link withAppId} 和 {@link withAppKey} 替代. - */ - constructor(params: { + private constructor(params: { appKey: string; appId: string; - autoLogin?: boolean; debugModel?: boolean; acceptInvitationAlways?: boolean; autoAcceptGroupInvitation?: boolean; - requireAck?: boolean; requireDeliveryAck?: boolean; deleteMessagesAsExitGroup?: boolean; deleteMessagesAsExitChatRoom?: boolean; @@ -322,7 +314,6 @@ export class ChatOptions { isAutoDownload?: boolean; apnsCertName?: string; pushKitCertName?: string; - pushConfig?: ChatPushConfig; areaCode?: ChatAreaCode; logTag?: string; logTimestamp?: boolean; @@ -345,9 +336,7 @@ export class ChatOptions { webSocketPort?: number; dohVendor?: number; enableUserInfo?: boolean; - enableAutoSyncContacts?: boolean; - enableChatroomConversation?: boolean; - autoLoadConversations?: boolean; + dataSyncType?: ChatDataSyncType; ntpServers?: string[]; }) { if (!params.appKey && !params.appId) { @@ -358,11 +347,9 @@ export class ChatOptions { } this.appId = params.appId; this.appKey = params.appKey; - this.autoLogin = params.autoLogin ?? true; this.debugModel = params.debugModel ?? false; this.acceptInvitationAlways = params.acceptInvitationAlways ?? false; this.autoAcceptGroupInvitation = params.autoAcceptGroupInvitation ?? false; - this.requireAck = params.requireAck ?? true; this.requireDeliveryAck = params.requireDeliveryAck ?? false; this.deleteMessagesAsExitGroup = params.deleteMessagesAsExitGroup ?? true; this.deleteMessagesAsExitChatRoom = @@ -375,7 +362,6 @@ export class ChatOptions { this.isAutoDownload = params.isAutoDownload ?? true; this.apnsCertName = params.apnsCertName; this.pushKitCertName = params.pushKitCertName; - this.pushConfig = params.pushConfig; this.enableDNSConfig = params.enableDNSConfig !== undefined ? params.enableDNSConfig : true; this.dnsUrl = params.dnsUrl ?? ''; @@ -402,20 +388,15 @@ export class ChatOptions { this.webSocketPort = params.webSocketPort ?? 0; this.dohVendor = params.dohVendor ?? 1; // agora is 2. this.enableUserInfo = params.enableUserInfo ?? false; - this.enableAutoSyncContacts = params.enableAutoSyncContacts ?? false; - this.enableChatroomConversation = - params.enableChatroomConversation ?? false; - this.autoLoadConversations = params.autoLoadConversations ?? true; + this.dataSyncType = params.dataSyncType; this.ntpServers = params.ntpServers; } static withAppId(params: { appId: string; - autoLogin?: boolean; debugModel?: boolean; acceptInvitationAlways?: boolean; autoAcceptGroupInvitation?: boolean; - requireAck?: boolean; requireDeliveryAck?: boolean; deleteMessagesAsExitGroup?: boolean; deleteMessagesAsExitChatRoom?: boolean; @@ -426,7 +407,6 @@ export class ChatOptions { isAutoDownload?: boolean; apnsCertName?: string; pushKitCertName?: string; - pushConfig?: ChatPushConfig; areaCode?: ChatAreaCode; logTag?: string; logTimestamp?: boolean; @@ -449,9 +429,7 @@ export class ChatOptions { webSocketPort?: number; dohVendor?: number; enableUserInfo?: boolean; - enableAutoSyncContacts?: boolean; - enableChatroomConversation?: boolean; - autoLoadConversations?: boolean; + dataSyncType?: ChatDataSyncType; ntpServers?: string[]; }) { return new ChatOptions({ @@ -462,11 +440,9 @@ export class ChatOptions { } static withAppKey(params: { appKey: string; - autoLogin?: boolean; debugModel?: boolean; acceptInvitationAlways?: boolean; autoAcceptGroupInvitation?: boolean; - requireAck?: boolean; requireDeliveryAck?: boolean; deleteMessagesAsExitGroup?: boolean; deleteMessagesAsExitChatRoom?: boolean; @@ -477,7 +453,6 @@ export class ChatOptions { isAutoDownload?: boolean; apnsCertName?: string; pushKitCertName?: string; - pushConfig?: ChatPushConfig; areaCode?: ChatAreaCode; logTag?: string; logTimestamp?: boolean; @@ -500,9 +475,7 @@ export class ChatOptions { webSocketPort?: number; dohVendor?: number; enableUserInfo?: boolean; - enableAutoSyncContacts?: boolean; - enableChatroomConversation?: boolean; - autoLoadConversations?: boolean; + dataSyncType?: ChatDataSyncType; ntpServers?: string[]; }) { return new ChatOptions({ diff --git a/src/common/ChatPushConfig.ts b/src/common/ChatPushConfig.ts index ea797637..1707c3f9 100644 --- a/src/common/ChatPushConfig.ts +++ b/src/common/ChatPushConfig.ts @@ -1,5 +1,3 @@ -import { Platform } from 'react-native'; - /** * 推送消息的展示样式。 */ @@ -14,53 +12,6 @@ export enum ChatPushDisplayStyle { Summary, } -/** - * 推送配置类。 - * - * 谷歌的 Firebase 云信息传递 (FCM) 服务,详见 {@url https://firebase.google.com/docs/cloud-messaging/concept-options}。 - * 苹果的推送通知服务(APNs),详见 {@url https://developer.apple.com/documentation/usernotifications}。 - * 关于 React Native 平台的信息,详见 {@url https://reactnative.dev/docs/platform}。 - * - * 例如: - * ```console.log('android: ext: ', Platform.OS, (Platform as any).constants);``` - * Print result: - * ``` - * [ - * {"Brand": "google", "Fingerprint": "google/walleye/walleye:11/RP1A.200720.009/6720564:user/release-keys", "Manufacturer": "Google", "Model": "Pixel 2", "Release": "11", "Serial": "unknown", "Version": 30, "isTesting": false, "reactNativeVersion": {"major": 0, "minor": 66, "patch": 4, "prerelease": null}, "uiMode": "normal"} - * ] - * ``` - */ -export class ChatPushConfig { - /** - * 设备 ID。 - * 对于 FCM,该参数为发送方的ID;对于 APNs,该参数为证书名称。 - */ - deviceId?: string; - /** - * 设备 token。 - * 设备 token 来自第三方推送服务商的回调或主动调用方法。 - * 详见具体推送服务商的 SDK 说明。 - */ - deviceToken?: string; - /** - * 设备生产商,详见 {@link Platform.constants.Manufacturer}。 - */ - manufacturer?: string; - /** - * 构建一个推送配置实例。 - */ - constructor(params?: { deviceId?: string; deviceToken?: string }) { - this.deviceToken = params?.deviceToken; - this.deviceId = params?.deviceId; - if (Platform.OS === 'ios') { - } else if (Platform.OS === 'android') { - this.manufacturer = ( - Platform as any - ).constants.Manufacturer.toLowerCase(); - } - } -} - /** * 推送选项类。 */ diff --git a/src/common/ChatRTCTokenInfo.ts b/src/common/ChatRTCTokenInfo.ts index c812a34d..eba0207c 100644 --- a/src/common/ChatRTCTokenInfo.ts +++ b/src/common/ChatRTCTokenInfo.ts @@ -1,5 +1,5 @@ /** - * The ChatRTCTokenInfo class is used to store RTC Token information, including the user's Agora uid, the Token string, and the expiration timestamp. + * RTC Token 信息类,用于存储用户的声网 UID、Token 字符串和过期时间戳。 */ export class ChatRTCTokenInfo { rtcToken: string; @@ -7,10 +7,9 @@ export class ChatRTCTokenInfo { uid: number; /** - * - * @param rtcToken The rtc token. - * @param expireTimeStamp The expire time. - * @param uid The rtc uid + * @param rtcToken RTC Token。 + * @param expireTimeStamp 过期时间戳。 + * @param uid RTC UID。 */ constructor(params: { rtcToken: string; diff --git a/src/common/ChatRoom.ts b/src/common/ChatRoom.ts index a4cd92a4..ad812013 100644 --- a/src/common/ChatRoom.ts +++ b/src/common/ChatRoom.ts @@ -103,10 +103,6 @@ export class ChatRoom { * 聊天室黑名单列表。 */ blockList?: Array; - /** - * 聊天室禁言列表。 - */ - muteList?: Array; /** * 聊天室禁言列表。 * @@ -162,7 +158,6 @@ export class ChatRoom { adminList?: Array; memberList?: Array; blockList?: Array; - muteList?: Array; muteKVList?: Record; isAllMemberMuted?: boolean; permissionType: number; @@ -180,7 +175,6 @@ export class ChatRoom { this.adminList = params.adminList; this.memberList = params.memberList; this.blockList = params.blockList; - this.muteList = params.muteList; this.muteKVList = params.muteKVList; this.isAllMemberMuted = params.isAllMemberMuted; this.permissionType = ChatRoomPermissionTypeFromNumber( diff --git a/src/index.ts b/src/index.ts index 04e72f76..716fd558 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,6 @@ export * from './ChatPresenceManager'; export * from './ChatPushManager'; export * from './ChatRoomManager'; export * from './ChatUserInfoManager'; -export * from './common/ChatAreaCode'; export * from './common/ChatConst'; export * from './common/ChatContact'; export * from './common/ChatConversation'; @@ -15,9 +14,11 @@ export * from './common/ChatCursorResult'; export * from './common/ChatDeviceInfo'; export * from './common/ChatError'; export * from './common/ChatGroup'; +export * from './common/ChatGroupMemberInfo'; export * from './common/ChatLog'; export * from './common/ChatMessage'; export * from './common/ChatMessageReaction'; +export * from './common/ChatMessageReadReceipt'; export * from './common/ChatMessageSearchOption'; export * from './common/ChatMessageSenderInfo'; export * from './common/ChatMessageStreamChunk';