Skip to content

Commit bb74ff5

Browse files
committed
fix(notification/js): normalize replay payload variants and preserve callback compatibility
1 parent 5d86849 commit bb74ff5

2 files changed

Lines changed: 236 additions & 5 deletions

File tree

plugins/notification/api-iife.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/notification/guest-js/index.ts

Lines changed: 235 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,16 +573,247 @@ async function onNotificationReceived(
573573
return await addPluginListener('notification', 'notification', cb)
574574
}
575575

576-
async function onAction(
576+
function normalisePendingActions(
577+
pending: unknown
578+
): ActionPerformedNotification[] {
579+
const normalisedActions: ActionPerformedNotification[] = []
580+
const seenObjects = new WeakSet<object>()
581+
const seenActionKeys = new Set<string>()
582+
583+
const toRecord = (value: unknown): Record<string, unknown> | null => {
584+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
585+
return null
586+
}
587+
588+
const record = value as Record<string, unknown>
589+
const wrapped = record.nameValuePairs
590+
if (wrapped && typeof wrapped === 'object') {
591+
return toRecord(wrapped)
592+
}
593+
594+
return record
595+
}
596+
597+
const buildAction = (
598+
candidate: unknown
599+
): ActionPerformedNotification | null => {
600+
const record = toRecord(candidate)
601+
if (!record) {
602+
return null
603+
}
604+
605+
const actionId = record.actionId
606+
if (typeof actionId !== 'string' || actionId.length === 0) {
607+
return null
608+
}
609+
610+
const action: ActionPerformedNotification = {
611+
actionId
612+
}
613+
614+
const rawId = record.id
615+
if (typeof rawId === 'number') {
616+
action.id = rawId
617+
} else if (typeof rawId === 'string') {
618+
const parsedId = Number.parseInt(rawId, 10)
619+
if (!Number.isNaN(parsedId)) {
620+
action.id = parsedId
621+
}
622+
}
623+
624+
if (typeof record.inputValue === 'string') {
625+
action.inputValue = record.inputValue
626+
}
627+
628+
const toNumber = (value: unknown): number | null => {
629+
if (typeof value === 'number' && Number.isFinite(value)) {
630+
return value
631+
}
632+
if (typeof value === 'string') {
633+
const parsed = Number.parseInt(value, 10)
634+
if (!Number.isNaN(parsed)) {
635+
return parsed
636+
}
637+
}
638+
return null
639+
}
640+
641+
const toStringRecord = (value: unknown): Record<string, string> => {
642+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
643+
return {}
644+
}
645+
646+
const source = value as Record<string, unknown>
647+
const output: Record<string, string> = {}
648+
for (const [key, item] of Object.entries(source)) {
649+
if (typeof item === 'string') {
650+
output[key] = item
651+
}
652+
}
653+
return output
654+
}
655+
656+
const toUnknownRecord = (value: unknown): Record<string, unknown> => {
657+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
658+
return {}
659+
}
660+
return value as Record<string, unknown>
661+
}
662+
663+
const coerceActiveNotification = (
664+
value: unknown
665+
): ActiveNotification | null => {
666+
const notificationRecord = toRecord(value)
667+
if (!notificationRecord) {
668+
return null
669+
}
670+
671+
const id = toNumber(notificationRecord.id)
672+
if (id === null) {
673+
return null
674+
}
675+
676+
const activeNotification: ActiveNotification = {
677+
id,
678+
groupSummary:
679+
typeof notificationRecord.groupSummary === 'boolean'
680+
? notificationRecord.groupSummary
681+
: false,
682+
data: toStringRecord(notificationRecord.data),
683+
extra: toUnknownRecord(notificationRecord.extra),
684+
attachments: Array.isArray(notificationRecord.attachments)
685+
? (notificationRecord.attachments as Attachment[])
686+
: []
687+
}
688+
689+
if (typeof notificationRecord.tag === 'string') {
690+
activeNotification.tag = notificationRecord.tag
691+
}
692+
if (typeof notificationRecord.title === 'string') {
693+
activeNotification.title = notificationRecord.title
694+
}
695+
if (typeof notificationRecord.body === 'string') {
696+
activeNotification.body = notificationRecord.body
697+
}
698+
if (typeof notificationRecord.group === 'string') {
699+
activeNotification.group = notificationRecord.group
700+
}
701+
if (typeof notificationRecord.actionTypeId === 'string') {
702+
activeNotification.actionTypeId = notificationRecord.actionTypeId
703+
}
704+
if (typeof notificationRecord.sound === 'string') {
705+
activeNotification.sound = notificationRecord.sound
706+
}
707+
if (
708+
notificationRecord.schedule &&
709+
typeof notificationRecord.schedule === 'object'
710+
) {
711+
activeNotification.schedule = notificationRecord.schedule as Schedule
712+
}
713+
714+
return activeNotification
715+
}
716+
717+
if ('notification' in record) {
718+
action.notification = coerceActiveNotification(record.notification)
719+
}
720+
721+
return action
722+
}
723+
724+
const addAction = (action: ActionPerformedNotification): void => {
725+
const key = `${action.id ?? ''}|${action.actionId}|${action.inputValue ?? ''}`
726+
if (seenActionKeys.has(key)) {
727+
return
728+
}
729+
seenActionKeys.add(key)
730+
normalisedActions.push(action)
731+
}
732+
733+
const walk = (value: unknown): void => {
734+
if (!value || typeof value !== 'object') {
735+
return
736+
}
737+
738+
if (Array.isArray(value)) {
739+
for (const entry of value) {
740+
walk(entry)
741+
}
742+
return
743+
}
744+
745+
const objectValue = value as object
746+
if (seenObjects.has(objectValue)) {
747+
return
748+
}
749+
seenObjects.add(objectValue)
750+
751+
const record = value as Record<string, unknown>
752+
753+
const directAction = buildAction(record)
754+
if (directAction) {
755+
addAction(directAction)
756+
return
757+
}
758+
759+
const wrappedValue = record.value
760+
if (wrappedValue !== undefined) {
761+
walk(wrappedValue)
762+
}
763+
764+
// Some host bridges return array-like objects (`{ 0: ..., length: N }`).
765+
if (typeof record.length === 'number') {
766+
for (let index = 0; index < record.length; index += 1) {
767+
walk(record[String(index)])
768+
}
769+
}
770+
771+
for (const entry of Object.values(record)) {
772+
walk(entry)
773+
}
774+
}
775+
776+
walk(pending)
777+
778+
return normalisedActions
779+
}
780+
781+
/**
782+
* Registers a listener for notification action events.
783+
*
784+
* @since 2.0.0
785+
*/
786+
function onAction(
577787
cb: (notification: ActionPerformedNotification) => void
788+
): Promise<PluginListener>
789+
/**
790+
* Registers a listener for notification action events.
791+
*
792+
* @deprecated Use the `ActionPerformedNotification` callback type.
793+
* @since 2.0.0
794+
*/
795+
function onAction(cb: (notification: Options) => void): Promise<PluginListener>
796+
async function onAction(
797+
cb:
798+
| ((notification: ActionPerformedNotification) => void)
799+
| ((notification: Options) => void)
578800
): Promise<PluginListener> {
579-
const listener = await addPluginListener('notification', 'actionPerformed', cb)
801+
const actionCallback = cb as (notification: ActionPerformedNotification) => void
802+
const listener = await addPluginListener(
803+
'notification',
804+
'actionPerformed',
805+
(notification: ActionPerformedNotification) => actionCallback(notification)
806+
)
580807
try {
581-
const pending = await invoke<ActionPerformedNotification[]>(
808+
const pendingResult = await invoke<unknown>(
582809
'plugin:notification|register_action_listener_ready'
583810
)
811+
const pending = normalisePendingActions(pendingResult)
812+
console.debug(
813+
`[NotificationPlugin] register_action_listener_ready replay count=${pending.length}`
814+
)
584815
for (const notification of pending) {
585-
cb(notification)
816+
actionCallback(notification)
586817
}
587818
} catch {
588819
// Older plugin versions and non-Android targets may not implement this command.

0 commit comments

Comments
 (0)