@@ -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