feat(android-sdk): 添加完整的IM客户端SDK实现
- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能 - 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力 - 实现了群组管理功能,包括创建、成员管理、权限设置等操作 - 添加了好友关系链管理,支持添加、删除、分组等操作 - 实现了会话管理功能,包括置顶、免打扰、已读状态等 - 添加了黑名单、资料管理、搜索等辅助功能 - 补齐了批量操作接口,提升客户端操作效率 - 实现了WebSocket连接管理和事件监听机制 - 添加了离线消息同步和状态管理功能
这个提交包含在:
父节点
293f9d6a96
当前提交
a58f920a3f
@ -110,6 +110,23 @@ export class ImClient {
|
||||
)
|
||||
}
|
||||
|
||||
sync() {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) return
|
||||
if (!this.activeAppId) return
|
||||
this.sendFrame(
|
||||
'SEND',
|
||||
{
|
||||
destination: '/app/chat.sync',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
JSON.stringify({ appId: this.activeAppId }),
|
||||
)
|
||||
}
|
||||
|
||||
private sendSync() {
|
||||
this.sync()
|
||||
}
|
||||
|
||||
subscribeGroup(groupId: string) {
|
||||
const alreadySubscribed = this.groupSubscriptions.has(groupId)
|
||||
this.groupSubscriptions.add(groupId)
|
||||
@ -204,6 +221,7 @@ export class ImClient {
|
||||
this.groupSubscriptions.forEach(groupId => {
|
||||
this.subscribe(`/topic/group/${groupId}`, `group-${groupId}`)
|
||||
})
|
||||
this.sendSync()
|
||||
this.listeners.forEach(listener => listener.onConnected?.())
|
||||
return
|
||||
}
|
||||
|
||||
@ -701,6 +701,29 @@ export const ImSDK = {
|
||||
return msg
|
||||
},
|
||||
|
||||
async syncOfflineMessages(maxCount = 100): Promise<ImMessage[]> {
|
||||
const config = getConfig()
|
||||
const res = await apiRequest<ImMessage[] | { data?: ImMessage[] }>('/api/im/messages/offline', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId, maxCount: String(maxCount) },
|
||||
})
|
||||
const messages = Array.isArray(res) ? res : (res.data ?? [])
|
||||
if (ImDatabase.isInitialized() && _currentUserId) {
|
||||
for (const msg of messages) {
|
||||
await ImDatabase.saveMessage(normalizeMessage(msg), _currentUserId)
|
||||
}
|
||||
}
|
||||
return messages
|
||||
},
|
||||
|
||||
async offlineMessageCount(): Promise<number> {
|
||||
const config = getConfig()
|
||||
const res = await apiRequest<{ count: number }>('/api/im/messages/offline/count', {
|
||||
params: { appId: config.appId },
|
||||
})
|
||||
return res.count ?? 0
|
||||
},
|
||||
|
||||
async createGroup(name: string, memberIds: string[], groupType = 'WORK'): Promise<ImGroup> {
|
||||
const config = getConfig()
|
||||
return apiRequest<ImGroup>('/api/im/groups', {
|
||||
@ -1212,6 +1235,90 @@ export const ImSDK = {
|
||||
return Array.isArray(res) ? res : (res.content ?? [])
|
||||
},
|
||||
|
||||
async batchAddFriends(friendIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>('/api/im/friends/batch', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { friendIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchRemoveFriends(friendIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>('/api/im/friends/batch/remove', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { friendIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchAcceptFriendRequests(requestIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>('/api/im/friend-requests/batch/accept', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { requestIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchRejectFriendRequests(requestIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>('/api/im/friend-requests/batch/reject', {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { requestIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchAddGroupMembers(groupId: string, userIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/members/batch`, {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { userIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchRemoveGroupMembers(groupId: string, userIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/members/batch/remove`, {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { userIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchAcceptGroupJoinRequests(groupId: string, requestIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/batch/accept`, {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { requestIds },
|
||||
})
|
||||
},
|
||||
|
||||
async batchRejectGroupJoinRequests(groupId: string, requestIds: string[]): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/join-requests/batch/reject`, {
|
||||
method: 'POST',
|
||||
params: { appId: config.appId },
|
||||
body: { requestIds },
|
||||
})
|
||||
},
|
||||
|
||||
async modifyGroupMemberInfo(groupId: string, userId: string, nickname?: string, role?: string): Promise<void> {
|
||||
const config = getConfig()
|
||||
const body: Record<string, string> = {}
|
||||
if (nickname !== undefined) body.nickname = nickname
|
||||
if (role !== undefined) body.role = role
|
||||
await apiRequest<void>(`/api/im/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}/info`, {
|
||||
method: 'PUT',
|
||||
params: { appId: config.appId },
|
||||
body,
|
||||
})
|
||||
},
|
||||
|
||||
async getDraft(targetId: string, chatType: ChatType): Promise<string> {
|
||||
const config = getConfig()
|
||||
if (ImDatabase.isInitialized()) {
|
||||
|
||||
@ -20,6 +20,8 @@ export const listConversationGroups = (): ReturnType<typeof _ImSDK.listConversat
|
||||
export const listConversationGroupItems = (groupName: string): ReturnType<typeof _ImSDK.listConversationGroupItems> => _ImSDK.listConversationGroupItems(groupName)
|
||||
export const locateHistoryPage = (toId: string, messageId: string, pageSize?: number, maxPages?: number): ReturnType<typeof _ImSDK.locateHistoryPage> => _ImSDK.locateHistoryPage(toId, messageId, pageSize, maxPages)
|
||||
export const locateGroupHistoryPage = (groupId: string, messageId: string, pageSize?: number, maxPages?: number): ReturnType<typeof _ImSDK.locateGroupHistoryPage> => _ImSDK.locateGroupHistoryPage(groupId, messageId, pageSize, maxPages)
|
||||
export const syncOfflineMessages = (maxCount?: number): ReturnType<typeof _ImSDK.syncOfflineMessages> => _ImSDK.syncOfflineMessages(maxCount)
|
||||
export const offlineMessageCount = (): ReturnType<typeof _ImSDK.offlineMessageCount> => _ImSDK.offlineMessageCount()
|
||||
export { ImClient } from './ImClient'
|
||||
export { ImDatabase } from './db/ImDatabase'
|
||||
export type { MessageSearchParams } from './db/ImDatabase'
|
||||
|
||||
@ -0,0 +1,224 @@
|
||||
package com.xuqm.push;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
public class XuqmPushModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private static final String TAG = "XuqmPushModule";
|
||||
private static final String EVENT_TOKEN = "XuqmPushToken";
|
||||
|
||||
public XuqmPushModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "XuqmPushModule";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void detectVendor(Promise promise) {
|
||||
String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toUpperCase() : "";
|
||||
String vendor;
|
||||
switch (manufacturer) {
|
||||
case "HUAWEI":
|
||||
vendor = "HUAWEI";
|
||||
break;
|
||||
case "XIAOMI":
|
||||
case "REDMI":
|
||||
vendor = "XIAOMI";
|
||||
break;
|
||||
case "HONOR":
|
||||
vendor = "HONOR";
|
||||
break;
|
||||
case "OPPO":
|
||||
case "REALME":
|
||||
vendor = "OPPO";
|
||||
break;
|
||||
case "VIVO":
|
||||
case "IQOO":
|
||||
vendor = "VIVO";
|
||||
break;
|
||||
default:
|
||||
vendor = "FCM";
|
||||
break;
|
||||
}
|
||||
promise.resolve(vendor);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void registerPush(Promise promise) {
|
||||
Context context = getReactApplicationContext();
|
||||
String vendor = resolveVendor();
|
||||
try {
|
||||
switch (vendor) {
|
||||
case "HUAWEI":
|
||||
registerHuawei(context);
|
||||
break;
|
||||
case "XIAOMI":
|
||||
registerXiaomi(context);
|
||||
break;
|
||||
case "OPPO":
|
||||
registerOppo(context);
|
||||
break;
|
||||
case "VIVO":
|
||||
registerVivo(context);
|
||||
break;
|
||||
case "HONOR":
|
||||
registerHonor(context);
|
||||
break;
|
||||
default:
|
||||
registerFcm(context);
|
||||
break;
|
||||
}
|
||||
promise.resolve(null);
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "registerPush failed: " + e.getMessage());
|
||||
promise.reject("PUSH_REGISTER_ERROR", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveVendor() {
|
||||
String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toUpperCase() : "";
|
||||
switch (manufacturer) {
|
||||
case "HUAWEI": return "HUAWEI";
|
||||
case "XIAOMI":
|
||||
case "REDMI": return "XIAOMI";
|
||||
case "HONOR": return "HONOR";
|
||||
case "OPPO":
|
||||
case "REALME": return "OPPO";
|
||||
case "VIVO":
|
||||
case "IQOO": return "VIVO";
|
||||
default: return "FCM";
|
||||
}
|
||||
}
|
||||
|
||||
private void emitToken(String token, String vendor) {
|
||||
com.facebook.react.bridge.WritableMap map = new com.facebook.react.bridge.WritableNativeMap();
|
||||
map.putString("token", token);
|
||||
map.putString("vendor", vendor);
|
||||
getReactApplicationContext()
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(EVENT_TOKEN, map);
|
||||
}
|
||||
|
||||
// ── Vendor registrations (via reflection) ────────────────────────────────
|
||||
|
||||
private void registerHuawei(Context context) throws Exception {
|
||||
Class<?> hmsClass = Class.forName("com.huawei.hms.aaid.HmsInstanceId");
|
||||
Object instance = hmsClass.getMethod("getInstance", Context.class).invoke(null, context);
|
||||
String appId = getHuaweiAppId(context);
|
||||
if (appId.isEmpty()) {
|
||||
Log.w(TAG, "Huawei appId not found");
|
||||
return;
|
||||
}
|
||||
Object token = hmsClass.getMethod("getToken", String.class, String.class)
|
||||
.invoke(instance, appId, "HCM");
|
||||
if (token instanceof String && !((String) token).isEmpty()) {
|
||||
emitToken((String) token, "HUAWEI");
|
||||
}
|
||||
}
|
||||
|
||||
private String getHuaweiAppId(Context context) {
|
||||
try {
|
||||
Class<?> configClass = Class.forName("com.huawei.agconnect.config.AGConnectServicesConfig");
|
||||
Object config = configClass.getMethod("fromContext", Context.class).invoke(null, context);
|
||||
return (String) configClass.getMethod("getString", String.class).invoke(config, "client/app_id");
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void registerXiaomi(Context context) throws Exception {
|
||||
Class<?> miPushClass = Class.forName("com.xiaomi.mipush.sdk.MiPushClient");
|
||||
Bundle meta = context.getPackageManager().getApplicationInfo(
|
||||
context.getPackageName(), PackageManager.GET_META_DATA).metaData;
|
||||
if (meta == null) return;
|
||||
String appId = meta.getString("XUQM_XIAOMI_APP_ID", "");
|
||||
String appKey = meta.getString("XUQM_XIAOMI_APP_KEY", "");
|
||||
if (!appId.isEmpty() && !appKey.isEmpty()) {
|
||||
miPushClass.getMethod("registerPush", Context.class, String.class, String.class)
|
||||
.invoke(null, context, appId, appKey);
|
||||
} else {
|
||||
Log.w(TAG, "Xiaomi appId/appKey not configured in meta-data");
|
||||
}
|
||||
}
|
||||
|
||||
private void registerOppo(Context context) throws Exception {
|
||||
Class<?> pushManagerClass = Class.forName("com.heytap.mcssdk.PushManager");
|
||||
Object instance = pushManagerClass.getMethod("getInstance").invoke(null);
|
||||
Bundle meta = context.getPackageManager().getApplicationInfo(
|
||||
context.getPackageName(), PackageManager.GET_META_DATA).metaData;
|
||||
if (meta == null) return;
|
||||
String appKey = meta.getString("XUQM_OPPO_APP_KEY", "");
|
||||
String appSecret = meta.getString("XUQM_OPPO_APP_SECRET", "");
|
||||
if (appKey.isEmpty() || appSecret.isEmpty()) {
|
||||
Log.w(TAG, "OPPO appKey/appSecret not configured in meta-data");
|
||||
return;
|
||||
}
|
||||
Class<?> callbackClass = Class.forName("com.heytap.mcssdk.callback.PushCallback");
|
||||
Object proxy = Proxy.newProxyInstance(
|
||||
callbackClass.getClassLoader(),
|
||||
new Class<?>[]{callbackClass},
|
||||
(proxy1, method, args) -> {
|
||||
if ("onRegister".equals(method.getName()) && args != null && args.length > 1) {
|
||||
String regId = args[1] instanceof String ? (String) args[1] : null;
|
||||
if (regId != null && !regId.isEmpty()) {
|
||||
emitToken(regId, "OPPO");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
pushManagerClass.getMethod("register", Context.class, String.class, String.class, callbackClass)
|
||||
.invoke(instance, context, appKey, appSecret, proxy);
|
||||
}
|
||||
|
||||
private void registerVivo(Context context) throws Exception {
|
||||
Class<?> pushClientClass = Class.forName("com.vivo.push.PushClient");
|
||||
Object instance = pushClientClass.getMethod("getInstance", Context.class).invoke(null, context);
|
||||
pushClientClass.getMethod("initialize").invoke(instance);
|
||||
}
|
||||
|
||||
private void registerHonor(Context context) throws Exception {
|
||||
Class<?> honorPushClass = Class.forName("com.hihonor.push.sdk.HonorPushClient");
|
||||
Object client = honorPushClass.getMethod("getInstance").invoke(null);
|
||||
honorPushClass.getMethod("init", Context.class, boolean.class).invoke(client, context, true);
|
||||
}
|
||||
|
||||
private void registerFcm(Context context) throws Exception {
|
||||
Class<?> fcmClass = Class.forName("com.google.firebase.messaging.FirebaseMessaging");
|
||||
Object instance = fcmClass.getMethod("getInstance").invoke(null);
|
||||
Object task = fcmClass.getMethod("getToken").invoke(instance);
|
||||
Class<?> onSuccessClass = Class.forName("com.google.android.gms.tasks.OnSuccessListener");
|
||||
Object proxy = Proxy.newProxyInstance(
|
||||
onSuccessClass.getClassLoader(),
|
||||
new Class<?>[]{onSuccessClass},
|
||||
(proxy1, method, args) -> {
|
||||
if (args != null && args.length > 0 && args[0] instanceof String) {
|
||||
String token = (String) args[0];
|
||||
if (!token.isEmpty()) {
|
||||
emitToken(token, "FCM");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
task.getClass().getMethod("addOnSuccessListener", onSuccessClass).invoke(task, proxy);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface XuqmPushModule : RCTEventEmitter <RCTBridgeModule>
|
||||
@end
|
||||
|
||||
@implementation XuqmPushModule
|
||||
|
||||
RCT_EXPORT_MODULE(XuqmPushModule);
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[@"XuqmPushToken"];
|
||||
}
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup {
|
||||
return NO;
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(detectVendor:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
NSString *vendor = @"APNS";
|
||||
#if TARGET_OS_SIMULATOR
|
||||
vendor = @"FCM";
|
||||
#endif
|
||||
resolve(vendor);
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(registerPush:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||
resolve(@(YES));
|
||||
});
|
||||
}
|
||||
|
||||
// Called by the AppDelegate to report the device token
|
||||
- (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
const unsigned char *dataBuffer = (const unsigned char *)[deviceToken bytes];
|
||||
NSUInteger dataLength = [deviceToken length];
|
||||
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
|
||||
for (NSUInteger i = 0; i < dataLength; ++i) {
|
||||
[hexString appendFormat:@"%02x", dataBuffer[i]];
|
||||
}
|
||||
NSString *token = [hexString copy];
|
||||
[self sendEventWithName:@"XuqmPushToken" body:@{@"token": token, @"vendor": @"APNS"}];
|
||||
}
|
||||
|
||||
@end
|
||||
33
packages/push/src/NativePush.ts
普通文件
33
packages/push/src/NativePush.ts
普通文件
@ -0,0 +1,33 @@
|
||||
import { NativeModules, NativeEventEmitter, Platform } from 'react-native'
|
||||
|
||||
interface XuqmPushModuleInterface {
|
||||
detectVendor: () => Promise<string>
|
||||
registerPush: () => Promise<boolean>
|
||||
}
|
||||
|
||||
const _native = NativeModules.XuqmPushModule as XuqmPushModuleInterface | undefined
|
||||
const _eventEmitter = _native ? new NativeEventEmitter(_native as any) : null
|
||||
|
||||
export function isNativePushAvailable(): boolean {
|
||||
return !!_native
|
||||
}
|
||||
|
||||
export async function detectVendorNative(): Promise<string> {
|
||||
if (!_native?.detectVendor) return Platform.OS === 'ios' ? 'APNS' : 'FCM'
|
||||
return _native.detectVendor()
|
||||
}
|
||||
|
||||
export async function registerPushNative(): Promise<void> {
|
||||
if (!_native?.registerPush) return
|
||||
await _native.registerPush()
|
||||
}
|
||||
|
||||
export function addPushTokenListener(
|
||||
callback: (event: { token: string; vendor: string }) => void,
|
||||
): () => void {
|
||||
if (!_eventEmitter) {
|
||||
return () => {}
|
||||
}
|
||||
const subscription = _eventEmitter.addListener('XuqmPushToken', callback)
|
||||
return () => subscription.remove()
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { apiRequest, getConfig, getDeviceInfo, getUserId as getCommonUserId } from '@xuqm/rn-common'
|
||||
import type { PushVendor } from '@xuqm/rn-common'
|
||||
import { detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush'
|
||||
|
||||
export type { PushVendor }
|
||||
|
||||
@ -10,6 +11,7 @@ type PendingDeviceToken = {
|
||||
|
||||
let currentUserId: string | null = null
|
||||
let pendingToken: PendingDeviceToken | null = null
|
||||
let _tokenUnsubscribe: (() => void) | null = null
|
||||
|
||||
async function registerPendingToken(): Promise<void> {
|
||||
const userId = currentUserId ?? getCommonUserId()
|
||||
@ -43,6 +45,36 @@ export const PushSDK = {
|
||||
await registerPendingToken()
|
||||
},
|
||||
|
||||
/**
|
||||
* Auto-detect vendor and request native push registration.
|
||||
* On Android this attempts to register with the vendor SDK (Huawei, Xiaomi, OPPO, vivo, Honor, FCM).
|
||||
* On iOS this requests APNs registration.
|
||||
* Listen for token updates via onPushToken(callback).
|
||||
*/
|
||||
async requestNativeRegistration(): Promise<void> {
|
||||
await registerPushNative()
|
||||
},
|
||||
|
||||
/**
|
||||
* Listen for push tokens from the native layer.
|
||||
* Call setDeviceToken(token, vendor) inside the callback to register with the server.
|
||||
*/
|
||||
onPushToken(callback: (token: string, vendor: string) => void): () => void {
|
||||
if (_tokenUnsubscribe) {
|
||||
_tokenUnsubscribe()
|
||||
_tokenUnsubscribe = null
|
||||
}
|
||||
_tokenUnsubscribe = addPushTokenListener((event) => {
|
||||
callback(event.token, event.vendor)
|
||||
})
|
||||
return () => {
|
||||
if (_tokenUnsubscribe) {
|
||||
_tokenUnsubscribe()
|
||||
_tokenUnsubscribe = null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a push device token for the given user.
|
||||
* If vendor is omitted, it is auto-detected from the device brand.
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export { PushSDK } from './PushSDK'
|
||||
export type { PushVendor } from './PushSDK'
|
||||
export { isNativePushAvailable, detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush'
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户