接入原生壳本地通知能力
新增 notification.showLocal HostBridge 契约和 H5 facade 移动端通过 expo-notifications 发送即时本地通知 桌面端通过 Tauri notification 插件发送系统通知 更新壳能力检查、测试、方案文档和共享决策记录
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
normalizeHostBridgeExportFileName,
|
||||
normalizeHostBridgeExternalUrl,
|
||||
normalizeHostBridgeLifecycleState,
|
||||
normalizeHostBridgeLocalNotification,
|
||||
} from './hostBridge';
|
||||
|
||||
describe('HostBridge shared contract helpers', () => {
|
||||
@@ -57,11 +58,52 @@ describe('HostBridge shared contract helpers', () => {
|
||||
expect(isHostBridgeCapability('file.importImage')).toBe(true);
|
||||
expect(isHostBridgeCapability('file.imageDropped')).toBe(true);
|
||||
expect(isHostBridgeCapability('app.setBadgeCount')).toBe(true);
|
||||
expect(isHostBridgeCapability('notification.showLocal')).toBe(true);
|
||||
expect(isHostBridgeCapability('navigation.canGoBack')).toBe(true);
|
||||
expect(isHostBridgeCapability('unknown.capability')).toBe(false);
|
||||
expect(isHostBridgeCapability(null)).toBe(false);
|
||||
});
|
||||
|
||||
test('归一化宿主本地通知内容', () => {
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({
|
||||
title: ' 生成完成 ',
|
||||
body: ' 作品已准备好 可以试玩 ',
|
||||
}),
|
||||
).toEqual({
|
||||
title: '生成完成',
|
||||
body: '作品已准备好 可以试玩',
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({
|
||||
title: '生成完成',
|
||||
body: '',
|
||||
}),
|
||||
).toEqual({
|
||||
title: '生成完成',
|
||||
});
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({
|
||||
title: 'a'.repeat(90),
|
||||
body: 'b'.repeat(250),
|
||||
}),
|
||||
).toEqual({
|
||||
title: 'a'.repeat(80),
|
||||
body: 'b'.repeat(240),
|
||||
});
|
||||
expect(normalizeHostBridgeLocalNotification({ title: '' })).toBeNull();
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({ title: '生成\n完成' }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
normalizeHostBridgeLocalNotification({
|
||||
title: '生成完成',
|
||||
body: '坏\u0001内容',
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(normalizeHostBridgeLocalNotification(null)).toBeNull();
|
||||
});
|
||||
|
||||
test('归一化宿主角标数量', () => {
|
||||
expect(normalizeHostBridgeBadgeCount(0)).toBe(0);
|
||||
expect(normalizeHostBridgeBadgeCount(12)).toBe(12);
|
||||
|
||||
@@ -30,6 +30,7 @@ export const HOST_BRIDGE_METHODS = [
|
||||
'file.exportImage',
|
||||
'file.importImage',
|
||||
'haptics.impact',
|
||||
'notification.showLocal',
|
||||
] as const;
|
||||
|
||||
export type HostBridgeMethod = (typeof HOST_BRIDGE_METHODS)[number];
|
||||
@@ -304,6 +305,64 @@ export type HapticsImpactPayload = {
|
||||
style?: 'light' | 'medium' | 'heavy';
|
||||
};
|
||||
|
||||
export type LocalNotificationPayload = {
|
||||
title: string;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
export const HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH = 80;
|
||||
export const HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH = 240;
|
||||
|
||||
function normalizeHostBridgePlainText(
|
||||
value: unknown,
|
||||
maxLength: number,
|
||||
required: boolean,
|
||||
) {
|
||||
if (typeof value !== 'string') {
|
||||
return required ? null : undefined;
|
||||
}
|
||||
|
||||
if (hasHostBridgeControlCharacter(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = value.trim().replace(/\s+/g, ' ');
|
||||
if (!text) {
|
||||
return required ? null : undefined;
|
||||
}
|
||||
|
||||
return text.slice(0, maxLength);
|
||||
}
|
||||
|
||||
export function normalizeHostBridgeLocalNotification(
|
||||
payload: unknown,
|
||||
): LocalNotificationPayload | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = payload as Partial<LocalNotificationPayload>;
|
||||
const title = normalizeHostBridgePlainText(
|
||||
candidate.title,
|
||||
HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH,
|
||||
true,
|
||||
);
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = normalizeHostBridgePlainText(
|
||||
candidate.body,
|
||||
HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH,
|
||||
false,
|
||||
);
|
||||
if (body === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return body ? { title, body } : { title };
|
||||
}
|
||||
|
||||
export type ShareSetTargetPayload = {
|
||||
target: unknown;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user