@@ -25,6 +25,37 @@ export type NormalizeReplyOptions = {
2525 onSkip ?: ( reason : NormalizeReplySkipReason ) => void ;
2626} ;
2727
28+ function isNoReplyActionValue ( value : unknown ) : boolean {
29+ return typeof value === "string" && value . trim ( ) . toUpperCase ( ) === SILENT_REPLY_TOKEN ;
30+ }
31+
32+ function parseNoReplyActionJsonText ( text : string | undefined ) : boolean {
33+ const trimmed = text ?. trim ( ) ;
34+ if ( ! trimmed || ! trimmed . startsWith ( "{" ) || ! trimmed . endsWith ( "}" ) ) {
35+ return false ;
36+ }
37+ try {
38+ const parsed = JSON . parse ( trimmed ) as Record < string , unknown > ;
39+ return isNoReplyActionValue ( parsed . action ) ;
40+ } catch {
41+ return false ;
42+ }
43+ }
44+
45+ function containsNoReplyAction ( value : unknown ) : boolean {
46+ if ( ! value || typeof value !== "object" ) {
47+ return false ;
48+ }
49+ if ( Array . isArray ( value ) ) {
50+ return value . some ( ( entry ) => containsNoReplyAction ( entry ) ) ;
51+ }
52+ const record = value as Record < string , unknown > ;
53+ if ( isNoReplyActionValue ( record . action ) ) {
54+ return true ;
55+ }
56+ return Object . values ( record ) . some ( ( entry ) => containsNoReplyAction ( entry ) ) ;
57+ }
58+
2859export function normalizeReplyPayload (
2960 payload : ReplyPayload ,
3061 opts : NormalizeReplyOptions = { } ,
@@ -33,16 +64,29 @@ export function normalizeReplyPayload(
3364 const hasChannelData = Boolean (
3465 payload . channelData && Object . keys ( payload . channelData ) . length > 0 ,
3566 ) ;
67+ const hasNoReplyActionChannelData = containsNoReplyAction ( payload . channelData ) ;
68+ const textIsNoReplyActionJson = parseNoReplyActionJsonText ( payload . text ) ;
3669 const trimmed = payload . text ?. trim ( ) ?? "" ;
37- if ( ! trimmed && ! hasMedia && ! hasChannelData ) {
70+ const hasEffectiveChannelData = hasChannelData && ! hasNoReplyActionChannelData ;
71+
72+ if ( ( textIsNoReplyActionJson || hasNoReplyActionChannelData ) && ! hasMedia && ! trimmed ) {
73+ opts . onSkip ?.( "silent" ) ;
74+ return null ;
75+ }
76+ if ( textIsNoReplyActionJson && ! hasMedia && ! hasEffectiveChannelData ) {
77+ opts . onSkip ?.( "silent" ) ;
78+ return null ;
79+ }
80+
81+ if ( ! trimmed && ! hasMedia && ! hasEffectiveChannelData ) {
3882 opts . onSkip ?.( "empty" ) ;
3983 return null ;
4084 }
4185
4286 const silentToken = opts . silentToken ?? SILENT_REPLY_TOKEN ;
4387 let text = payload . text ?? undefined ;
4488 if ( text && isSilentReplyText ( text , silentToken ) ) {
45- if ( ! hasMedia && ! hasChannelData ) {
89+ if ( ! hasMedia && ! hasEffectiveChannelData ) {
4690 opts . onSkip ?.( "silent" ) ;
4791 return null ;
4892 }
@@ -53,7 +97,7 @@ export function normalizeReplyPayload(
5397 // silent just like the exact-match path above. (#30916, #30955)
5498 if ( text && text . includes ( silentToken ) && ! isSilentReplyText ( text , silentToken ) ) {
5599 text = stripSilentToken ( text , silentToken ) ;
56- if ( ! text && ! hasMedia && ! hasChannelData ) {
100+ if ( ! text && ! hasMedia && ! hasEffectiveChannelData ) {
57101 opts . onSkip ?.( "silent" ) ;
58102 return null ;
59103 }
@@ -69,7 +113,7 @@ export function normalizeReplyPayload(
69113 if ( stripped . didStrip ) {
70114 opts . onHeartbeatStrip ?.( ) ;
71115 }
72- if ( stripped . shouldSkip && ! hasMedia && ! hasChannelData ) {
116+ if ( stripped . shouldSkip && ! hasMedia && ! hasEffectiveChannelData ) {
73117 opts . onSkip ?.( "heartbeat" ) ;
74118 return null ;
75119 }
@@ -79,7 +123,7 @@ export function normalizeReplyPayload(
79123 if ( text ) {
80124 text = sanitizeUserFacingText ( text , { errorContext : Boolean ( payload . isError ) } ) ;
81125 }
82- if ( ! text ?. trim ( ) && ! hasMedia && ! hasChannelData ) {
126+ if ( ! text ?. trim ( ) && ! hasMedia && ! hasEffectiveChannelData ) {
83127 opts . onSkip ?.( "empty" ) ;
84128 return null ;
85129 }
0 commit comments