From 6eee8939055d1c40539aabbe0163cc332ae16816 Mon Sep 17 00:00:00 2001 From: mathiiiiiis Date: Mon, 16 Feb 2026 18:01:39 +0100 Subject: [PATCH 1/4] Add native Android module for inline custom emoji images in notifications Replaces notifee with a custom NotificationCompat.Builder that renders emoji images inline using RemoteViews, and adds server/DM context via subText --- .../nerimityreactnative/MainApplication.kt | 2 + .../custom/EmojiNotificationModule.kt | 346 +++++++++++ .../custom/EmojiNotificationPackage.kt | 16 + .../layout/notification_emoji_collapsed.xml | 22 + .../res/layout/notification_emoji_content.xml | 22 + .../res/layout/notification_emoji_segment.xml | 8 + .../notification_emoji_segment_large.xml | 6 + .../res/layout/notification_message_row.xml | 7 + .../res/layout/notification_text_segment.xml | 6 + .../notification_text_segment_single.xml | 9 + package-lock.json | 553 +++++++++--------- src/pushNotifications.ts | 198 ++++--- 12 files changed, 843 insertions(+), 352 deletions(-) create mode 100644 android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt create mode 100644 android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationPackage.kt create mode 100644 android/app/src/main/res/layout/notification_emoji_collapsed.xml create mode 100644 android/app/src/main/res/layout/notification_emoji_content.xml create mode 100644 android/app/src/main/res/layout/notification_emoji_segment.xml create mode 100644 android/app/src/main/res/layout/notification_emoji_segment_large.xml create mode 100644 android/app/src/main/res/layout/notification_message_row.xml create mode 100644 android/app/src/main/res/layout/notification_text_segment.xml create mode 100644 android/app/src/main/res/layout/notification_text_segment_single.xml diff --git a/android/app/src/main/java/com/nerimityreactnative/MainApplication.kt b/android/app/src/main/java/com/nerimityreactnative/MainApplication.kt index fb0b848..5dba811 100644 --- a/android/app/src/main/java/com/nerimityreactnative/MainApplication.kt +++ b/android/app/src/main/java/com/nerimityreactnative/MainApplication.kt @@ -10,6 +10,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.soloader.SoLoader +import com.nerimityreactnative.custom.EmojiNotificationPackage class MainApplication : Application(), ReactApplication { @@ -19,6 +20,7 @@ class MainApplication : Application(), ReactApplication { PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) + add(EmojiNotificationPackage()) } override fun getJSMainModuleName(): String = "index" diff --git a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt new file mode 100644 index 0000000..ed45fea --- /dev/null +++ b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt @@ -0,0 +1,346 @@ +package com.nerimityreactnative.custom + +import android.app.PendingIntent +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.text.Spanned +import android.widget.RemoteViews +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import com.nerimityreactnative.R +import java.net.URL + +class EmojiNotificationModule(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + companion object { + private const val MAX_MESSAGES = 6 + // channel id => list of (body, emojiBitmaps) + private val messageHistory = mutableMapOf>() + } + + private data class MessageEntry( + val body: String, + val unicodeBody: String, + val emojiBitmaps: Map + ) + + override fun getName(): String = "EmojiNotificationModule" + + @ReactMethod + fun displayNotification(params: ReadableMap) { + val context = reactApplicationContext + val id = params.getString("id") ?: return + val title = params.getString("title") ?: "" + val body = params.getString("body") ?: "" + val channelId = params.getString("channelId") ?: return + val largeIconUrl = if (params.hasKey("largeIcon") && !params.isNull("largeIcon")) params.getString("largeIcon") else null + val circularLargeIcon = if (params.hasKey("circularLargeIcon")) params.getBoolean("circularLargeIcon") else false + val subText = if (params.hasKey("subText") && !params.isNull("subText")) params.getString("subText") else null + val fallbackAvatarLetter = if (params.hasKey("fallbackAvatarLetter") && !params.isNull("fallbackAvatarLetter")) params.getString("fallbackAvatarLetter") else null + val fallbackAvatarColor = if (params.hasKey("fallbackAvatarColor") && !params.isNull("fallbackAvatarColor")) params.getString("fallbackAvatarColor") else null + + val emojis = mutableListOf() + if (params.hasKey("emojis") && !params.isNull("emojis")) { + val emojiArray = params.getArray("emojis") + if (emojiArray != null) { + for (i in 0 until emojiArray.size()) { + val emojiMap = emojiArray.getMap(i) + emojis.add( + EmojiInfo( + placeholder = emojiMap.getString("placeholder") ?: "", + url = emojiMap.getString("url") ?: "" + ) + ) + } + } + } + + CoroutineScope(Dispatchers.IO).launch { + val emojiBitmaps = mutableMapOf() + for (emoji in emojis) { + try { + val bitmap = downloadBitmap(emoji.url) + if (bitmap != null) { + emojiBitmaps[emoji.placeholder] = bitmap + } + } catch (_: Exception) {} + } + + var largeIconBitmap: Bitmap? = null + if (largeIconUrl != null) { + try { + val raw = downloadBitmap(largeIconUrl) + if (raw != null) { + largeIconBitmap = if (circularLargeIcon) makeCircular(raw) else raw + } + } catch (_: Exception) {} + } + if (largeIconBitmap == null && fallbackAvatarLetter != null && fallbackAvatarColor != null) { + largeIconBitmap = generateLetterAvatar(fallbackAvatarLetter, fallbackAvatarColor) + } + + // body + // unicode emoji fallback (text rendering) + val unicodeBody = replaceEmojiPlaceholdersWithUnicode(body, emojiBitmaps.keys) + + // add to message history + // store unicode body (text fallback) and keep bitmaps (image rendering) + val history = messageHistory.getOrPut(id) { mutableListOf() } + history.add(MessageEntry(body, unicodeBody, emojiBitmaps)) + while (history.size > MAX_MESSAGES) { + history.removeAt(0) + } + + // build collapsed view => only latest message, single line (+ ellipsis) + val latestSegments = splitBodyIntoSegments(body, emojiBitmaps.keys) + val isSingleEmoji = latestSegments.size == 1 && latestSegments[0].isEmoji + val appPackageName = context.packageName + + val collapsedView = RemoteViews(appPackageName, R.layout.notification_emoji_collapsed) + collapsedView.setTextViewText(R.id.notification_title, fromHtml(title)) + collapsedView.removeAllViews(R.id.notification_body_row) + if (isSingleEmoji && emojiBitmaps.containsKey(latestSegments[0].content)) { + val emojiView = RemoteViews(appPackageName, R.layout.notification_emoji_segment) + emojiView.setImageViewBitmap(R.id.segment_emoji, emojiBitmaps[latestSegments[0].content]) + collapsedView.addView(R.id.notification_body_row, emojiView) + } else { + val textView = RemoteViews(appPackageName, R.layout.notification_text_segment_single) + textView.setTextViewText(R.id.segment_text_single, fromHtml(unicodeBody)) + collapsedView.addView(R.id.notification_body_row, textView) + } + + val headsUpView = RemoteViews(appPackageName, R.layout.notification_emoji_collapsed) + headsUpView.setTextViewText(R.id.notification_title, fromHtml(title)) + headsUpView.removeAllViews(R.id.notification_body_row) + if (isSingleEmoji && emojiBitmaps.containsKey(latestSegments[0].content)) { + val emojiView = RemoteViews(appPackageName, R.layout.notification_emoji_segment) + emojiView.setImageViewBitmap(R.id.segment_emoji, emojiBitmaps[latestSegments[0].content]) + headsUpView.addView(R.id.notification_body_row, emojiView) + } else { + val textView = RemoteViews(appPackageName, R.layout.notification_text_segment_single) + textView.setTextViewText(R.id.segment_text_single, fromHtml(unicodeBody)) + headsUpView.addView(R.id.notification_body_row, textView) + } + + // build expanded view => complete message history + val expandedView = buildExpandedRemoteViews(id, title, isSingleEmoji && history.size == 1) + + withContext(Dispatchers.Main) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + ?: Intent() + launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + val pendingIntent = PendingIntent.getActivity( + context, id.hashCode(), launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(context.resources.getIdentifier("ic_stat_notify", "drawable", context.packageName)) + .setContentTitle(fromHtml(title)) + .setContentText(fromHtml(unicodeBody)) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCustomContentView(collapsedView) + .setCustomHeadsUpContentView(headsUpView) + .setCustomBigContentView(expandedView) + .setStyle(NotificationCompat.DecoratedCustomViewStyle()) + + if (subText != null) { + builder.setSubText(subText) + } + + if (largeIconBitmap != null) { + builder.setLargeIcon(largeIconBitmap) + } + + try { + NotificationManagerCompat.from(context).notify(id.hashCode(), builder.build()) + } catch (_: SecurityException) {} + } + } + } + + private fun buildExpandedRemoteViews( + channelId: String, + title: String, + isSingleEmoji: Boolean + ): RemoteViews { + val packageName = reactApplicationContext.packageName + val expandedView = RemoteViews(packageName, R.layout.notification_emoji_content) + expandedView.setTextViewText(R.id.notification_title, fromHtml(title)) + expandedView.removeAllViews(R.id.notification_body_row) + + val history = messageHistory[channelId] ?: return expandedView + + for (entry in history) { + val segments = splitBodyIntoSegments(entry.body, entry.emojiBitmaps.keys) + val unicodeSegments = splitBodyIntoSegments(entry.unicodeBody, entry.emojiBitmaps.keys) + val singleEmoji = isSingleEmoji && history.size == 1 + + // each message gets own horizontal row + val rowView = RemoteViews(packageName, R.layout.notification_message_row) + rowView.removeAllViews(R.id.message_row_content) + + for (i in segments.indices) { + val segment = segments[i] + if (segment.isEmoji && entry.emojiBitmaps.containsKey(segment.content)) { + val segmentLayoutId = if (singleEmoji) R.layout.notification_emoji_segment_large else R.layout.notification_emoji_segment + val emojiView = RemoteViews(packageName, segmentLayoutId) + emojiView.setImageViewBitmap(R.id.segment_emoji, entry.emojiBitmaps[segment.content]) + rowView.addView(R.id.message_row_content, emojiView) + } else if (segment.content.isNotEmpty()) { + val textContent = if (i < unicodeSegments.size && !unicodeSegments[i].isEmoji) unicodeSegments[i].content else segment.content + val textView = RemoteViews(packageName, R.layout.notification_text_segment) + textView.setTextViewText(R.id.segment_text, fromHtml(textContent)) + rowView.addView(R.id.message_row_content, textView) + } + } + + expandedView.addView(R.id.notification_body_row, rowView) + } + + return expandedView + } + + private fun buildBodyRemoteViews( + layoutId: Int, + segments: List, + emojiBitmaps: Map, + isSingleEmoji: Boolean + ): RemoteViews { + val packageName = reactApplicationContext.packageName + val contentView = RemoteViews(packageName, layoutId) + + contentView.removeAllViews(R.id.notification_body_row) + + for (segment in segments) { + if (segment.isEmoji && emojiBitmaps.containsKey(segment.content)) { + val segmentLayoutId = if (isSingleEmoji) R.layout.notification_emoji_segment_large else R.layout.notification_emoji_segment + val emojiView = RemoteViews(packageName, segmentLayoutId) + emojiView.setImageViewBitmap(R.id.segment_emoji, emojiBitmaps[segment.content]) + contentView.addView(R.id.notification_body_row, emojiView) + } else if (segment.content.isNotEmpty()) { + val textView = RemoteViews(packageName, R.layout.notification_text_segment) + textView.setTextViewText(R.id.segment_text, fromHtml(segment.content)) + contentView.addView(R.id.notification_body_row, textView) + } + } + + return contentView + } + + private fun splitBodyIntoSegments( + body: String, + placeholders: Set + ): List { + if (placeholders.isEmpty()) { + return listOf(Segment(body, false)) + } + + val segments = mutableListOf() + var remaining = body + + while (remaining.isNotEmpty()) { + var earliestIndex = Int.MAX_VALUE + var earliestPlaceholder = "" + + for (placeholder in placeholders) { + val index = remaining.indexOf(placeholder) + if (index in 0 until earliestIndex) { + earliestIndex = index + earliestPlaceholder = placeholder + } + } + + if (earliestIndex == Int.MAX_VALUE) { + segments.add(Segment(remaining, false)) + break + } + + if (earliestIndex > 0) { + segments.add(Segment(remaining.substring(0, earliestIndex), false)) + } + + segments.add(Segment(earliestPlaceholder, true)) + remaining = remaining.substring(earliestIndex + earliestPlaceholder.length) + } + + return segments + } + + private fun generateLetterAvatar(letter: String, hexColor: String): Bitmap { + val size = 128 + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG) + bgPaint.color = try { + android.graphics.Color.parseColor(hexColor) + } catch (_: Exception) { + android.graphics.Color.parseColor("#7c7c7c") + } + canvas.drawOval(RectF(0f, 0f, size.toFloat(), size.toFloat()), bgPaint) + + val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) + textPaint.color = android.graphics.Color.WHITE + textPaint.textSize = size * 0.45f + textPaint.textAlign = Paint.Align.CENTER + textPaint.typeface = android.graphics.Typeface.DEFAULT_BOLD + + val textBounds = android.graphics.Rect() + textPaint.getTextBounds(letter, 0, letter.length, textBounds) + val y = size / 2f + textBounds.height() / 2f + + canvas.drawText(letter, size / 2f, y, textPaint) + return bitmap + } + + private fun downloadBitmap(url: String): Bitmap? { + val connection = URL(url).openConnection() + connection.connectTimeout = 5000 + connection.readTimeout = 5000 + return connection.getInputStream().use { BitmapFactory.decodeStream(it) } + } + + private fun makeCircular(bitmap: Bitmap): Bitmap { + val size = minOf(bitmap.width, bitmap.height) + val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + val rect = RectF(0f, 0f, size.toFloat(), size.toFloat()) + canvas.drawOval(rect, paint) + paint.xfermode = android.graphics.PorterDuffXfermode(android.graphics.PorterDuff.Mode.SRC_IN) + canvas.drawBitmap(bitmap, null, rect, paint) + return output + } + + @Suppress("DEPRECATION") + private fun fromHtml(html: String): Spanned { + return android.text.Html.fromHtml(html, android.text.Html.FROM_HTML_MODE_COMPACT) + } + + private fun replaceEmojiPlaceholdersWithUnicode(text: String, placeholders: Set): String { + var result = text + for (placeholder in placeholders) { + result = result.replace(placeholder, "\uD83D\uDDBC\uFE0F") // current emoji: 🖼️ + } + return result + } + + private data class EmojiInfo(val placeholder: String, val url: String) + private data class Segment(val content: String, val isEmoji: Boolean) +} diff --git a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationPackage.kt b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationPackage.kt new file mode 100644 index 0000000..fb768c7 --- /dev/null +++ b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationPackage.kt @@ -0,0 +1,16 @@ +package com.nerimityreactnative.custom + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class EmojiNotificationPackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(EmojiNotificationModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} diff --git a/android/app/src/main/res/layout/notification_emoji_collapsed.xml b/android/app/src/main/res/layout/notification_emoji_collapsed.xml new file mode 100644 index 0000000..899ff6f --- /dev/null +++ b/android/app/src/main/res/layout/notification_emoji_collapsed.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/android/app/src/main/res/layout/notification_emoji_content.xml b/android/app/src/main/res/layout/notification_emoji_content.xml new file mode 100644 index 0000000..5ac3732 --- /dev/null +++ b/android/app/src/main/res/layout/notification_emoji_content.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/android/app/src/main/res/layout/notification_emoji_segment.xml b/android/app/src/main/res/layout/notification_emoji_segment.xml new file mode 100644 index 0000000..ccde100 --- /dev/null +++ b/android/app/src/main/res/layout/notification_emoji_segment.xml @@ -0,0 +1,8 @@ + + diff --git a/android/app/src/main/res/layout/notification_emoji_segment_large.xml b/android/app/src/main/res/layout/notification_emoji_segment_large.xml new file mode 100644 index 0000000..d3fadf8 --- /dev/null +++ b/android/app/src/main/res/layout/notification_emoji_segment_large.xml @@ -0,0 +1,6 @@ + + diff --git a/android/app/src/main/res/layout/notification_message_row.xml b/android/app/src/main/res/layout/notification_message_row.xml new file mode 100644 index 0000000..12a982e --- /dev/null +++ b/android/app/src/main/res/layout/notification_message_row.xml @@ -0,0 +1,7 @@ + + diff --git a/android/app/src/main/res/layout/notification_text_segment.xml b/android/app/src/main/res/layout/notification_text_segment.xml new file mode 100644 index 0000000..9dc625c --- /dev/null +++ b/android/app/src/main/res/layout/notification_text_segment.xml @@ -0,0 +1,6 @@ + + diff --git a/android/app/src/main/res/layout/notification_text_segment_single.xml b/android/app/src/main/res/layout/notification_text_segment_single.xml new file mode 100644 index 0000000..120bff9 --- /dev/null +++ b/android/app/src/main/res/layout/notification_text_segment_single.xml @@ -0,0 +1,9 @@ + + diff --git a/package-lock.json b/package-lock.json index aad83b6..bb35550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -195,13 +195,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -553,18 +554,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -595,38 +596,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/types": "^7.29.0" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -2277,26 +2266,23 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -2324,14 +2310,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2424,9 +2409,9 @@ "license": "Python-2.0" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2451,9 +2436,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2531,9 +2516,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -5389,9 +5374,9 @@ } }, "node_modules/@react-native/community-cli-plugin/node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "license": "MIT", "dependencies": { "queue": "6.0.2" @@ -6123,9 +6108,9 @@ } }, "node_modules/@react-native/metro-config/node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "dev": true, "license": "MIT", "dependencies": { @@ -7794,9 +7779,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7887,9 +7872,9 @@ "license": "MIT" }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7915,6 +7900,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -7987,29 +7985,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -8259,17 +8234,17 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -8291,6 +8266,35 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8512,9 +8516,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -8844,6 +8848,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9009,14 +9027,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, "engines": { "node": ">= 0.4" } @@ -9025,7 +9039,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9058,10 +9071,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -9071,15 +9083,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -9369,9 +9381,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -9468,9 +9480,9 @@ "license": "Python-2.0" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -9602,9 +9614,9 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -9898,22 +9910,18 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", - "integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" @@ -10195,14 +10203,16 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -10309,17 +10319,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10423,6 +10437,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -10488,9 +10515,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -10556,13 +10583,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10591,15 +10617,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -10627,10 +10644,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10643,7 +10659,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -10743,25 +10758,29 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13272,9 +13291,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -13642,9 +13661,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -13943,6 +13962,15 @@ "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", "license": "Apache-2.0" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mem": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", @@ -14949,9 +14977,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -15142,9 +15170,9 @@ } }, "node_modules/node-dir/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -15184,9 +15212,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -15385,9 +15413,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -15801,9 +15829,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -16802,9 +16830,9 @@ } }, "node_modules/react-native-macos/node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "license": "MIT", "peer": true, "dependencies": { @@ -18031,9 +18059,9 @@ "peer": true }, "node_modules/react-native-windows/node_modules/image-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", - "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "license": "MIT", "peer": true, "dependencies": { @@ -18869,12 +18897,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -19157,24 +19179,24 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -19195,6 +19217,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -19226,9 +19257,9 @@ } }, "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -19244,20 +19275,29 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -19722,9 +19762,15 @@ } }, "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT" }, "node_modules/sudo-prompt": { @@ -19781,18 +19827,6 @@ "node": ">=0.10.0" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -19889,9 +19923,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -19971,15 +20005,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "license": "BSD-3-Clause" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -20335,9 +20360,9 @@ } }, "node_modules/username/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "license": "MIT", "peer": true, "dependencies": { diff --git a/src/pushNotifications.ts b/src/pushNotifications.ts index 591823f..6981884 100644 --- a/src/pushNotifications.ts +++ b/src/pushNotifications.ts @@ -1,13 +1,12 @@ -import notifee, { - AndroidInboxStyle, - AndroidStyle, - AndroidVisibility, -} from '@notifee/react-native'; +import notifee from '@notifee/react-native'; +import {NativeModules} from 'react-native'; import env from './env'; import {getUserId} from './EncryptedStore'; import {dmChannelMatch, serverChannelMatch} from './UrlPatternMatchers'; import {currentUrl} from './components/CustomWebView'; +const {EmojiNotificationModule} = NativeModules; + export enum MessageType { CONTENT = 0, JOIN_SERVER = 1, @@ -80,24 +79,13 @@ export async function handlePushNotification( } export async function showServerPushNotification(data: ServerNotificationData) { - const existingNotification = await notifee - .getDisplayedNotifications() - .then(res => res.find(n => n.notification.id === data.channelId)); - - const selfUserId = - existingNotification?.notification?.data?.selfUserId || (await getUserId()); + const selfUserId = await getUserId(); if (selfUserId === data.cUserId) { return; } - const existingLines = - (existingNotification?.notification?.android?.style as AndroidInboxStyle) - ?.lines || []; - const creatorName = sanitize(data.cName); - - const username = `${creatorName}:`; let content = data.content; // lets assume its an image message @@ -123,58 +111,38 @@ export async function showServerPushNotification(data: ServerNotificationData) { content = 'has started a call.'; } - let newLines = [username, content]; + let emojis: {placeholder: string; url: string}[] = []; + if (type === MessageType.CONTENT && data.content) { + const result = replaceCustomEmojisWithPlaceholders(data.content); + content = formatMarkup(sanitize(result.body)); + emojis = result.emojis; + } + + const bodyText = `${creatorName}: ${content}`; - // Display a notification - await notifee.displayNotification({ + EmojiNotificationModule.displayNotification({ id: data.channelId, - title: `${sanitize(data.serverName)} #${sanitize(data.channelName)}`, - body: newLines.join(' '), - data: { - selfUserId: selfUserId!, - channelId: data.channelId, - serverId: data.serverId, - userId: data.cUserId, - }, - android: { - smallIcon: 'ic_stat_notify', - pressAction: { - id: 'default', - }, - groupId: Math.random().toString(), - visibility: AndroidVisibility.PUBLIC, - circularLargeIcon: true, - channelId: ANDROID_CHANNELS.serverMessages, - ...(data.sAvatar - ? {largeIcon: `${env.NERIMITY_CDN}${data.sAvatar}`} - : undefined), - style: { - type: AndroidStyle.INBOX, - title: `${sanitize(data.serverName)} #${sanitize( - data.channelName, - )}`, - lines: [...existingLines, ...newLines].slice(-5), - }, - }, + title: `${sanitize(data.serverName)} | #${sanitize(data.channelName)}`, + body: bodyText, + emojis, + channelId: ANDROID_CHANNELS.serverMessages, + subText: sanitize(data.serverName), + largeIcon: data.sAvatar + ? `${env.NERIMITY_CDN}${data.sAvatar}` + : null, + circularLargeIcon: true, + fallbackAvatarLetter: data.serverName?.charAt(0)?.toUpperCase() || '?', + fallbackAvatarColor: data.sHexColor || '#7c7c7c', }); } export async function showDMNotificationData(data: DMNotificationData) { - const existingNotification = await notifee - .getDisplayedNotifications() - .then(res => res.find(n => n.notification.id === data.channelId)); - - const selfUserId = - existingNotification?.notification?.data?.selfUserId || (await getUserId()); + const selfUserId = await getUserId(); if (selfUserId === data.cUserId) { return; } - const existingLines = - (existingNotification?.notification?.android?.style as AndroidInboxStyle) - ?.lines || []; - let newLine = sanitize(data.content); // lets assume its an image message @@ -187,34 +155,26 @@ export async function showDMNotificationData(data: DMNotificationData) { newLine = 'has started a call.'; } - // Display a notification - await notifee.displayNotification({ + let emojis: {placeholder: string; url: string}[] = []; + if (type === MessageType.CONTENT && data.content) { + const result = replaceCustomEmojisWithPlaceholders(data.content); + newLine = formatMarkup(sanitize(result.body)); + emojis = result.emojis; + } + + EmojiNotificationModule.displayNotification({ id: data.channelId, - title: `@${sanitize(data.cName)}`, + title: `${sanitize(data.cName)}`, body: newLine, - data: { - selfUserId: selfUserId!, - channelId: data.channelId, - userId: data.cUserId, - }, - android: { - smallIcon: 'ic_stat_notify', - pressAction: { - id: 'default', - }, - groupId: Math.random().toString(), - visibility: AndroidVisibility.PUBLIC, - circularLargeIcon: true, - channelId: ANDROID_CHANNELS.dmMessages, - ...(data.uAvatar - ? {largeIcon: `${env.NERIMITY_CDN}${data.uAvatar}`} - : undefined), - style: { - type: AndroidStyle.INBOX, - title: `@${sanitize(data.cName)}`, - lines: [...existingLines, newLine].slice(-5), - }, - }, + emojis, + channelId: ANDROID_CHANNELS.dmMessages, + subText: 'Direct', + largeIcon: data.uAvatar + ? `${env.NERIMITY_CDN}${data.uAvatar}` + : null, + circularLargeIcon: true, + fallbackAvatarLetter: data.cName?.charAt(0)?.toUpperCase() || '?', + fallbackAvatarColor: data.uHexColor || '#7c7c7c', }); } @@ -232,10 +192,72 @@ function sanitize(string?: string) { '&': '&', '<': '<', '>': '>', - '"': '"', - "'": ''', - '/': '/', } as const; - const reg = /[&<>"'/]/gi; + const reg = /[&<>]/g; return string.replace(reg, match => map[match as keyof typeof map]); } + +function formatMarkup(text: string): string { + // headers: # to ###### => bold + text = text.replace(/^#{1,6}\s+(.+)$/gm, '$1'); + // spoiler: ||text|| => [spoiler] + text = text.replace(/\|\|(.+?)\|\|/g, '[spoiler]'); + // bold+italic: ***text*** or ___text___ + text = text.replace(/\*\*\*(.*?)\*\*\*/g, '$1'); + text = text.replace(/___(.*?)___/g, '$1'); + // bold: **text** + text = text.replace(/\*\*(.*?)\*\*/g, '$1'); + // ttalic: *text* or _text_ + text = text.replace(/\*([^*]+)\*/g, '$1'); + text = text.replace(/(?$1'); + // strikethrough: ~~text~~ + text = text.replace(/~~(.*?)~~/g, '$1'); + // link: [text](url) => text + text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); + // checkbox: -[ ] or -[x] + text = text.replace(/-\[ \]/g, '☐'); + text = text.replace(/-\[x\]/g, '☑'); + // timestamp: [tr:1234567] => [timestamp] + text = text.replace(/\[tr:\d+\]/g, '[timestamp]'); + // color: [#hex]text => text + text = text.replace(/\[#[0-9a-fA-F]{3,8}\]/g, ''); + return text; +} + +const CUSTOM_EMOJI_REGEX = /:\[ce:(\d+):([^\]]+)\]/g; + +function getCustomEmojiUrl(emojiId: string): string { + return `${env.NERIMITY_CDN}emojis/${emojiId}.webp`; +} + +function extractAllCustomEmojis( + content: string, +): {id: string; name: string}[] { + const results: {id: string; name: string}[] = []; + const regex = /:\[ce:(\d+):([^\]]+)\]/g; + let match; + while ((match = regex.exec(content)) !== null) { + results.push({id: match[1], name: match[2]}); + } + return results; +} + +function replaceCustomEmojisWithPlaceholders(content: string): { + body: string; + emojis: {placeholder: string; url: string}[]; +} { + const emojis: {placeholder: string; url: string}[] = []; + const seen = new Set(); + const allEmojis = extractAllCustomEmojis(content); + + for (const emoji of allEmojis) { + const placeholder = `:${emoji.name}:`; + if (!seen.has(emoji.id)) { + seen.add(emoji.id); + emojis.push({placeholder, url: getCustomEmojiUrl(emoji.id)}); + } + } + + const body = content.replace(CUSTOM_EMOJI_REGEX, ':$2:'); + return {body, emojis}; +} From 37b02ae1a2b5aca03f2c5c50c098473b6974a910 Mon Sep 17 00:00:00 2001 From: mathiiiiiis Date: Tue, 17 Feb 2026 16:11:03 +0100 Subject: [PATCH 2/4] Fix custom emoji URL handling & clear notification history on dismiss Use correct file extensions for emoji types (gif for animated, webp for static/webp-animated) and clear message history when notifications are swiped away via a BroadcastReceiver --- android/app/src/main/AndroidManifest.xml | 3 +++ .../custom/EmojiNotificationModule.kt | 13 +++++++++++++ .../custom/NotificationDismissReceiver.kt | 16 ++++++++++++++++ src/pushNotifications.ts | 17 +++++++++-------- 4 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 android/app/src/main/java/com/nerimityreactnative/custom/NotificationDismissReceiver.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ea87431..5582641 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,5 +30,8 @@ + diff --git a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt index ed45fea..f6e2993 100644 --- a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt +++ b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt @@ -29,6 +29,10 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : private const val MAX_MESSAGES = 6 // channel id => list of (body, emojiBitmaps) private val messageHistory = mutableMapOf>() + + fun clearHistory(channelId: String) { + messageHistory.remove(channelId) + } } private data class MessageEntry( @@ -147,11 +151,20 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) + val dismissIntent = Intent(context, NotificationDismissReceiver::class.java).apply { + putExtra(NotificationDismissReceiver.EXTRA_CHANNEL_ID, id) + } + val deletePendingIntent = PendingIntent.getBroadcast( + context, id.hashCode(), dismissIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val builder = NotificationCompat.Builder(context, channelId) .setSmallIcon(context.resources.getIdentifier("ic_stat_notify", "drawable", context.packageName)) .setContentTitle(fromHtml(title)) .setContentText(fromHtml(unicodeBody)) .setContentIntent(pendingIntent) + .setDeleteIntent(deletePendingIntent) .setAutoCancel(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCustomContentView(collapsedView) diff --git a/android/app/src/main/java/com/nerimityreactnative/custom/NotificationDismissReceiver.kt b/android/app/src/main/java/com/nerimityreactnative/custom/NotificationDismissReceiver.kt new file mode 100644 index 0000000..3b7816b --- /dev/null +++ b/android/app/src/main/java/com/nerimityreactnative/custom/NotificationDismissReceiver.kt @@ -0,0 +1,16 @@ +package com.nerimityreactnative.custom + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class NotificationDismissReceiver : BroadcastReceiver() { + companion object { + const val EXTRA_CHANNEL_ID = "notification_channel_id" + } + + override fun onReceive(context: Context, intent: Intent) { + val channelId = intent.getStringExtra(EXTRA_CHANNEL_ID) ?: return + EmojiNotificationModule.clearHistory(channelId) + } +} diff --git a/src/pushNotifications.ts b/src/pushNotifications.ts index 6981884..706c5ed 100644 --- a/src/pushNotifications.ts +++ b/src/pushNotifications.ts @@ -224,20 +224,21 @@ function formatMarkup(text: string): string { return text; } -const CUSTOM_EMOJI_REGEX = /:\[ce:(\d+):([^\]]+)\]/g; +const CUSTOM_EMOJI_REGEX = /\[(?:w?a)?ce:(\d+):([^\]]+)\]/g; -function getCustomEmojiUrl(emojiId: string): string { - return `${env.NERIMITY_CDN}emojis/${emojiId}.webp`; +function getCustomEmojiUrl(emojiId: string, type: string): string { + const ext = type === 'ace' ? 'gif' : 'webp'; + return `${env.NERIMITY_CDN}emojis/${emojiId}.${ext}`; } function extractAllCustomEmojis( content: string, -): {id: string; name: string}[] { - const results: {id: string; name: string}[] = []; - const regex = /:\[ce:(\d+):([^\]]+)\]/g; +): {id: string; name: string; type: string}[] { + const results: {id: string; name: string; type: string}[] = []; + const regex = /\[((?:w?a)?ce):(\d+):([^\]]+)\]/g; let match; while ((match = regex.exec(content)) !== null) { - results.push({id: match[1], name: match[2]}); + results.push({id: match[2], name: match[3], type: match[1]}); } return results; } @@ -254,7 +255,7 @@ function replaceCustomEmojisWithPlaceholders(content: string): { const placeholder = `:${emoji.name}:`; if (!seen.has(emoji.id)) { seen.add(emoji.id); - emojis.push({placeholder, url: getCustomEmojiUrl(emoji.id)}); + emojis.push({placeholder, url: getCustomEmojiUrl(emoji.id, emoji.type)}); } } From 6b9b5f57d54e1bf8a07ba998dae50f1f963e6a73 Mon Sep 17 00:00:00 2001 From: mathiiiiiis Date: Wed, 25 Feb 2026 20:13:49 +0100 Subject: [PATCH 3/4] Fix notification emoji loading & formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Buffer bitmap bytes before decoding to fix unreliable emoji image loading on Samsung (BitmapFactory.decodeStream fails on non-marking network streams) - Replace 🖼️ fallback with :name: format for unloaded custom emojis - Bold sender username in server and DM notification message rows --- .../custom/EmojiNotificationModule.kt | 9 +++------ src/pushNotifications.ts | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt index f6e2993..c0c2080 100644 --- a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt +++ b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt @@ -326,7 +326,8 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : val connection = URL(url).openConnection() connection.connectTimeout = 5000 connection.readTimeout = 5000 - return connection.getInputStream().use { BitmapFactory.decodeStream(it) } + val bytes = connection.getInputStream().use { it.readBytes() } + return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) } private fun makeCircular(bitmap: Bitmap): Bitmap { @@ -347,11 +348,7 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : } private fun replaceEmojiPlaceholdersWithUnicode(text: String, placeholders: Set): String { - var result = text - for (placeholder in placeholders) { - result = result.replace(placeholder, "\uD83D\uDDBC\uFE0F") // current emoji: 🖼️ - } - return result + return text } private data class EmojiInfo(val placeholder: String, val url: String) diff --git a/src/pushNotifications.ts b/src/pushNotifications.ts index 706c5ed..3e9c398 100644 --- a/src/pushNotifications.ts +++ b/src/pushNotifications.ts @@ -118,7 +118,7 @@ export async function showServerPushNotification(data: ServerNotificationData) { emojis = result.emojis; } - const bodyText = `${creatorName}: ${content}`; + const bodyText = `${creatorName}: ${content}`; EmojiNotificationModule.displayNotification({ id: data.channelId, @@ -165,7 +165,7 @@ export async function showDMNotificationData(data: DMNotificationData) { EmojiNotificationModule.displayNotification({ id: data.channelId, title: `${sanitize(data.cName)}`, - body: newLine, + body: `${sanitize(data.cName)}: ${newLine}`, emojis, channelId: ANDROID_CHANNELS.dmMessages, subText: 'Direct', From c55fdb1a4abb409c01ea515e1519b25f2e4e0194 Mon Sep 17 00:00:00 2001 From: mathiiiiiis Date: Wed, 25 Feb 2026 20:37:27 +0100 Subject: [PATCH 4/4] Open channel on notification tap Pass channelId/serverId/userId as intent extras, capture them in MainActivity onCreate/onNewIntent, and retrieve from JS via a new getPendingNotificationClick() native method --- App.tsx | 11 +++++- .../com/nerimityreactnative/MainActivity.kt | 23 +++++++----- .../custom/EmojiNotificationModule.kt | 35 +++++++++++++++++++ src/pushNotifications.ts | 13 +++++++ 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/App.tsx b/App.tsx index 69bb544..302cc4b 100644 --- a/App.tsx +++ b/App.tsx @@ -9,7 +9,7 @@ import { import {CustomVideo, CustomVideoRef} from './src/components/ui/CustomVideo'; import TrackPlayer from 'react-native-track-player'; -import {handlePushNotification, registerNotificationChannels} from './src/pushNotifications'; +import {handlePushNotification, registerNotificationChannels, getPendingNotificationClick} from './src/pushNotifications'; import messaging, { FirebaseMessagingTypes, @@ -82,6 +82,11 @@ function App(): JSX.Element { ); useEffect(() => { + // check if app was launched by tapping a native notification (cold start) + getPendingNotificationClick().then(data => { + if (data) handleNotificationClick({data}); + }); + notifee.getInitialNotification().then(initN => { if (!initN?.notification) { return; @@ -98,6 +103,10 @@ function App(): JSX.Element { ); const event = AppState.addEventListener('focus', () => { + // check if app was resumed by tapping a native notification (background state) + getPendingNotificationClick().then(data => { + if (data) handleNotificationClick({data}); + }); if (backgroundClickedNotification) { handleNotificationClick(backgroundClickedNotification); } diff --git a/android/app/src/main/java/com/nerimityreactnative/MainActivity.kt b/android/app/src/main/java/com/nerimityreactnative/MainActivity.kt index 505e3bb..dc14965 100644 --- a/android/app/src/main/java/com/nerimityreactnative/MainActivity.kt +++ b/android/app/src/main/java/com/nerimityreactnative/MainActivity.kt @@ -1,22 +1,29 @@ package com.nerimityreactnative +import android.content.Intent +import android.os.Bundle import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate +import com.nerimityreactnative.custom.EmojiNotificationModule class MainActivity : ReactActivity() { - /** - * Returns the name of the main component registered from JavaScript. This is used to schedule - * rendering of the component. - */ override fun getMainComponentName(): String = "NerimityReactNative" - /** - * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] - * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] - */ override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) + + // called when app is launched from dead state via notification tap + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + intent?.let { EmojiNotificationModule.storeNotificationData(it) } + } + + // called when app is already running (background) and notification is tapped + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + intent?.let { EmojiNotificationModule.storeNotificationData(it) } + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt index c0c2080..e54d714 100644 --- a/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt +++ b/android/app/src/main/java/com/nerimityreactnative/custom/EmojiNotificationModule.kt @@ -11,6 +11,8 @@ import android.text.Spanned import android.widget.RemoteViews import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod @@ -30,9 +32,20 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : // channel id => list of (body, emojiBitmaps) private val messageHistory = mutableMapOf>() + var pendingChannelId: String? = null + var pendingServerId: String? = null + var pendingUserId: String? = null + fun clearHistory(channelId: String) { messageHistory.remove(channelId) } + + fun storeNotificationData(intent: android.content.Intent) { + val channelId = intent.getStringExtra("notif_channelId") ?: return + pendingChannelId = channelId + pendingServerId = intent.getStringExtra("notif_serverId") + pendingUserId = intent.getStringExtra("notif_userId") + } } private data class MessageEntry( @@ -55,6 +68,8 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : val subText = if (params.hasKey("subText") && !params.isNull("subText")) params.getString("subText") else null val fallbackAvatarLetter = if (params.hasKey("fallbackAvatarLetter") && !params.isNull("fallbackAvatarLetter")) params.getString("fallbackAvatarLetter") else null val fallbackAvatarColor = if (params.hasKey("fallbackAvatarColor") && !params.isNull("fallbackAvatarColor")) params.getString("fallbackAvatarColor") else null + val serverId = if (params.hasKey("serverId") && !params.isNull("serverId")) params.getString("serverId") else null + val userId = if (params.hasKey("userId") && !params.isNull("userId")) params.getString("userId") else null val emojis = mutableListOf() if (params.hasKey("emojis") && !params.isNull("emojis")) { @@ -146,6 +161,9 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: Intent() launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + launchIntent.putExtra("notif_channelId", id) + serverId?.let { launchIntent.putExtra("notif_serverId", it) } + userId?.let { launchIntent.putExtra("notif_userId", it) } val pendingIntent = PendingIntent.getActivity( context, id.hashCode(), launchIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE @@ -187,6 +205,23 @@ class EmojiNotificationModule(reactContext: ReactApplicationContext) : } } + @ReactMethod + fun getPendingNotificationClick(promise: Promise) { + val channelId = pendingChannelId + if (channelId != null) { + val map = Arguments.createMap() + map.putString("channelId", channelId) + pendingServerId?.let { map.putString("serverId", it) } + pendingUserId?.let { map.putString("userId", it) } + pendingChannelId = null + pendingServerId = null + pendingUserId = null + promise.resolve(map) + } else { + promise.resolve(null) + } + } + private fun buildExpandedRemoteViews( channelId: String, title: String, diff --git a/src/pushNotifications.ts b/src/pushNotifications.ts index 3e9c398..1838467 100644 --- a/src/pushNotifications.ts +++ b/src/pushNotifications.ts @@ -133,6 +133,7 @@ export async function showServerPushNotification(data: ServerNotificationData) { circularLargeIcon: true, fallbackAvatarLetter: data.serverName?.charAt(0)?.toUpperCase() || '?', fallbackAvatarColor: data.sHexColor || '#7c7c7c', + serverId: data.serverId, }); } @@ -175,6 +176,7 @@ export async function showDMNotificationData(data: DMNotificationData) { circularLargeIcon: true, fallbackAvatarLetter: data.cName?.charAt(0)?.toUpperCase() || '?', fallbackAvatarColor: data.uHexColor || '#7c7c7c', + userId: data.cUserId, }); } @@ -224,6 +226,17 @@ function formatMarkup(text: string): string { return text; } +export function getPendingNotificationClick(): Promise<{ + channelId: string; + serverId?: string; + userId?: string; +} | null> { + if (!EmojiNotificationModule?.getPendingNotificationClick) { + return Promise.resolve(null); + } + return EmojiNotificationModule.getPendingNotificationClick(); +} + const CUSTOM_EMOJI_REGEX = /\[(?:w?a)?ce:(\d+):([^\]]+)\]/g; function getCustomEmojiUrl(emojiId: string, type: string): string {