@use-voltra/android 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commonjs/index.js +31 -0
- package/build/commonjs/index.js.map +1 -1
- package/build/commonjs/jsx/primitives.js +36 -0
- package/build/commonjs/jsx/primitives.js.map +1 -1
- package/build/commonjs/modifiers/createAndroidModifier.js +20 -0
- package/build/commonjs/modifiers/createAndroidModifier.js.map +1 -0
- package/build/commonjs/modifiers/index.js +188 -0
- package/build/commonjs/modifiers/index.js.map +1 -0
- package/build/commonjs/ongoing-notification/components.js +7 -1
- package/build/commonjs/ongoing-notification/components.js.map +1 -1
- package/build/commonjs/ongoing-notification/renderer.js +354 -18
- package/build/commonjs/ongoing-notification/renderer.js.map +1 -1
- package/build/commonjs/ongoing-notification/types.js +56 -0
- package/build/commonjs/ongoing-notification/types.js.map +1 -1
- package/build/commonjs/server.js.map +1 -1
- package/build/module/index.js +3 -1
- package/build/module/index.js.map +1 -1
- package/build/module/jsx/primitives.js +1 -0
- package/build/module/jsx/primitives.js.map +1 -1
- package/build/module/modifiers/createAndroidModifier.js +15 -0
- package/build/module/modifiers/createAndroidModifier.js.map +1 -0
- package/build/module/modifiers/index.js +169 -0
- package/build/module/modifiers/index.js.map +1 -0
- package/build/module/ongoing-notification/components.js +6 -0
- package/build/module/ongoing-notification/components.js.map +1 -1
- package/build/module/ongoing-notification/renderer.js +354 -18
- package/build/module/ongoing-notification/renderer.js.map +1 -1
- package/build/module/ongoing-notification/types.js +55 -1
- package/build/module/ongoing-notification/types.js.map +1 -1
- package/build/module/server.js.map +1 -1
- package/build/typescript/commonjs/index.d.ts +4 -3
- package/build/typescript/commonjs/index.d.ts.map +1 -1
- package/build/typescript/commonjs/jsx/baseProps.d.ts +8 -0
- package/build/typescript/commonjs/jsx/baseProps.d.ts.map +1 -1
- package/build/typescript/commonjs/jsx/primitives.d.ts +1 -0
- package/build/typescript/commonjs/jsx/primitives.d.ts.map +1 -1
- package/build/typescript/commonjs/modifiers/createAndroidModifier.d.ts +13 -0
- package/build/typescript/commonjs/modifiers/createAndroidModifier.d.ts.map +1 -0
- package/build/typescript/commonjs/modifiers/index.d.ts +171 -0
- package/build/typescript/commonjs/modifiers/index.d.ts.map +1 -0
- package/build/typescript/commonjs/ongoing-notification/components.d.ts +8 -2
- package/build/typescript/commonjs/ongoing-notification/components.d.ts.map +1 -1
- package/build/typescript/commonjs/ongoing-notification/renderer.d.ts.map +1 -1
- package/build/typescript/commonjs/ongoing-notification/types.d.ts +281 -3
- package/build/typescript/commonjs/ongoing-notification/types.d.ts.map +1 -1
- package/build/typescript/commonjs/server.d.ts +1 -1
- package/build/typescript/commonjs/server.d.ts.map +1 -1
- package/build/typescript/commonjs/types.d.ts +1 -81
- package/build/typescript/commonjs/types.d.ts.map +1 -1
- package/build/typescript/module/index.d.ts +4 -3
- package/build/typescript/module/index.d.ts.map +1 -1
- package/build/typescript/module/jsx/baseProps.d.ts +8 -0
- package/build/typescript/module/jsx/baseProps.d.ts.map +1 -1
- package/build/typescript/module/jsx/primitives.d.ts +1 -0
- package/build/typescript/module/jsx/primitives.d.ts.map +1 -1
- package/build/typescript/module/modifiers/createAndroidModifier.d.ts +13 -0
- package/build/typescript/module/modifiers/createAndroidModifier.d.ts.map +1 -0
- package/build/typescript/module/modifiers/index.d.ts +171 -0
- package/build/typescript/module/modifiers/index.d.ts.map +1 -0
- package/build/typescript/module/ongoing-notification/components.d.ts +8 -2
- package/build/typescript/module/ongoing-notification/components.d.ts.map +1 -1
- package/build/typescript/module/ongoing-notification/renderer.d.ts.map +1 -1
- package/build/typescript/module/ongoing-notification/types.d.ts +281 -3
- package/build/typescript/module/ongoing-notification/types.d.ts.map +1 -1
- package/build/typescript/module/server.d.ts +1 -1
- package/build/typescript/module/server.d.ts.map +1 -1
- package/build/typescript/module/types.d.ts +1 -81
- package/build/typescript/module/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +36 -2
- package/src/jsx/baseProps.tsx +8 -0
- package/src/jsx/primitives.ts +1 -0
- package/src/modifiers/createAndroidModifier.ts +21 -0
- package/src/modifiers/index.ts +166 -0
- package/src/ongoing-notification/components.tsx +22 -1
- package/src/ongoing-notification/renderer.ts +451 -18
- package/src/ongoing-notification/types.ts +298 -4
- package/src/server.ts +15 -0
- package/src/types.ts +17 -97
- package/build/commonjs/preload-utils.js +0 -55
- package/build/commonjs/preload-utils.js.map +0 -1
- package/build/commonjs/utils/index.js +0 -2
- package/build/commonjs/utils/index.js.map +0 -1
- package/build/module/preload-utils.js +0 -51
- package/build/module/preload-utils.js.map +0 -1
- package/build/module/utils/index.js +0 -2
- package/build/module/utils/index.js.map +0 -1
- package/build/typescript/commonjs/preload-utils.d.ts +0 -8
- package/build/typescript/commonjs/preload-utils.d.ts.map +0 -1
- package/build/typescript/commonjs/utils/index.d.ts +0 -1
- package/build/typescript/commonjs/utils/index.d.ts.map +0 -1
- package/build/typescript/module/preload-utils.d.ts +0 -8
- package/build/typescript/module/preload-utils.d.ts.map +0 -1
- package/build/typescript/module/utils/index.d.ts +0 -1
- package/build/typescript/module/utils/index.d.ts.map +0 -1
- package/src/preload-utils.ts +0 -78
- package/src/utils/index.ts +0 -0
|
@@ -6,20 +6,48 @@ import { ANDROID_ONGOING_NOTIFICATION_COMPONENT_TAG } from './components.js'
|
|
|
6
6
|
import type {
|
|
7
7
|
AndroidOngoingNotificationActionPayload,
|
|
8
8
|
AndroidOngoingNotificationActionProps,
|
|
9
|
+
AndroidOngoingNotificationBigPicturePayload,
|
|
10
|
+
AndroidOngoingNotificationBigPictureProps,
|
|
9
11
|
AndroidOngoingNotificationBigTextPayload,
|
|
10
12
|
AndroidOngoingNotificationBigTextProps,
|
|
13
|
+
AndroidOngoingNotificationCommonDisplayProps,
|
|
11
14
|
AndroidOngoingNotificationContent,
|
|
15
|
+
AndroidOngoingNotificationInboxPayload,
|
|
16
|
+
AndroidOngoingNotificationInboxProps,
|
|
17
|
+
AndroidOngoingNotificationMetricPayload,
|
|
18
|
+
AndroidOngoingNotificationMetricProps,
|
|
19
|
+
AndroidOngoingNotificationMetricSemanticStyle,
|
|
20
|
+
AndroidOngoingNotificationMetricValuePayload,
|
|
12
21
|
AndroidOngoingNotificationPayload,
|
|
13
22
|
AndroidOngoingNotificationProgressPayload,
|
|
14
23
|
AndroidOngoingNotificationProgressPoint,
|
|
15
24
|
AndroidOngoingNotificationProgressProps,
|
|
16
25
|
AndroidOngoingNotificationProgressSegment,
|
|
26
|
+
AndroidOngoingNotificationPublicVersion,
|
|
17
27
|
} from './types.js'
|
|
18
28
|
|
|
19
29
|
void getAndroidComponentId
|
|
20
30
|
|
|
21
31
|
const PAYLOAD_VERSION = 1 as const
|
|
22
32
|
|
|
33
|
+
/**
|
|
34
|
+
* InboxStyle only renders six lines, so more than that is rejected at render time instead of being
|
|
35
|
+
* silently truncated. Kept in sync with MAX_INBOX_LINES in the Android client.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_INBOX_LINES = 6
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Size of an inline picture that is still reasonable to hand to a native post. Push providers cap
|
|
41
|
+
* data payloads at a few kilobytes (FCM allows 4096 bytes), so no inline picture travels through a
|
|
42
|
+
* push message. Warns instead of failing: local starts and self-hosted transports can legitimately
|
|
43
|
+
* carry more. Compared against the decoded size, because a base64 string is four characters per
|
|
44
|
+
* three bytes and its length flatters the image.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_INLINE_PICTURE_BYTES = 256 * 1024
|
|
47
|
+
|
|
48
|
+
/** Bytes a base64 string holds once decoded. Padding leaves this at most two bytes high. */
|
|
49
|
+
const decodedBase64Bytes = (base64: string): number => Math.ceil((base64.length * 3) / 4)
|
|
50
|
+
|
|
23
51
|
const flattenChildren = (node: ReactNode): ReactNode[] => {
|
|
24
52
|
if (node === null || node === undefined || typeof node === 'boolean') {
|
|
25
53
|
return []
|
|
@@ -123,6 +151,16 @@ const assertOptionalImageSource = (value: unknown, propName: string): ImageSourc
|
|
|
123
151
|
return value
|
|
124
152
|
}
|
|
125
153
|
|
|
154
|
+
const assertRequiredImageSource = (value: unknown, propName: string): ImageSource => {
|
|
155
|
+
const source = assertOptionalImageSource(value, propName)
|
|
156
|
+
|
|
157
|
+
if (source === undefined) {
|
|
158
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" is required.`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return source
|
|
162
|
+
}
|
|
163
|
+
|
|
126
164
|
const assertBoolean = (value: unknown, propName: string): boolean | undefined => {
|
|
127
165
|
if (value === undefined) {
|
|
128
166
|
return undefined
|
|
@@ -211,15 +249,45 @@ const normalizeProgressPoints = (value: unknown): AndroidOngoingNotificationProg
|
|
|
211
249
|
})
|
|
212
250
|
}
|
|
213
251
|
|
|
214
|
-
const
|
|
215
|
-
if (value
|
|
216
|
-
|
|
252
|
+
const normalizeInboxLines = (value: unknown): string[] => {
|
|
253
|
+
if (!Array.isArray(value)) {
|
|
254
|
+
throw new Error('[Voltra] [Android] Ongoing notification prop "lines" must be an array of non-empty strings.')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (value.length === 0) {
|
|
258
|
+
throw new Error('[Voltra] [Android] Ongoing notification prop "lines" must contain at least one line.')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (value.length > MAX_INBOX_LINES) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`[Voltra] [Android] Ongoing notification prop "lines" must contain at most ${MAX_INBOX_LINES} lines, got ${value.length}.`
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return value.map((line, index) => assertString(line, `lines[${index}]`))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const warnInlinePicture = (picture: ImageSource): void => {
|
|
271
|
+
const base64 = 'base64' in picture ? picture.base64 : undefined
|
|
272
|
+
|
|
273
|
+
if (base64 === undefined) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const pictureBytes = decodedBase64Bytes(base64)
|
|
278
|
+
|
|
279
|
+
if (pictureBytes > MAX_INLINE_PICTURE_BYTES) {
|
|
280
|
+
console.warn(
|
|
281
|
+
`[Voltra] [Android] Ongoing notification prop "picture" carries a ${pictureBytes}-byte image, which exceeds ${MAX_INLINE_PICTURE_BYTES} bytes. Inline pictures cannot travel through push messages: preload the image and reference it with assetName instead.`
|
|
282
|
+
)
|
|
217
283
|
}
|
|
284
|
+
}
|
|
218
285
|
|
|
286
|
+
const normalizeEpoch = (value: unknown, propName: string): number => {
|
|
219
287
|
if (value instanceof Date) {
|
|
220
288
|
const timestamp = value.getTime()
|
|
221
289
|
if (!Number.isFinite(timestamp)) {
|
|
222
|
-
throw new Error(
|
|
290
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be a valid Date or timestamp.`)
|
|
223
291
|
}
|
|
224
292
|
|
|
225
293
|
return timestamp
|
|
@@ -229,7 +297,66 @@ const normalizeWhen = (value: unknown): number | undefined => {
|
|
|
229
297
|
return value
|
|
230
298
|
}
|
|
231
299
|
|
|
232
|
-
throw new Error(
|
|
300
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be a valid Date or timestamp.`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const normalizeWhen = (value: unknown): number | undefined => {
|
|
304
|
+
if (value === undefined) {
|
|
305
|
+
return undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return normalizeEpoch(value, 'when')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// `true` keeps meaning "count up" for payloads and props written before the
|
|
312
|
+
// chip gained a direction; only 'countDown' sets chronometerCountDown, and a
|
|
313
|
+
// countdown without `when` has nothing to count down to.
|
|
314
|
+
const normalizeChronometer = (
|
|
315
|
+
value: unknown,
|
|
316
|
+
when: number | undefined
|
|
317
|
+
): { chronometer?: boolean; chronometerCountDown?: boolean } => {
|
|
318
|
+
if (value === undefined) {
|
|
319
|
+
return {}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (typeof value === 'boolean') {
|
|
323
|
+
return { chronometer: value }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (value === 'countUp') {
|
|
327
|
+
return { chronometer: true }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (value === 'countDown') {
|
|
331
|
+
if (when === undefined) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
'[Voltra] [Android] Ongoing notification prop "chronometer" set to "countDown" requires the "when" prop.'
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { chronometer: true, chronometerCountDown: true }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
throw new Error(
|
|
341
|
+
'[Voltra] [Android] Ongoing notification prop "chronometer" must be a boolean, "countUp" or "countDown".'
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const normalizePublicVersion = (value: unknown): AndroidOngoingNotificationPublicVersion | undefined => {
|
|
346
|
+
if (value === undefined) {
|
|
347
|
+
return undefined
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
351
|
+
throw new Error('[Voltra] [Android] Ongoing notification prop "publicVersion" must be an object with a title.')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { title, text } = value as { title?: unknown; text?: unknown }
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
title: assertString(title, 'publicVersion.title'),
|
|
358
|
+
text: assertOptionalString(text, 'publicVersion.text'),
|
|
359
|
+
}
|
|
233
360
|
}
|
|
234
361
|
|
|
235
362
|
const getElementKind = (element: ReactElement<Record<string, unknown>>) => {
|
|
@@ -250,6 +377,33 @@ const normalizeActionPayload = (
|
|
|
250
377
|
}
|
|
251
378
|
}
|
|
252
379
|
|
|
380
|
+
const normalizeCommonDisplayFields = (
|
|
381
|
+
props: AndroidOngoingNotificationCommonDisplayProps & { largeIcon?: ImageSource }
|
|
382
|
+
): {
|
|
383
|
+
title?: string
|
|
384
|
+
subText?: string
|
|
385
|
+
shortCriticalText?: string
|
|
386
|
+
when?: number
|
|
387
|
+
chronometer?: boolean
|
|
388
|
+
chronometerCountDown?: boolean
|
|
389
|
+
showWhen?: boolean
|
|
390
|
+
largeIcon?: ImageSource
|
|
391
|
+
publicVersion?: AndroidOngoingNotificationPublicVersion
|
|
392
|
+
} => {
|
|
393
|
+
const when = normalizeWhen(props.when)
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
title: assertOptionalNonEmptyString(props.title, 'title'),
|
|
397
|
+
subText: assertOptionalString(props.subText, 'subText'),
|
|
398
|
+
shortCriticalText: assertOptionalString(props.shortCriticalText, 'shortCriticalText'),
|
|
399
|
+
when,
|
|
400
|
+
...normalizeChronometer(props.chronometer, when),
|
|
401
|
+
showWhen: assertBoolean(props.showWhen, 'showWhen'),
|
|
402
|
+
largeIcon: assertOptionalImageSource(props.largeIcon, 'largeIcon'),
|
|
403
|
+
publicVersion: normalizePublicVersion(props.publicVersion),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
253
407
|
const normalizeActions = (value: ReactNode): AndroidOngoingNotificationActionPayload[] | undefined => {
|
|
254
408
|
const actions: AndroidOngoingNotificationActionPayload[] = []
|
|
255
409
|
|
|
@@ -293,16 +447,11 @@ const normalizeProgressPayload = (
|
|
|
293
447
|
return {
|
|
294
448
|
v: PAYLOAD_VERSION,
|
|
295
449
|
kind: 'progress',
|
|
296
|
-
|
|
297
|
-
subText: assertOptionalString(props.subText, 'subText'),
|
|
450
|
+
...normalizeCommonDisplayFields(props),
|
|
298
451
|
text: assertOptionalString(props.text, 'text'),
|
|
299
452
|
value,
|
|
300
453
|
max,
|
|
301
454
|
indeterminate: assertBoolean(props.indeterminate, 'indeterminate'),
|
|
302
|
-
shortCriticalText: assertOptionalString(props.shortCriticalText, 'shortCriticalText'),
|
|
303
|
-
when: normalizeWhen(props.when),
|
|
304
|
-
chronometer: assertBoolean(props.chronometer, 'chronometer'),
|
|
305
|
-
largeIcon: assertOptionalImageSource(props.largeIcon, 'largeIcon'),
|
|
306
455
|
progressTrackerIcon: assertOptionalImageSource(props.progressTrackerIcon, 'progressTrackerIcon'),
|
|
307
456
|
progressStartIcon: assertOptionalImageSource(props.progressStartIcon, 'progressStartIcon'),
|
|
308
457
|
progressEndIcon: assertOptionalImageSource(props.progressEndIcon, 'progressEndIcon'),
|
|
@@ -320,14 +469,286 @@ const normalizeBigTextPayload = (
|
|
|
320
469
|
return {
|
|
321
470
|
v: PAYLOAD_VERSION,
|
|
322
471
|
kind: 'bigText',
|
|
323
|
-
|
|
324
|
-
subText: assertOptionalString(props.subText, 'subText'),
|
|
472
|
+
...normalizeCommonDisplayFields(props),
|
|
325
473
|
text,
|
|
326
474
|
bigText: assertOptionalString(props.bigText, 'bigText') ?? text,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
475
|
+
actions: normalizeActions(props.children),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const METRIC_TIME_PATTERN = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/
|
|
480
|
+
|
|
481
|
+
const METRIC_SEMANTIC_STYLES = ['unspecified', 'info', 'safe', 'caution', 'danger'] as const
|
|
482
|
+
|
|
483
|
+
const METRIC_TIME_FORMATS = ['adaptive', 'chronometer'] as const
|
|
484
|
+
|
|
485
|
+
const assertMetricNonEmptyString = (value: unknown, propName: string): string => {
|
|
486
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
487
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be a non-empty string.`)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return value
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const normalizeMetricTimeFormat = (value: unknown, propName: string) => {
|
|
494
|
+
if (value === undefined) {
|
|
495
|
+
return undefined
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!(METRIC_TIME_FORMATS as readonly unknown[]).includes(value)) {
|
|
499
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be "adaptive" or "chronometer".`)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return value as (typeof METRIC_TIME_FORMATS)[number]
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const normalizeMetricValue = (
|
|
506
|
+
value: unknown,
|
|
507
|
+
unit: unknown,
|
|
508
|
+
propName: string
|
|
509
|
+
): AndroidOngoingNotificationMetricValuePayload => {
|
|
510
|
+
const topLevelUnit = assertOptionalString(unit, `${propName}.unit`)
|
|
511
|
+
const rejectTopLevelUnit = () => {
|
|
512
|
+
if (topLevelUnit !== undefined) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.unit" only applies to "int", "float" and "text" values.`
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Shorthands: an integer reads as int, any other number as float, a string as text.
|
|
520
|
+
if (typeof value === 'number') {
|
|
521
|
+
if (!Number.isFinite(value)) {
|
|
522
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be a finite number.`)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return Number.isInteger(value)
|
|
526
|
+
? { type: 'int', value, unit: topLevelUnit }
|
|
527
|
+
: { type: 'float', value, unit: topLevelUnit }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (typeof value === 'string') {
|
|
531
|
+
return { type: 'text', value: assertMetricNonEmptyString(value, `${propName}.value`), unit: topLevelUnit }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be a number, a string, or a metric value object.`
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const source = value as Record<string, unknown>
|
|
541
|
+
const type = source.type
|
|
542
|
+
|
|
543
|
+
switch (type) {
|
|
544
|
+
case 'int': {
|
|
545
|
+
const numberValue = assertFiniteNumber(source.value, `${propName}.value`)
|
|
546
|
+
if (!Number.isInteger(numberValue)) {
|
|
547
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be an integer.`)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
type: 'int',
|
|
552
|
+
value: numberValue,
|
|
553
|
+
unit: assertOptionalString(source.unit, `${propName}.unit`) ?? topLevelUnit,
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
case 'float': {
|
|
557
|
+
const numberValue = assertFiniteNumber(source.value, `${propName}.value`)
|
|
558
|
+
const min = source.min === undefined ? undefined : assertFiniteNumber(source.min, `${propName}.min`)
|
|
559
|
+
const max = source.max === undefined ? undefined : assertFiniteNumber(source.max, `${propName}.max`)
|
|
560
|
+
|
|
561
|
+
if (min !== undefined && min < 0) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.min" must be greater than or equal to 0.`
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (max !== undefined && max < 0) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.max" must be greater than or equal to 0.`
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.min" must not exceed "${propName}.max".`
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let fractionDigits: number | undefined
|
|
580
|
+
if (source.fractionDigits !== undefined) {
|
|
581
|
+
fractionDigits = assertFiniteNumber(source.fractionDigits, `${propName}.fractionDigits`)
|
|
582
|
+
if (!Number.isInteger(fractionDigits) || fractionDigits < 0) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.fractionDigits" must be an integer greater than or equal to 0.`
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
type: 'float',
|
|
591
|
+
value: numberValue,
|
|
592
|
+
unit: assertOptionalString(source.unit, `${propName}.unit`) ?? topLevelUnit,
|
|
593
|
+
min,
|
|
594
|
+
max,
|
|
595
|
+
fractionDigits,
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
case 'text':
|
|
599
|
+
return {
|
|
600
|
+
type: 'text',
|
|
601
|
+
value: assertMetricNonEmptyString(source.value, `${propName}.value`),
|
|
602
|
+
unit: assertOptionalString(source.unit, `${propName}.unit`) ?? topLevelUnit,
|
|
603
|
+
}
|
|
604
|
+
case 'time': {
|
|
605
|
+
rejectTopLevelUnit()
|
|
606
|
+
const timeValue = assertMetricNonEmptyString(source.value, `${propName}.value`)
|
|
607
|
+
if (!METRIC_TIME_PATTERN.test(timeValue)) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be a time in "HH:mm" or "HH:mm:ss" form.`
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return { type: 'time', value: timeValue }
|
|
614
|
+
}
|
|
615
|
+
case 'timer':
|
|
616
|
+
rejectTopLevelUnit()
|
|
617
|
+
return {
|
|
618
|
+
type: 'timer',
|
|
619
|
+
endsAt: normalizeEpoch(source.endsAt, `${propName}.endsAt`),
|
|
620
|
+
format: normalizeMetricTimeFormat(source.format, `${propName}.format`),
|
|
621
|
+
}
|
|
622
|
+
case 'stopwatch':
|
|
623
|
+
rejectTopLevelUnit()
|
|
624
|
+
return {
|
|
625
|
+
type: 'stopwatch',
|
|
626
|
+
startedAt: normalizeEpoch(source.startedAt, `${propName}.startedAt`),
|
|
627
|
+
format: normalizeMetricTimeFormat(source.format, `${propName}.format`),
|
|
628
|
+
}
|
|
629
|
+
case 'pausedTimer': {
|
|
630
|
+
rejectTopLevelUnit()
|
|
631
|
+
const remainingMillis = assertFiniteNumber(source.remainingMillis, `${propName}.remainingMillis`)
|
|
632
|
+
if (remainingMillis < 0) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.remainingMillis" must be greater than or equal to 0.`
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return { type: 'pausedTimer', remainingMillis }
|
|
639
|
+
}
|
|
640
|
+
case 'pausedStopwatch': {
|
|
641
|
+
rejectTopLevelUnit()
|
|
642
|
+
const elapsedMillis = assertFiniteNumber(source.elapsedMillis, `${propName}.elapsedMillis`)
|
|
643
|
+
if (elapsedMillis < 0) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.elapsedMillis" must be greater than or equal to 0.`
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return { type: 'pausedStopwatch', elapsedMillis }
|
|
650
|
+
}
|
|
651
|
+
default:
|
|
652
|
+
throw new Error(
|
|
653
|
+
`[Voltra] [Android] Ongoing notification prop "${propName}.type" must be "int", "float", "text", "time", "timer", "stopwatch", "pausedTimer", or "pausedStopwatch".`
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const normalizeMetricPayload = (
|
|
659
|
+
props: AndroidOngoingNotificationMetricProps
|
|
660
|
+
): AndroidOngoingNotificationMetricPayload => {
|
|
661
|
+
if (!Array.isArray(props.metrics)) {
|
|
662
|
+
throw new Error('[Voltra] [Android] Ongoing notification prop "metrics" must be an array.')
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (props.metrics.length < 1 || props.metrics.length > 3) {
|
|
666
|
+
throw new Error('[Voltra] [Android] Ongoing notification prop "metrics" must contain between 1 and 3 metrics.')
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const metrics = props.metrics.map((descriptor, index) => {
|
|
670
|
+
if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
|
|
671
|
+
throw new Error(`[Voltra] [Android] Ongoing notification prop "metrics[${index}]" must be an object.`)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const label = assertMetricNonEmptyString(descriptor.label, `metrics[${index}].label`)
|
|
675
|
+
if (label.length > 10) {
|
|
676
|
+
throw new Error(
|
|
677
|
+
`[Voltra] [Android] Ongoing notification prop "metrics[${index}].label" must be at most 10 characters long.`
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return { label, value: normalizeMetricValue(descriptor.value, descriptor.unit, `metrics[${index}]`) }
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
let criticalMetric: number | undefined
|
|
685
|
+
if (props.criticalMetric !== undefined) {
|
|
686
|
+
criticalMetric = assertFiniteNumber(props.criticalMetric, 'criticalMetric')
|
|
687
|
+
if (!Number.isInteger(criticalMetric) || criticalMetric < 0 || criticalMetric >= metrics.length) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`[Voltra] [Android] Ongoing notification prop "criticalMetric" must be an index between 0 and ${
|
|
690
|
+
metrics.length - 1
|
|
691
|
+
}.`
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let semanticStyle: AndroidOngoingNotificationMetricSemanticStyle | undefined
|
|
697
|
+
if (props.semanticStyle !== undefined) {
|
|
698
|
+
if (!(METRIC_SEMANTIC_STYLES as readonly string[]).includes(props.semanticStyle)) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
'[Voltra] [Android] Ongoing notification prop "semanticStyle" must be "unspecified", "info", "safe", "caution", or "danger".'
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
semanticStyle = props.semanticStyle
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
v: PAYLOAD_VERSION,
|
|
709
|
+
kind: 'metric',
|
|
710
|
+
...normalizeCommonDisplayFields(props),
|
|
711
|
+
metrics,
|
|
712
|
+
criticalMetric,
|
|
713
|
+
semanticStyle,
|
|
714
|
+
actions: normalizeActions(props.children),
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const normalizeBigPicturePayload = (
|
|
719
|
+
props: AndroidOngoingNotificationBigPictureProps
|
|
720
|
+
): AndroidOngoingNotificationBigPicturePayload => {
|
|
721
|
+
const picture = assertRequiredImageSource(props.picture, 'picture')
|
|
722
|
+
warnInlinePicture(picture)
|
|
723
|
+
|
|
724
|
+
return {
|
|
725
|
+
v: PAYLOAD_VERSION,
|
|
726
|
+
kind: 'bigPicture',
|
|
727
|
+
...normalizeCommonDisplayFields(props),
|
|
728
|
+
text: assertOptionalString(props.text, 'text'),
|
|
729
|
+
picture,
|
|
730
|
+
summaryText: assertOptionalNonEmptyString(props.summaryText, 'summaryText'),
|
|
731
|
+
pictureContentDescription: assertOptionalNonEmptyString(
|
|
732
|
+
props.pictureContentDescription,
|
|
733
|
+
'pictureContentDescription'
|
|
734
|
+
),
|
|
735
|
+
showPictureWhenCollapsed: assertBoolean(props.showPictureWhenCollapsed, 'showPictureWhenCollapsed'),
|
|
736
|
+
bigLargeIcon: assertOptionalImageSource(props.bigLargeIcon, 'bigLargeIcon'),
|
|
737
|
+
hideLargeIconWhenExpanded: assertBoolean(props.hideLargeIconWhenExpanded, 'hideLargeIconWhenExpanded'),
|
|
738
|
+
actions: normalizeActions(props.children),
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const normalizeInboxPayload = (props: AndroidOngoingNotificationInboxProps): AndroidOngoingNotificationInboxPayload => {
|
|
743
|
+
const lines = normalizeInboxLines(props.lines)
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
v: PAYLOAD_VERSION,
|
|
747
|
+
kind: 'inbox',
|
|
748
|
+
...normalizeCommonDisplayFields(props),
|
|
749
|
+
text: props.text === undefined ? lines[0] : assertString(props.text, 'text'),
|
|
750
|
+
lines,
|
|
751
|
+
summaryText: assertOptionalNonEmptyString(props.summaryText, 'summaryText'),
|
|
331
752
|
actions: normalizeActions(props.children),
|
|
332
753
|
}
|
|
333
754
|
}
|
|
@@ -346,8 +767,20 @@ export const renderAndroidOngoingNotificationPayloadToJson = (
|
|
|
346
767
|
return normalizeBigTextPayload(element.props as AndroidOngoingNotificationBigTextProps)
|
|
347
768
|
}
|
|
348
769
|
|
|
770
|
+
if (kind === 'bigPicture') {
|
|
771
|
+
return normalizeBigPicturePayload(element.props as AndroidOngoingNotificationBigPictureProps)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (kind === 'inbox') {
|
|
775
|
+
return normalizeInboxPayload(element.props as AndroidOngoingNotificationInboxProps)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (kind === 'metric') {
|
|
779
|
+
return normalizeMetricPayload(element.props as AndroidOngoingNotificationMetricProps)
|
|
780
|
+
}
|
|
781
|
+
|
|
349
782
|
throw new Error(
|
|
350
|
-
'[Voltra] [Android] Ongoing notification content must use AndroidOngoingNotification.Progress or AndroidOngoingNotification.
|
|
783
|
+
'[Voltra] [Android] Ongoing notification content must use AndroidOngoingNotification.Progress, AndroidOngoingNotification.BigText, AndroidOngoingNotification.BigPicture, AndroidOngoingNotification.Inbox, or AndroidOngoingNotification.Metric.'
|
|
351
784
|
)
|
|
352
785
|
}
|
|
353
786
|
|