feat(notification): migrate from expo-notifications to JPush push service
Replace expo-notifications with JPush for push notifications across the app. This includes adding JPush native module dependencies, configuring the JPush plugin in app.json, implementing jpushService wrapper, and updating notification services to use JPush APIs. Also adds device registration on token refresh for both mobile and web platforms. BREAKING CHANGE: expo-notifications removed in favor of JPush for push notifications
This commit is contained in:
21
app.json
21
app.json
@@ -35,6 +35,7 @@
|
||||
"monochromeImage": "./assets/android-icon-monochrome.png"
|
||||
},
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"jsEngine": "hermes",
|
||||
"package": "cn.qczlit.withyou",
|
||||
"versionCode": 7,
|
||||
"permissions": [
|
||||
@@ -55,7 +56,9 @@
|
||||
"android.permission.ACCESS_MEDIA_LOCATION",
|
||||
"android.permission.READ_MEDIA_IMAGES",
|
||||
"android.permission.READ_MEDIA_VIDEO",
|
||||
"android.permission.READ_MEDIA_AUDIO"
|
||||
"android.permission.READ_MEDIA_AUDIO",
|
||||
"android.permission.ACCESS_NETWORK_STATE",
|
||||
"android.permission.ACCESS_WIFI_STATE"
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
@@ -109,7 +112,7 @@
|
||||
"isAccessMediaLocationEnabled": true
|
||||
}
|
||||
],
|
||||
[
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
"photosPermission": "允许威友访问您的照片以选择图片",
|
||||
@@ -138,13 +141,21 @@
|
||||
"proguardRules": "-keep class com.google.android.exoplayer2.** { *; }"
|
||||
}
|
||||
],
|
||||
"expo-font"
|
||||
"expo-font",
|
||||
[
|
||||
"./plugins/withJPush",
|
||||
{
|
||||
"appKey": "2cbe4da12e2c24aa3dca4610",
|
||||
"channel": "developer-default"
|
||||
}
|
||||
]
|
||||
],
|
||||
"jsEngine": "hermes",
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "65540196-d37d-437b-8496-227df0317069"
|
||||
}
|
||||
},
|
||||
"jpushAppKey": "2cbe4da12e2c24aa3dca4610",
|
||||
"jpushChannel": "developer-default"
|
||||
},
|
||||
"owner": "qojo"
|
||||
}
|
||||
|
||||
@@ -1,19 +1,83 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { Redirect } from 'expo-router';
|
||||
|
||||
import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
|
||||
import { messageManager, useAuthStore } from '../../src/stores';
|
||||
import { jpushService } from '../../src/services/notification/jpushService';
|
||||
|
||||
function getDeviceType(): 'ios' | 'android' | 'web' {
|
||||
switch (Platform.OS) {
|
||||
case 'ios': return 'ios';
|
||||
case 'android': return 'android';
|
||||
default: return 'web';
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrCreateDeviceID(): Promise<string> {
|
||||
try {
|
||||
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
||||
const DEVICE_ID_KEY = 'withyou_device_id';
|
||||
let deviceId = await AsyncStorage.getItem(DEVICE_ID_KEY);
|
||||
if (deviceId) return deviceId;
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
deviceId = `${Platform.OS}_${timestamp}_${random}`;
|
||||
await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||
return deviceId;
|
||||
} catch {
|
||||
return `${Platform.OS}_${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default function AppLayout() {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const userID = useAuthStore((s) => s.currentUser?.id);
|
||||
const deviceRegistered = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
messageManager.initialize();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !userID) return;
|
||||
|
||||
if (!deviceRegistered.current) {
|
||||
deviceRegistered.current = true;
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
// Web: register device without push_token (JPush not available on web)
|
||||
getOrCreateDeviceID().then((deviceId) => {
|
||||
const { pushService } = require('../../src/services/notification/pushService');
|
||||
pushService.registerDevice({
|
||||
device_id: deviceId,
|
||||
device_type: 'web',
|
||||
device_name: 'Web Browser',
|
||||
}).catch((err: any) => {
|
||||
console.warn('[Push] web device register failed:', err?.message);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Mobile: JPush handles registration internally in initialize()
|
||||
jpushService.initialize().then((ok) => {
|
||||
if (ok && userID) {
|
||||
jpushService.setAliasForUser(userID);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isAuthenticated) {
|
||||
deviceRegistered.current = false;
|
||||
jpushService.cleanup();
|
||||
}
|
||||
};
|
||||
}, [isAuthenticated, userID]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
|
||||
return <AppRouteStack />;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { PaperProvider } from 'react-native-paper';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import * as SystemUI from 'expo-system-ui';
|
||||
import { useFonts } from 'expo-font';
|
||||
|
||||
@@ -27,6 +26,7 @@ import { installAlertOverride } from '@/services/ui';
|
||||
import { useAuthStore } from '../src/stores';
|
||||
import { checkForAPKUpdate } from '@/services/platform';
|
||||
import { CallScreen, IncomingCallModal, FloatingCallWindow } from '../src/components/call';
|
||||
import { jpushService } from '@/services/notification/jpushService';
|
||||
|
||||
registerNotificationPresentationHandler();
|
||||
|
||||
@@ -128,7 +128,6 @@ function SessionGate({ children }: { children: React.ReactNode }) {
|
||||
|
||||
function NotificationBootstrap() {
|
||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||
const notificationResponseListener = useRef<Notifications.EventSubscription | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const initNotifications = async () => {
|
||||
@@ -146,22 +145,13 @@ function NotificationBootstrap() {
|
||||
appState.current = nextAppState;
|
||||
});
|
||||
|
||||
notificationResponseListener.current = Notifications.addNotificationResponseReceivedListener(
|
||||
(response) => {
|
||||
void response.notification.request.content.data;
|
||||
}
|
||||
);
|
||||
|
||||
const notificationReceivedSubscription = Notifications.addNotificationReceivedListener(
|
||||
(notification) => {
|
||||
void notification;
|
||||
}
|
||||
);
|
||||
// Listen for JPush notification taps
|
||||
jpushService.onNotification((message) => {
|
||||
console.log('[NotificationBootstrap] JPush notification tapped:', message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
notificationResponseListener.current?.remove();
|
||||
notificationReceivedSubscription.remove();
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
59
package-lock.json
generated
59
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "with_you",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "with_you",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.1.1",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
@@ -31,7 +31,6 @@
|
||||
"expo-intent-launcher": "~55.0.9",
|
||||
"expo-linear-gradient": "^55.0.8",
|
||||
"expo-media-library": "~55.0.9",
|
||||
"expo-notifications": "^55.0.10",
|
||||
"expo-router": "^55.0.4",
|
||||
"expo-sqlite": "~55.0.10",
|
||||
"expo-status-bar": "~55.0.4",
|
||||
@@ -39,6 +38,8 @@
|
||||
"expo-task-manager": "~55.0.9",
|
||||
"expo-updates": "^55.0.12",
|
||||
"expo-video": "^55.0.10",
|
||||
"jcore-react-native": "^2.3.5",
|
||||
"jpush-react-native": "^3.2.6",
|
||||
"katex": "^0.16.42",
|
||||
"markdown-it": "^14.1.1",
|
||||
"prismjs": "^1.30.0",
|
||||
@@ -4357,12 +4358,6 @@
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/badgin": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/badgin/-/badgin-1.2.3.tgz",
|
||||
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
@@ -5522,15 +5517,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-application": {
|
||||
"version": "55.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/expo-application/-/expo-application-55.0.10.tgz",
|
||||
"integrity": "sha512-5ccf+S6hsQz+doi907TOJxKzV5AKgAgw004z4FoDWSoGhfab0LUPg6uyvOspuU4cbNvqw8EAy08hZbVO8nKc9Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-asset": {
|
||||
"version": "55.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/expo-asset/-/expo-asset-55.0.10.tgz",
|
||||
@@ -5845,24 +5831,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-notifications": {
|
||||
"version": "55.0.13",
|
||||
"resolved": "https://registry.npmmirror.com/expo-notifications/-/expo-notifications-55.0.13.tgz",
|
||||
"integrity": "sha512-vbtSBcMkYtNTO+6WKdeOzysOqvtmiq/sQrUKJpYcB75m9hBFcAfI2klpXdUiGg5kMr/ygBmFENSolQt1B9QY8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/image-utils": "^0.8.12",
|
||||
"abort-controller": "^3.0.0",
|
||||
"badgin": "^1.1.5",
|
||||
"expo-application": "~55.0.10",
|
||||
"expo-constants": "~55.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-router": {
|
||||
"version": "55.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/expo-router/-/expo-router-55.0.7.tgz",
|
||||
@@ -7353,6 +7321,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jcore-react-native": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/jcore-react-native/-/jcore-react-native-2.3.5.tgz",
|
||||
"integrity": "sha512-rxkDPGiSyNVHjNibUGeqFaV07PbMqKZnoHJa0srt002QGUH4/9K4Ux2tK0EkqHLFvGFsJqGWwjaxEqQIPvoPTA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react-native": ">= 0.60"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz",
|
||||
@@ -7558,6 +7535,16 @@
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jpush-react-native": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/jpush-react-native/-/jpush-react-native-3.2.6.tgz",
|
||||
"integrity": "sha512-dnrdfwsZcAYsQLknVMRMFml/f5cDUFlVKaqpgN41au+g2lxYvPyY7v15XZbNiMdnAjzzVgPhmUW8kuptkLtDXw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"jcore-react-native": ">= 1.9.3",
|
||||
"react-native": ">= 0.50"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "with_you",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
@@ -37,7 +37,6 @@
|
||||
"expo-intent-launcher": "~55.0.9",
|
||||
"expo-linear-gradient": "^55.0.8",
|
||||
"expo-media-library": "~55.0.9",
|
||||
"expo-notifications": "^55.0.10",
|
||||
"expo-router": "^55.0.4",
|
||||
"expo-sqlite": "~55.0.10",
|
||||
"expo-status-bar": "~55.0.4",
|
||||
@@ -45,6 +44,8 @@
|
||||
"expo-task-manager": "~55.0.9",
|
||||
"expo-updates": "^55.0.12",
|
||||
"expo-video": "^55.0.10",
|
||||
"jcore-react-native": "^2.3.5",
|
||||
"jpush-react-native": "^3.2.6",
|
||||
"katex": "^0.16.42",
|
||||
"markdown-it": "^14.1.1",
|
||||
"prismjs": "^1.30.0",
|
||||
|
||||
155
plugins/withJPush.js
Normal file
155
plugins/withJPush.js
Normal file
@@ -0,0 +1,155 @@
|
||||
const {
|
||||
withAndroidManifest,
|
||||
withDangerousMod,
|
||||
withAppDelegate,
|
||||
withInfoPlist,
|
||||
withAppBuildGradle,
|
||||
} = require('@expo/config-plugins');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const withJPush = (config, options = {}) => {
|
||||
const { appKey, channel = 'developer-default' } = options;
|
||||
|
||||
if (!appKey) {
|
||||
throw new Error('JPush appKey is required in plugin options');
|
||||
}
|
||||
|
||||
// 1. Android: Inject manifestPlaceholders into app/build.gradle
|
||||
config = withAppBuildGradle(config, (config) => {
|
||||
const block = '\n manifestPlaceholders = [\n' +
|
||||
' JPUSH_APPKEY: "' + appKey + '",\n' +
|
||||
' JPUSH_CHANNEL: "' + channel + '"\n' +
|
||||
' ]';
|
||||
|
||||
const contents = config.modResults.contents;
|
||||
|
||||
if (contents.includes('manifestPlaceholders')) {
|
||||
config.modResults.contents = contents.replace(
|
||||
/manifestPlaceholders\s*=\s*\[[\s\S]*?\]/,
|
||||
block.trim()
|
||||
);
|
||||
} else if (contents.includes('REACT_NATIVE_RELEASE_LEVEL')) {
|
||||
const lines = contents.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].includes('REACT_NATIVE_RELEASE_LEVEL')) {
|
||||
lines.splice(i + 1, 0, block);
|
||||
break;
|
||||
}
|
||||
}
|
||||
config.modResults.contents = lines.join('\n');
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// 2. Android: Add JPush metadata and permissions to AndroidManifest
|
||||
config = withAndroidManifest(config, (config) => {
|
||||
const manifest = config.modResults;
|
||||
|
||||
if (!manifest.manifest['uses-permission']) {
|
||||
manifest.manifest['uses-permission'] = [];
|
||||
}
|
||||
|
||||
const requiredPermissions = [
|
||||
'android.permission.RECEIVE_USER_PRESENT',
|
||||
'android.permission.POST_NOTIFICATIONS',
|
||||
];
|
||||
|
||||
requiredPermissions.forEach((permission) => {
|
||||
const exists = manifest.manifest['uses-permission'].find(
|
||||
(p) => p.$['android:name'] === permission
|
||||
);
|
||||
if (!exists) {
|
||||
manifest.manifest['uses-permission'].push({
|
||||
$: { 'android:name': permission },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// 3. iOS: Add JPush initialization to AppDelegate
|
||||
config = withAppDelegate(config, (config) => {
|
||||
const { contents } = config.modResults;
|
||||
|
||||
if (!contents.includes('JPush')) {
|
||||
let newContents = contents;
|
||||
|
||||
if (!newContents.includes('#import <UserNotifications/UserNotifications.h>')) {
|
||||
newContents = newContents.replace(
|
||||
/(#import "AppDelegate.h")/,
|
||||
`$1\n#import <UserNotifications/UserNotifications.h>`
|
||||
);
|
||||
}
|
||||
|
||||
if (!newContents.includes('#import <jcore-react-native/JCoreModule.h>')) {
|
||||
newContents = newContents.replace(
|
||||
/(#import "AppDelegate.h")/,
|
||||
`$1\n#import <jcore-react-native/JCoreModule.h>`
|
||||
);
|
||||
}
|
||||
|
||||
const didFinishLaunchPattern = /(- \(BOOL\)application:\(UIApplication \*\)application didFinishLaunchingWithOptions:\(NSDictionary \*\)launchOptions\s*\{)/;
|
||||
if (didFinishLaunchPattern.test(newContents)) {
|
||||
newContents = newContents.replace(
|
||||
didFinishLaunchPattern,
|
||||
`$1\n // JPush initialization\n [JCoreModule setupWithOption:launchOptions appKey:@"${appKey}" channel:@"${channel}" apsForProduction:NO];\n`
|
||||
);
|
||||
}
|
||||
|
||||
const jpushMethods = `
|
||||
// JPush: Register for remote notifications
|
||||
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||
[JCoreModule registerDeviceToken:deviceToken];
|
||||
}
|
||||
|
||||
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
|
||||
NSLog(@"did fail to register for remote notifications with error: %@", error);
|
||||
}
|
||||
|
||||
// JPush: Handle remote notification
|
||||
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
|
||||
[JCoreModule didReceiveRemoteNotification:userInfo];
|
||||
completionHandler(UIBackgroundFetchResultNewData);
|
||||
}
|
||||
|
||||
// UNUserNotificationCenter delegate
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
|
||||
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound);
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
|
||||
[JCoreModule didReceiveRemoteNotification:response.notification.request.content.userInfo];
|
||||
completionHandler();
|
||||
}
|
||||
`;
|
||||
|
||||
if (!newContents.includes('JCoreModule registerDeviceToken')) {
|
||||
newContents = newContents.replace(
|
||||
/@end\s*$/,
|
||||
`${jpushMethods}\n@end\n`
|
||||
);
|
||||
}
|
||||
|
||||
config.modResults.contents = newContents;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// 4. iOS: Add required capabilities and permissions to Info.plist
|
||||
config = withInfoPlist(config, (config) => {
|
||||
config.modResults.UIBackgroundModes = config.modResults.UIBackgroundModes || [];
|
||||
if (!config.modResults.UIBackgroundModes.includes('remote-notification')) {
|
||||
config.modResults.UIBackgroundModes.push('remote-notification');
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
module.exports = withJPush;
|
||||
@@ -240,7 +240,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const styles = useMemo(() => createHomeStyles(colors, responsivePadding), [colors, responsivePadding]);
|
||||
const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
|
||||
|
||||
const [sortIndex, setSortIndex] = useState(0);
|
||||
const [sortIndex, setSortIndex] = useState(2);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]);
|
||||
const [activeCapsuleId, setActiveCapsuleId] = useState('');
|
||||
|
||||
@@ -356,10 +356,10 @@ class ApiClient {
|
||||
const data = await response.json();
|
||||
if (data.code === 0 && data.data?.token) {
|
||||
await this.setToken(data.data.token);
|
||||
// 后端返回refresh_token (snake_case),兼容两种命名
|
||||
if (data.data.refresh_token || data.data.refreshToken) {
|
||||
await this.setRefreshToken(data.data.refresh_token || data.data.refreshToken);
|
||||
}
|
||||
this.registerDeviceOnRefresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -370,6 +370,52 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 冷启动/刷新token后自动注册设备,方便存量设备接入
|
||||
private registerDeviceOnRefresh(): void {
|
||||
(async () => {
|
||||
try {
|
||||
const { pushService } = await import('../notification/pushService');
|
||||
const { jpushService } = await import('../notification/jpushService');
|
||||
const AsyncStorage = (await import('@react-native-async-storage/async-storage')).default;
|
||||
|
||||
const DEVICE_ID_KEY = 'withyou_device_id';
|
||||
let deviceId = await AsyncStorage.getItem(DEVICE_ID_KEY);
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
if (!deviceId) {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
deviceId = `web_${timestamp}_${random}`;
|
||||
await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||
}
|
||||
await pushService.registerDevice({
|
||||
device_id: deviceId,
|
||||
device_type: 'web',
|
||||
device_name: 'Web Browser',
|
||||
});
|
||||
} else {
|
||||
// Mobile: use JPush registration ID if available
|
||||
const regID = jpushService.getRegistrationIDValue();
|
||||
if (!deviceId) {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
deviceId = `${Platform.OS}_${timestamp}_${random}`;
|
||||
await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||
}
|
||||
await pushService.registerDevice({
|
||||
device_id: deviceId,
|
||||
device_type: Platform.OS === 'ios' ? 'ios' : 'android',
|
||||
...(regID ? { push_token: regID } : {}),
|
||||
device_name: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} Device`,
|
||||
});
|
||||
}
|
||||
console.log('[API] device registered on token refresh');
|
||||
} catch (error) {
|
||||
console.warn('[API] device register on refresh failed:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// GET 请求
|
||||
async get<T>(path: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
|
||||
return this.request<T>('GET', path, params);
|
||||
|
||||
@@ -14,4 +14,6 @@ export {
|
||||
} from './notificationPreferences';
|
||||
export type { NotificationPrefs } from './notificationPreferences';
|
||||
|
||||
export { pushService, registerDevice, getDevices, unregisterDevice, updateDeviceToken, getPushRecords } from './pushService';
|
||||
export { pushService, registerDevice, getDevices, unregisterDevice, updateDeviceToken, getPushRecords } from './pushService';
|
||||
|
||||
export { jpushService } from './jpushService';
|
||||
318
src/services/notification/jpushService.ts
Normal file
318
src/services/notification/jpushService.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { Platform, NativeModules } from 'react-native';
|
||||
import { pushService } from './pushService';
|
||||
|
||||
type JPushNotificationMessage = {
|
||||
messageID: string;
|
||||
title: string;
|
||||
content: string;
|
||||
extras: Record<string, string>;
|
||||
notificationType?: string;
|
||||
notificationEventType?: 'notificationArrived' | 'notificationOpened';
|
||||
};
|
||||
|
||||
type JPushCallback = (message: JPushNotificationMessage) => void;
|
||||
|
||||
let JPush: typeof import('jpush-react-native').default | null = null;
|
||||
let jpushAppKey = '';
|
||||
let jpushChannel = 'developer-default';
|
||||
|
||||
try {
|
||||
const nativeModule = NativeModules.JPushModule;
|
||||
if (nativeModule) {
|
||||
JPush = require('jpush-react-native').default;
|
||||
const Constants = require('expo-constants').default;
|
||||
const extra = Constants?.expoConfig?.extra || Constants?.extra || {};
|
||||
jpushAppKey = extra.jpushAppKey || '';
|
||||
jpushChannel = extra.jpushChannel || 'withyou-default';
|
||||
} else {
|
||||
console.warn('[JPush] native module not linked, skipping');
|
||||
}
|
||||
} catch {
|
||||
console.warn('[JPush] jpush-react-native not available');
|
||||
}
|
||||
|
||||
function getAuthStore() {
|
||||
return require('@/stores').useAuthStore;
|
||||
}
|
||||
|
||||
class JPushService {
|
||||
private registrationID: string | null = null;
|
||||
private isInitialized = false;
|
||||
private initPromise: Promise<boolean> | null = null;
|
||||
private notificationCallback: JPushCallback | null = null;
|
||||
private connectResolved = false;
|
||||
private listenersRegistered = false;
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
if (this.isInitialized) return true;
|
||||
if (Platform.OS === 'web') return false;
|
||||
if (!JPush) return false;
|
||||
if (this.initPromise) return this.initPromise;
|
||||
|
||||
this.initPromise = this._doInit();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private _registerListeners(): void {
|
||||
if (this.listenersRegistered) return;
|
||||
this.listenersRegistered = true;
|
||||
|
||||
JPush!.addConnectEventListener((result: { connectEnable: boolean }) => {
|
||||
console.log('[JPush] connect event:', result.connectEnable);
|
||||
if (result.connectEnable && !this.connectResolved) {
|
||||
this.connectResolved = true;
|
||||
this._onConnected();
|
||||
}
|
||||
});
|
||||
|
||||
JPush!.addNotificationListener((result: any) => {
|
||||
console.log('[JPush] notification:', result);
|
||||
if (this.notificationCallback) {
|
||||
this.notificationCallback({
|
||||
messageID: result.messageID || '',
|
||||
title: result.title || '',
|
||||
content: result.content || '',
|
||||
extras: result.extras || {},
|
||||
notificationType: result.extras?.notification_type,
|
||||
notificationEventType: result.notificationEventType,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
JPush!.addCustomMessageListener((result: any) => {
|
||||
console.log('[JPush] custom message:', result);
|
||||
});
|
||||
|
||||
JPush!.addLocalNotificationListener((result: any) => {
|
||||
console.log('[JPush] local notification:', result);
|
||||
});
|
||||
|
||||
JPush!.addTagAliasListener((result: any) => {
|
||||
console.log('[JPush] tag/alias result:', result);
|
||||
});
|
||||
|
||||
JPush!.addCommandEventListener((result: any) => {
|
||||
console.log('[JPush] command result:', result);
|
||||
});
|
||||
}
|
||||
|
||||
private async _doInit(): Promise<boolean> {
|
||||
try {
|
||||
JPush!.setLoggerEnable(true);
|
||||
|
||||
// Register listeners only once (idempotent guard)
|
||||
this._registerListeners();
|
||||
|
||||
// 初始化 SDK
|
||||
JPush!.init({
|
||||
appKey: jpushAppKey,
|
||||
channel: jpushChannel,
|
||||
production: !__DEV__,
|
||||
});
|
||||
|
||||
console.log('[JPush] init called, waiting for registration...');
|
||||
|
||||
// 轮询获取 RegistrationID,间隔逐渐增大
|
||||
const regID = await this._pollRegistrationID();
|
||||
if (regID) {
|
||||
this.registrationID = regID;
|
||||
this.isInitialized = true;
|
||||
console.log('[JPush] initialized with registrationID:', regID);
|
||||
await this.registerDeviceToServer();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 即使没拿到 registrationID,也标记为已初始化
|
||||
// connect 事件回来后会自动获取
|
||||
this.isInitialized = true;
|
||||
console.warn('[JPush] init done but no registrationID yet, will retry on connect');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[JPush] initialization failed:', error);
|
||||
this.initPromise = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _onConnected(): void {
|
||||
if (this.registrationID) return; // Already have registrationID, skip
|
||||
console.log('[JPush] connected, fetching registrationID...');
|
||||
this._fetchRegistrationID();
|
||||
}
|
||||
|
||||
private _fetchRegistrationID(): void {
|
||||
if (!JPush) return;
|
||||
JPush.getRegistrationID((result: any) => {
|
||||
console.log('[JPush] getRegistrationID result:', JSON.stringify(result));
|
||||
const regID = result?.registerID || result?.registrationID;
|
||||
if (regID) {
|
||||
this.registrationID = regID;
|
||||
this.isInitialized = true;
|
||||
console.log('[JPush] got registrationID:', regID);
|
||||
this.registerDeviceToServer();
|
||||
} else {
|
||||
console.warn('[JPush] getRegistrationID returned empty');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _pollRegistrationID(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
// 轮询策略: 2s, 4s, 6s, 8s, 10s (总计 30s)
|
||||
const delays = [2000, 4000, 6000, 8000, 10000];
|
||||
let attempt = 0;
|
||||
|
||||
const tryFetch = () => {
|
||||
if (!JPush) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
JPush.getRegistrationID((result: any) => {
|
||||
const regID = result?.registerID || result?.registrationID;
|
||||
console.log(`[JPush] poll attempt ${attempt + 1}, regID:`, regID || '(empty)');
|
||||
|
||||
if (regID) {
|
||||
resolve(regID);
|
||||
return;
|
||||
}
|
||||
|
||||
attempt++;
|
||||
if (attempt < delays.length) {
|
||||
setTimeout(tryFetch, delays[attempt] - (attempt > 0 ? delays[attempt - 1] : 0));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(tryFetch, delays[0]);
|
||||
});
|
||||
}
|
||||
|
||||
async registerDeviceToServer(): Promise<void> {
|
||||
if (!this.registrationID) return;
|
||||
|
||||
const useAuthStore = getAuthStore();
|
||||
const { isAuthenticated } = useAuthStore.getState();
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
try {
|
||||
const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
|
||||
const deviceID = await this.getDeviceID();
|
||||
|
||||
await pushService.registerDevice({
|
||||
device_id: deviceID,
|
||||
device_type: deviceType as 'ios' | 'android',
|
||||
push_token: this.registrationID,
|
||||
device_name: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} Device`,
|
||||
});
|
||||
|
||||
console.log('[JPush] device registered to server');
|
||||
} catch (error) {
|
||||
console.error('[JPush] failed to register device to server:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getDeviceID(): Promise<string> {
|
||||
try {
|
||||
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
||||
const DEVICE_ID_KEY = 'withyou_device_id';
|
||||
let deviceId = await AsyncStorage.getItem(DEVICE_ID_KEY);
|
||||
if (deviceId) return deviceId;
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
deviceId = `${Platform.OS}_${timestamp}_${random}`;
|
||||
await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||
return deviceId;
|
||||
} catch {
|
||||
return `${Platform.OS}_${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
onNotification(callback: JPushCallback): void {
|
||||
this.notificationCallback = callback;
|
||||
}
|
||||
|
||||
setTags(tags: string[]): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.addTags({ sequence: Date.now(), tags });
|
||||
} catch (error) {
|
||||
console.error('[JPush] setTags failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setAlias(alias: string): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.setAlias({ sequence: Date.now(), alias });
|
||||
} catch (error) {
|
||||
console.error('[JPush] setAlias failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async setAliasForUser(userID: string): Promise<void> {
|
||||
this.setAlias(userID);
|
||||
}
|
||||
|
||||
deleteAlias(): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.deleteAlias({ sequence: Date.now() });
|
||||
} catch (error) {
|
||||
console.error('[JPush] deleteAlias failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getRegistrationIDValue(): string | null {
|
||||
return this.registrationID;
|
||||
}
|
||||
|
||||
isJPushAvailable(): boolean {
|
||||
return this.isInitialized && this.registrationID !== null;
|
||||
}
|
||||
|
||||
addLocalNotification(params: {
|
||||
messageID: string;
|
||||
title: string;
|
||||
content: string;
|
||||
extras: Record<string, string>;
|
||||
}): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.addLocalNotification(params);
|
||||
} catch (error) {
|
||||
console.error('[JPush] addLocalNotification failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllNotifications(): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.clearAllNotifications();
|
||||
} catch (error) {
|
||||
console.error('[JPush] clearAllNotifications failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setBadge(badge: number): void {
|
||||
if (!JPush || !this.isInitialized || Platform.OS !== 'ios') return;
|
||||
try {
|
||||
JPush.setBadge({ badge, appBadge: badge });
|
||||
} catch (error) {
|
||||
console.error('[JPush] setBadge failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this.isInitialized = false;
|
||||
this.registrationID = null;
|
||||
this.notificationCallback = null;
|
||||
this.initPromise = null;
|
||||
this.connectResolved = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const jpushService = new JPushService();
|
||||
export default jpushService;
|
||||
@@ -1,14 +1,7 @@
|
||||
/**
|
||||
* 通知相关本地偏好(推送开关、提示音、震动)
|
||||
* 启动时 loadNotificationPreferences 会同步到内存,供 SSE / 系统通知 / Handler 读取
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { setVibrationEnabled } from '../platform/messageVibrationService';
|
||||
|
||||
export const NOTIFICATION_PREF_KEYS = {
|
||||
/** 与历史版本保持一致 */
|
||||
vibration: 'vibration_enabled',
|
||||
push: 'notification_push_enabled',
|
||||
sound: 'notification_sound_enabled',
|
||||
@@ -41,7 +34,6 @@ function parseBool(raw: string | null, fallback: boolean): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用启动时调用:从 AsyncStorage 恢复并写入震动服务内存态 */
|
||||
export async function loadNotificationPreferences(): Promise<NotificationPrefs> {
|
||||
try {
|
||||
const [vibrationRaw, pushRaw, soundRaw] = await Promise.all([
|
||||
@@ -77,18 +69,7 @@ export async function setSoundPreference(enabled: boolean): Promise<void> {
|
||||
await AsyncStorage.setItem(NOTIFICATION_PREF_KEYS.sound, JSON.stringify(enabled));
|
||||
}
|
||||
|
||||
/** 注册 expo-notifications 展示策略(读取内存缓存,切换开关后立即生效) */
|
||||
export function registerNotificationPresentationHandler(): void {
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => {
|
||||
const p = getNotificationPreferencesSync();
|
||||
return {
|
||||
shouldShowAlert: p.pushEnabled,
|
||||
shouldPlaySound: p.soundEnabled,
|
||||
shouldSetBadge: true,
|
||||
shouldShowBanner: p.pushEnabled,
|
||||
shouldShowList: p.pushEnabled,
|
||||
};
|
||||
},
|
||||
});
|
||||
// JPush handles notification presentation natively.
|
||||
// Preferences are read from getNotificationPreferencesSync() when needed.
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* 系统通知服务
|
||||
* 使用 expo-notifications 处理原生系统通知
|
||||
* 支持本地通知,类似 QQ 在通知栏显示消息
|
||||
*/
|
||||
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { Platform, AppState, AppStateStatus } from 'react-native';
|
||||
import type { WSChatMessage, WSNotificationMessage, WSAnnouncementMessage } from '../core/wsService';
|
||||
import { extractTextFromSegments } from '@/types/dto';
|
||||
@@ -12,13 +5,8 @@ import { getNotificationPreferencesSync } from './notificationPreferences';
|
||||
import { vibrateOnMessage } from '../platform/messageVibrationService';
|
||||
import { useMessageStore } from '@/stores/message/store';
|
||||
import { normalizeConversationId } from '@/stores/message/store';
|
||||
import { jpushService } from './jpushService';
|
||||
|
||||
// 通知渠道配置
|
||||
const CHANNEL_ID = 'default';
|
||||
const CHANNEL_NAME = '默认通知';
|
||||
const CHANNEL_DESCRIPTION = '接收系统消息、互动通知等';
|
||||
|
||||
// 通知类型
|
||||
export type AppNotificationType =
|
||||
| 'like_post'
|
||||
| 'like_comment'
|
||||
@@ -33,7 +21,6 @@ export type AppNotificationType =
|
||||
| 'announce'
|
||||
| 'chat';
|
||||
|
||||
// 获取通知标题
|
||||
export function getNotificationTitle(type: AppNotificationType, operatorName?: string): string {
|
||||
const titles: Record<AppNotificationType, string> = {
|
||||
like_post: '有人赞了你的帖子',
|
||||
@@ -64,41 +51,13 @@ class SystemNotificationService {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
this.currentAppState = AppState.currentState;
|
||||
this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => {
|
||||
this.currentAppState = nextAppState;
|
||||
});
|
||||
|
||||
if (existingStatus !== 'granted') {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
|
||||
if (finalStatus !== 'granted') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
await Notifications.setNotificationChannelAsync(CHANNEL_ID, {
|
||||
name: CHANNEL_NAME,
|
||||
importance: Notifications.AndroidImportance.HIGH, // HIGH 优先级足以触发震动
|
||||
vibrationPattern: [0, 300, 200, 300], // 等待0ms,震动300ms,停止200ms,震动300ms
|
||||
lightColor: '#FF6B35',
|
||||
enableVibrate: true, // 启用震动
|
||||
enableLights: true, // 启用呼吸灯
|
||||
});
|
||||
}
|
||||
|
||||
this.currentAppState = AppState.currentState;
|
||||
this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => {
|
||||
this.currentAppState = nextAppState;
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[SystemNotification] 初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
this.isInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
async showNotification(options: {
|
||||
@@ -107,34 +66,47 @@ class SystemNotificationService {
|
||||
data?: Record<string, string | number | object>;
|
||||
type: AppNotificationType;
|
||||
}): Promise<string | null> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用 scheduleNotificationAsync 立即显示通知
|
||||
// 配合 setNotificationHandler 确保前台也能显示
|
||||
|
||||
const { vibrationEnabled } = getNotificationPreferencesSync();
|
||||
// 构建通知内容
|
||||
const content: Notifications.NotificationContentInput = {
|
||||
title: options.title,
|
||||
body: options.body,
|
||||
data: options.data as Record<string, string | number | object> | undefined,
|
||||
// 显式设置震动(仅 Android 生效,且尊重「消息震动」开关)
|
||||
...(Platform.OS === 'android' && vibrationEnabled
|
||||
? { vibrationPattern: [0, 300, 200, 300] }
|
||||
: {}),
|
||||
};
|
||||
if (!getNotificationPreferencesSync().pushEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||
content,
|
||||
trigger: null, // null 表示立即显示
|
||||
if (Platform.OS === 'web') {
|
||||
// Web: use browser Notification API
|
||||
if ('Notification' in window) {
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission === 'granted') {
|
||||
const notification = new Notification(options.title, {
|
||||
body: options.body,
|
||||
data: options.data,
|
||||
});
|
||||
return notification.tag || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mobile: use JPush local notification
|
||||
const extras: Record<string, string> = {};
|
||||
if (options.data) {
|
||||
for (const [key, value] of Object.entries(options.data)) {
|
||||
extras[key] = String(value);
|
||||
}
|
||||
}
|
||||
extras['notification_type'] = options.type;
|
||||
|
||||
const notificationId = `notif_${options.type}_${Date.now()}`;
|
||||
|
||||
jpushService.addLocalNotification({
|
||||
messageID: notificationId,
|
||||
title: options.title,
|
||||
content: options.body,
|
||||
extras,
|
||||
});
|
||||
|
||||
return notificationId;
|
||||
} catch (error) {
|
||||
console.error('[SystemNotification] 显示通知失败:', error);
|
||||
console.error('[SystemNotification] show notification failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -204,9 +176,8 @@ class SystemNotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在后台时显示通知,前台时不显示(用户正在使用应用,可以直接看到消息)
|
||||
// Only show local notification when app is in background
|
||||
if (this.currentAppState !== 'active') {
|
||||
// 判断是否是聊天消息(通过 segments 字段)
|
||||
if ('segments' in message) {
|
||||
const chatMsg = message as WSChatMessage;
|
||||
const conversationId = normalizeConversationId(chatMsg.conversation_id);
|
||||
@@ -214,12 +185,9 @@ class SystemNotificationService {
|
||||
if (isNotificationMuted) {
|
||||
return;
|
||||
}
|
||||
void chatMsg;
|
||||
await this.showChatNotification(chatMsg);
|
||||
} else {
|
||||
const notifMsg = message as WSNotificationMessage | WSAnnouncementMessage;
|
||||
void notifMsg;
|
||||
// 系统通知只在后台显示,因此震动也在该分支触发
|
||||
vibrateOnMessage('notification').catch(() => {});
|
||||
await this.showWSNotification(notifMsg);
|
||||
}
|
||||
@@ -227,15 +195,18 @@ class SystemNotificationService {
|
||||
}
|
||||
|
||||
async clearAllNotifications(): Promise<void> {
|
||||
await Notifications.dismissAllNotificationsAsync();
|
||||
jpushService.clearAllNotifications();
|
||||
if (Platform.OS === 'web' && 'Notification' in window) {
|
||||
// No programmatic way to clear browser notifications
|
||||
}
|
||||
}
|
||||
|
||||
async clearBadge(): Promise<void> {
|
||||
await Notifications.setBadgeCountAsync(0);
|
||||
jpushService.setBadge(0);
|
||||
}
|
||||
|
||||
async setBadgeCount(count: number): Promise<void> {
|
||||
await Notifications.setBadgeCountAsync(count);
|
||||
jpushService.setBadge(count);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
@@ -251,4 +222,4 @@ class SystemNotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
export const systemNotificationService = new SystemNotificationService();
|
||||
export const systemNotificationService = new SystemNotificationService();
|
||||
@@ -5,6 +5,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true
|
||||
|
||||
Reference in New Issue
Block a user