Skip to content

feat: add Sonar-compatible stickers#1442

Open
vincenzopalazzo wants to merge 11 commits into
marmot-protocol:masterfrom
vincenzopalazzo:codex/sonar-stickers
Open

feat: add Sonar-compatible stickers#1442
vincenzopalazzo wants to merge 11 commits into
marmot-protocol:masterfrom
vincenzopalazzo:codex/sonar-stickers

Conversation

@vincenzopalazzo

@vincenzopalazzo vincenzopalazzo commented Jul 15, 2026

Copy link
Copy Markdown

Summary

  • add Sonar pack discovery, trusted pack/Signal deep links, preview, install/uninstall, and Signal import UI
  • add an emoji/sticker composer picker with optimistic send, retry, exact hash-bound rendering, replies, chat previews, notifications, and transcript export
  • keep protocol authority in Marmot's encrypted SQLite projection; Android only holds a bounded session asset LRU
  • ship regenerated Kotlin bindings and JNI libraries for arm64, armv7, x86, and x86_64 pinned to reviewed native commit 61feace

Native dependency

Privacy and security

  • Signal keys are removed from the Activity/Compose input state before native import and never persisted
  • only canonical HTTPS Sonar/Signal origins are routed; embedded relay hints are not trusted
  • native verifies exact coordinate + shortcode + plaintext hash before any asset fetch or send
  • asset bytes are capped, session-only, account-scoped, and cleared on switch/sign-out
  • historical sticker assets remain in White Noise SQLite so hash-bound messages stay stable after pack edits

Verification

  • ./gradlew :app:testDevDebugUnitTest :app:lintDevDebug :app:ktlintCheck :app:verifyRoborazziDevDebug
  • native: 202 SQLite tests, 325 Marmot unit tests, 11 audit integrations, and 66 relay-runtime tests
  • native: clippy with warnings denied, formatting, and diff checks

Open in Stage

Summary by CodeRabbit

  • New Features
    • Added sticker pack discovery, search, import, installation, and removal in Settings.
    • Added emoji/sticker picker support for sending stickers in conversations.
    • Added sticker rendering across messages, replies, previews, notifications, and transcript exports.
    • Added support for opening sticker pack and Signal sticker links directly in the app.
  • Bug Fixes
    • Improved sticker asset caching and in-flight loading behavior.
    • Added safer handling for sticker sends/import failures.
  • Localization
    • Added sticker-related UI text across supported languages.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds end-to-end sticker support: native APIs and models, secure deep-link handling, sticker-pack management, composer selection and sending, cached asset rendering, message projection, notifications, transcript export, localized strings, tests, and a Marmot dependency pin.

Changes

Native sticker contract

Layer / File(s) Summary
UniFFI sticker APIs and models
app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
Adds sticker-pack operations, asset fetching, sending, FFI models, serialization converters, nullable sticker fields, and sticker-specific exceptions.

Inbound links and pack management

Layer / File(s) Summary
Deep-link classification and routing
app/src/main/AndroidManifest.xml, app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt, app/src/main/java/dev/ipf/whitenoise/android/core/StickerLinks.kt, app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/*
Recognizes Sonar and Signal sticker links, filters sensitive routes, and routes valid inputs to sticker settings with account and lock-state handling.
Sticker pack settings
app/src/main/java/dev/ipf/whitenoise/android/ui/settings/*
Adds pack discovery, search, import, preview, install, uninstall, synchronization, loading states, and sanitized error handling.

Message state and presentation

Layer / File(s) Summary
Asset caching and sending
app/src/main/java/dev/ipf/whitenoise/android/state/AppState.kt, app/src/main/java/dev/ipf/whitenoise/android/state/Controllers.kt, app/src/main/java/dev/ipf/whitenoise/android/core/Stickers.kt
Adds account/epoch-guarded asset caching, concurrent fetch deduplication, installed-pack state, optimistic sticker sends, retry handling, and sticker message tags.
Projection and rendering
app/src/main/java/dev/ipf/whitenoise/android/core/*, app/src/main/java/dev/ipf/whitenoise/android/notifications/*, app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/*, app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/*
Propagates sticker references through records, labels previews and notifications, exports structured transcript fields, and renders static or animated sticker assets.

Composer and resources

Layer / File(s) Summary
Sticker picker integration
app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationScreen.kt, app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/*
Adds emoji/sticker tabs, installed-pack grids, sticker selection, and send callbacks restricted to standalone messages.
Localized sticker resources
app/src/main/res/values*/strings.xml
Adds sticker labels, pack-management text, tabs, counts, notifications, and import/error messages across supported locales.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Sonar-compatible sticker support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@stage-review

stage-review Bot commented Jul 15, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
app/src/main/res/values-zh/strings.xml (1)

1012-1012: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent translation for "sticker".

This string translates "sticker" as "贴纸", but the rest of the file consistently uses "贴图" (e.g., lines 996-997). Consider updating it for consistency.

📝 Proposed fix
-    <string name="sticker_operation_failed">贴纸操作失败。请检查网络连接后重试。</string>
+    <string name="sticker_operation_failed">贴图操作失败。请检查网络连接后重试。</string>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-zh/strings.xml` at line 1012, Update the
sticker_operation_failed string to use “贴图” instead of “贴纸”, matching the
established translation used by nearby sticker-related strings while preserving
the rest of the message.
app/src/main/res/values-pt/strings.xml (1)

1027-1027: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent translation for "sticker".

This string uses the English word "sticker", but other strings in this file consistently translate it to "figurinha" (e.g., lines 1011-1012). Consider updating it for consistency.

📝 Proposed fix
-    <string name="sticker_operation_failed">A ação do sticker falhou. Verifique a conexão e tente novamente.</string>
+    <string name="sticker_operation_failed">A ação da figurinha falhou. Verifique a conexão e tente novamente.</string>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-pt/strings.xml` at line 1027, Update the
sticker_operation_failed translation to use the established Portuguese term
“figurinha” instead of “sticker,” matching the terminology used by nearby
sticker-related strings in the same resource file.
app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt (1)

147-162: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Ensure complete mutual exclusion for inbound routing targets.

If a profile payload intent is routed while a prior inboundStickerRequest is still pending (i.e. not yet consumed by the UI), the current logic sets inboundProfilePayload but does not clear inboundStickerRequest. This could result in both states being active simultaneously, potentially causing overlapping sheets or navigation races in the Compose layer.

Consider adding a fallback branch to clear the sticker request if a profile payload is active, ensuring that only one inbound navigation intent is handled at a time.

♻️ Proposed fix
         when {
             parsedTarget != null -> inboundStickerRequest = null
             stickerInput != null -> {
                 inboundNotificationTarget = null
                 inboundProfilePayload = null
                 inboundStickerRequest = InboundStickerRequest(stickerInput, appState.activeAccountRef)
             }
 
             sensitiveSignalStickerRoute -> {
                 // A malformed Signal link can still carry `pack_key`. Drop it
                 // from every retained route even though it cannot be imported.
                 inboundNotificationTarget = null
                 inboundProfilePayload = null
                 inboundStickerRequest = null
             }
+            inboundProfilePayload != null -> inboundStickerRequest = null
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt` around lines
147 - 162, Update the inbound routing logic around the parsedTarget,
stickerInput, and sensitiveSignalStickerRoute branches to add a profile-payload
fallback that clears inboundStickerRequest whenever inboundProfilePayload is
active. Preserve the existing clearing behavior for notification and profile
targets so only one inbound navigation state remains pending at a time.
app/src/main/java/dev/ipf/whitenoise/android/state/AppState.kt (1)

1863-1900: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Verify: duplicate inFlightStickerAssets clear, and confirm Main-thread confinement for the sticker LRU.

Two observations, both low-confidence and worth a second look rather than confirmed bugs:

  • clearInMemoryMediaCaches() clears/cancels inFlightStickerAssets twice (top block and bottom block). The analogous inFlightDownloads map is cleared only once in the same function. If this is intentional defense against a cross-thread race (e.g. sign-out running off-Main while a StickerImage composable concurrently starts a fetch on the UI thread), a short comment would help; otherwise the second block looks like dead code left over from the diff.
  • stickerAssetCache/inFlightStickerAssets are mutated from stickerAsset() with no explicit dispatcher confinement, whereas this file documents elsewhere that ByteSizeLruCache is backed by a non-thread-safe LinkedHashMap and access must stay Main-confined (see the comment in evictExpiredMediaCaches). Today's only caller (StickerImage.kt) is Main-confined via a Composable LaunchedEffect, so this isn't an active bug, but nothing in stickerAsset() enforces or documents that contract for future callers.

Also applies to: 2443-2453, 2494-2497

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/whitenoise/android/state/AppState.kt` around lines
1863 - 1900, Review clearInMemoryMediaCaches and remove the duplicate
inFlightStickerAssets clearing block unless it intentionally protects a
cross-thread race; if retained, document that reason. In stickerAsset,
explicitly preserve or enforce Main-thread access for stickerAssetCache and
inFlightStickerAssets, matching the ByteSizeLruCache confinement contract
described near evictExpiredMediaCaches, and document the requirement if
enforcement is not practical.
app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.kt (1)

800-806: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify modifier layout directives.

Since the parent Box explicitly constrains the size to 196.dp, you can use Modifier.fillMaxSize() on the child StickerImage instead of repeating the width and height constraints.

♻️ Proposed refactor
-                        Box(modifier = Modifier.size(196.dp)) {
-                            StickerImage(
-                                appState = appState,
-                                stickerRef = stickerRef,
-                                contentDescription = stringResource(R.string.sticker),
-                                modifier = Modifier.fillMaxWidth().height(196.dp),
-                            )
+                        Box(modifier = Modifier.size(196.dp)) {
+                            StickerImage(
+                                appState = appState,
+                                stickerRef = stickerRef,
+                                contentDescription = stringResource(R.string.sticker),
+                                modifier = Modifier.fillMaxSize(),
+                            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.kt`
around lines 800 - 806, Update the StickerImage call inside the 196.dp Box in
MessageBubble to use Modifier.fillMaxSize() instead of separately applying
fillMaxWidth() and height(196.dp), preserving the existing parent-constrained
layout.
app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.kt (1)

36-94: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Decouple contentDescription from the LaunchedEffect key.

Currently, if the contentDescription string changes (e.g., due to a localized string update) or a different string instance is passed on recomposition, the LaunchedEffect cancels and re-triggers. This forces an unnecessary re-fetch and decode of the sticker asset.

You can prevent this by removing contentDescription from the remember and LaunchedEffect keys, storing only the loaded alt text in state, and resolving the final description during the render phase.

♻️ Proposed refactor
-    var presentation by remember(stickerRef) { mutableStateOf<DecodedAttachmentPresentation?>(null) }
-    var failed by remember(stickerRef) { mutableStateOf(false) }
-    var resolvedContentDescription by remember(stickerRef, contentDescription) { mutableStateOf(contentDescription) }
-
-    LaunchedEffect(stickerRef, contentDescription) {
+    var presentation by remember(stickerRef) { mutableStateOf<DecodedAttachmentPresentation?>(null) }
+    var failed by remember(stickerRef) { mutableStateOf(false) }
+    var loadedAltText by remember(stickerRef) { mutableStateOf<String?>(null) }
+
+    LaunchedEffect(stickerRef) {
         failed = false
         presentation = null
-        resolvedContentDescription = contentDescription
+        loadedAltText = null
         try {
             val asset = appState.stickerAsset(stickerRef)
-            resolvedContentDescription = asset.sticker.alt?.takeIf { it.isNotBlank() } ?: contentDescription
+            loadedAltText = asset.sticker.alt?.takeIf { it.isNotBlank() }
             presentation =
                 decodeMessageAttachmentImage(
                     bytes = asset.bytes,
                     mediaType = asset.sticker.mime,
                     staticMaxEdgePx = 768,
                     animatedMaxEdgePx = 768,
                 ) ?: run {
                     failed = true
                     null
                 }
         } catch (cancel: CancellationException) {
             throw cancel
         } catch (_: Throwable) {
             failed = true
         }
     }
 
     Box(modifier = modifier, contentAlignment = Alignment.Center) {
+        val finalContentDescription = loadedAltText ?: contentDescription
         when (val current = presentation) {
             is DecodedAttachmentPresentation.Static ->
                 Image(
                     bitmap = current.toImageBitmap(),
-                    contentDescription = resolvedContentDescription,
+                    contentDescription = finalContentDescription,
                     contentScale = contentScale,
                     modifier = Modifier.fillMaxSize(),
                 )
 
             is DecodedAttachmentPresentation.Animated ->
                 AnimatedDrawableAttachmentImage(
                     drawable = current.drawable,
-                    contentDescription = resolvedContentDescription,
+                    contentDescription = finalContentDescription,
                     contentScale = contentScale,
                     modifier = Modifier.fillMaxSize(),
                 )
 
             null ->
                 if (failed) {
                     Icon(
                         imageVector = Icons.Default.BrokenImage,
-                        contentDescription = resolvedContentDescription,
+                        contentDescription = finalContentDescription,
                         tint = MaterialTheme.colorScheme.onSurfaceVariant,
                     )
                 } else {
                     CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
                 }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.kt`
around lines 36 - 94, Decouple contentDescription from the sticker-loading
effect: in StickerImage, key the presentation and failure state only by
stickerRef, and key LaunchedEffect only by stickerRef. Store only the loaded
sticker alt text (or null) from the asset, then resolve the rendered description
from that state with contentDescription as the fallback during composition; do
not reset or refetch the asset when contentDescription changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt`:
- Around line 94-105: Replace busy-based coordination in runAction and its
callers with the existing account-scoped serialized mutation pattern, covering
account loading, debounced searches, and inbound-input effects. Ensure
concurrent sticker operations cannot overlap, and discard reload results when
their account or search query is no longer current; preserve cancellation
propagation and always clear busy only for the serialized operation that owns
it.

In `@app/src/main/marmotkit/MARMOT_VERSION`:
- Around line 1-4: The MARMOT_VERSION metadata pins bindings to a personal fork
and feature branch instead of the official upstream source. Update source and
branch metadata to use marmot-protocol/mdk mainline, ensure upstream native
changes are available there, and regenerate the bindings consistently with
scripts/release.sh before release.

---

Nitpick comments:
In `@app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt`:
- Around line 147-162: Update the inbound routing logic around the parsedTarget,
stickerInput, and sensitiveSignalStickerRoute branches to add a profile-payload
fallback that clears inboundStickerRequest whenever inboundProfilePayload is
active. Preserve the existing clearing behavior for notification and profile
targets so only one inbound navigation state remains pending at a time.

In `@app/src/main/java/dev/ipf/whitenoise/android/state/AppState.kt`:
- Around line 1863-1900: Review clearInMemoryMediaCaches and remove the
duplicate inFlightStickerAssets clearing block unless it intentionally protects
a cross-thread race; if retained, document that reason. In stickerAsset,
explicitly preserve or enforce Main-thread access for stickerAssetCache and
inFlightStickerAssets, matching the ByteSizeLruCache confinement contract
described near evictExpiredMediaCaches, and document the requirement if
enforcement is not practical.

In
`@app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.kt`:
- Around line 800-806: Update the StickerImage call inside the 196.dp Box in
MessageBubble to use Modifier.fillMaxSize() instead of separately applying
fillMaxWidth() and height(196.dp), preserving the existing parent-constrained
layout.

In `@app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.kt`:
- Around line 36-94: Decouple contentDescription from the sticker-loading
effect: in StickerImage, key the presentation and failure state only by
stickerRef, and key LaunchedEffect only by stickerRef. Store only the loaded
sticker alt text (or null) from the asset, then resolve the rendered description
from that state with contentDescription as the fallback during composition; do
not reset or refetch the asset when contentDescription changes.

In `@app/src/main/res/values-pt/strings.xml`:
- Line 1027: Update the sticker_operation_failed translation to use the
established Portuguese term “figurinha” instead of “sticker,” matching the
terminology used by nearby sticker-related strings in the same resource file.

In `@app/src/main/res/values-zh/strings.xml`:
- Line 1012: Update the sticker_operation_failed string to use “贴图” instead of
“贴纸”, matching the established translation used by nearby sticker-related
strings while preserving the rest of the message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdd8202f-6d85-4671-a3f0-6cdb7756eac3

📥 Commits

Reviewing files that changed from the base of the PR and between 15009ef and 0b51cf3.

⛔ Files ignored due to path filters (4)
  • app/src/main/jniLibs/arm64-v8a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/armeabi-v7a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86_64/libmarmot_uniffi.so is excluded by !**/*.so
📒 Files selected for processing (48)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
  • app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExport.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/MessageProjector.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/StickerLinks.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/Stickers.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/TimelineProjector.kt
  • app/src/main/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatter.kt
  • app/src/main/java/dev/ipf/whitenoise/android/state/AppState.kt
  • app/src/main/java/dev/ipf/whitenoise/android/state/Controllers.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/WhiteNoiseApp.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/common/CopyBundles.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/ComposerBar.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/EmojiPicker.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/replies/ReplyPreviewCard.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/AppDestinations.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/MainShell.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SettingsScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.kt
  • app/src/main/marmotkit/MARMOT_VERSION
  • app/src/main/res/values-b+zh+Hant/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-pt/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/dev/ipf/whitenoise/android/InboundStickerIntentTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExportTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/MessageProjectorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/StickerLinksTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/StickersTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/TimelineProjectorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatterTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/ChatRowPreviewMarkdownSourceTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/StickerAssetAccountGuardTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/TransientRelaySendErrorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/ComposerStickerBehaviorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/navigation/StickerInboundRoutingTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksErrorTest.kt
  • docs/brainstorms/2026-07-15-sonar-compatible-stickers.md

Comment thread app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt Outdated
Comment thread app/src/main/marmotkit/MARMOT_VERSION
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vincenzopalazzo

Copy link
Copy Markdown
Author

Fixed in 1190fc2. Also addressed all six collapsed nitpicks: profile/sticker route mutual exclusion, duplicate in-flight clear plus Main-confined sticker LRU access, simplified sticker sizing, removal of content-description-driven refetches, and consistent Portuguese/Chinese sticker terminology. Full Play/Zapstore unit, lint, ktlint, and Roborazzi verification is green.

@vincenzopalazzo
vincenzopalazzo marked this pull request as ready for review July 16, 2026 10:35
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/dev/ipf/whitenoise/android/core/MessageProjector.kt`:
- Line 107: Update isForwardableText and copyableText to require message.sticker
== null in addition to their existing media and plaintext checks. Preserve
current behavior for non-sticker messages while preventing sticker fallback
plaintext from being forwarded or copied as text.

In `@app/src/main/java/dev/ipf/whitenoise/android/core/TimelineProjector.kt`:
- Line 13: Update MessageProjector.mediaKind(message) to check message.sticker
before the existing imeta-based media resolution and return
ReplyMediaKind.Sticker when it is non-null; preserve the current behavior for
all other media types.

In
`@app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt`:
- Around line 296-314: In the StickerPackCard row within the packs list, capture
the current item once (for example, as a local pack value) before constructing
the callbacks, then use that captured item for the card, preview, and
install/uninstall action. Remove all callback-time reads of packs[index] so
reloads or list changes cannot select a different pack or access an invalid
index.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b59126f-3f40-4808-b4d1-dc92152540d0

📥 Commits

Reviewing files that changed from the base of the PR and between 0b51cf3 and 7bc8b92.

⛔ Files ignored due to path filters (4)
  • app/src/main/jniLibs/arm64-v8a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/armeabi-v7a/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86/libmarmot_uniffi.so is excluded by !**/*.so
  • app/src/main/jniLibs/x86_64/libmarmot_uniffi.so is excluded by !**/*.so
📒 Files selected for processing (48)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/dev/ipf/marmotkit/marmot_uniffi.kt
  • app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExport.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/MessageProjector.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/StickerLinks.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/Stickers.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/TimelineProjector.kt
  • app/src/main/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatter.kt
  • app/src/main/java/dev/ipf/whitenoise/android/state/AppState.kt
  • app/src/main/java/dev/ipf/whitenoise/android/state/Controllers.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/WhiteNoiseApp.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/common/CopyBundles.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/ComposerBar.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/EmojiPicker.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/replies/ReplyPreviewCard.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/AppDestinations.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/MainShell.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SettingsScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.kt
  • app/src/main/marmotkit/MARMOT_VERSION
  • app/src/main/res/values-b+zh+Hant/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-pt/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/res/values-zh/strings.xml
  • app/src/main/res/values/strings.xml
  • app/src/test/java/dev/ipf/whitenoise/android/InboundStickerIntentTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExportTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/MessageProjectorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/StickerLinksTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/StickersTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/TimelineProjectorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatterTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/ChatRowPreviewMarkdownSourceTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/StickerAssetAccountGuardTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/TransientRelaySendErrorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/ComposerStickerBehaviorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/navigation/StickerInboundRoutingTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksErrorTest.kt
  • docs/brainstorms/2026-07-15-sonar-compatible-stickers.md
🚧 Files skipped from review as they are similar to previous changes (41)
  • app/src/main/marmotkit/MARMOT_VERSION
  • app/src/test/java/dev/ipf/whitenoise/android/core/StickersTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/ui/ComposerStickerBehaviorTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatterTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/MessageProjectorTest.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/AppDestinations.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/StickerAssetAccountGuardTest.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/Stickers.kt
  • app/src/main/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatter.kt
  • app/src/main/res/values-pt/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/java/dev/ipf/whitenoise/android/ui/WhiteNoiseApp.kt
  • app/src/main/res/values-tr/strings.xml
  • app/src/main/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExport.kt
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-b+zh+Hant/strings.xml
  • app/src/test/java/dev/ipf/whitenoise/android/ui/navigation/StickerInboundRoutingTest.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/java/dev/ipf/whitenoise/android/ui/common/CopyBundles.kt
  • app/src/main/res/values-zh/strings.xml
  • app/src/test/java/dev/ipf/whitenoise/android/InboundStickerIntentTest.kt
  • app/src/main/AndroidManifest.xml
  • app/src/test/java/dev/ipf/whitenoise/android/core/TimelineProjectorTest.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/ChatRowPreviewMarkdownSourceTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/StickerLinksTest.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/settings/SettingsScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationScreen.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/ComposerBar.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/replies/ReplyPreviewCard.kt
  • app/src/main/java/dev/ipf/whitenoise/android/MainActivity.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/navigation/MainShell.kt
  • app/src/test/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExportTest.kt
  • app/src/test/java/dev/ipf/whitenoise/android/state/TransientRelaySendErrorTest.kt
  • app/src/main/java/dev/ipf/whitenoise/android/core/StickerLinks.kt
  • app/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/EmojiPicker.kt
  • app/src/main/java/dev/ipf/whitenoise/android/state/Controllers.kt

// body. The conversation row builds the name-resolved summary;
// this name-free form covers reply previews and copy-text.
isGroupSystem(message) -> GroupSystemEvents.previewText(message.plaintext, copy.groupSystem)
message.sticker != null -> copy.sticker

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle stickers in isForwardableText and copyableText.

While stickers render correctly here, if a cross-platform client sends a sticker with fallback plaintext (like a shortcode or alt-text), it will slip past isForwardableText and copyableText because they only check !isMedia(message) and message.plaintext.isNotBlank().
Explicitly exclude stickers in those methods (message.sticker == null) to ensure fallback text isn't inadvertently copied or forwarded as a text message.

🐛 Proposed fixes

For isForwardableText:

     fun isForwardableText(message: AppMessageRecordFfi): Boolean =
         message.kind == KindChat &&
+            message.sticker == null &&
             !isMedia(message) &&
             !isPendingMedia(message) &&
             streamId(message) == null &&
             message.plaintext.isNotBlank()

For copyableText:

     fun copyableText(
         message: AppMessageRecordFfi,
         editedText: String? = null,
     ): String? {
-        if (message.kind != KindChat || streamId(message) != null) return null
+        if (message.kind != KindChat || streamId(message) != null || message.sticker != null) return null
         val body = editedText?.takeIf { it.isNotBlank() } ?: message.plaintext
         return body.takeIf { it.isNotBlank() }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/whitenoise/android/core/MessageProjector.kt` at
line 107, Update isForwardableText and copyableText to require message.sticker
== null in addition to their existing media and plaintext checks. Preserve
current behavior for non-sticker messages while preventing sticker fallback
plaintext from being forwarded or copied as text.

)

enum class ReplyMediaKind { None, Photo, Video, Voice, Document }
enum class ReplyMediaKind { None, Photo, Video, Voice, Document, Sticker }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Support ReplyMediaKind.Sticker in MessageProjector.mediaKind.

With the addition of the Sticker media kind, MessageProjector.mediaKind(message) should be updated to return ReplyMediaKind.Sticker when message.sticker != null. Currently, it returns ReplyMediaKind.None for stickers because they lack an imeta tag, which could lead to incorrect media-kind resolution for callers relying on that public method.

🐛 Proposed fix for `MessageProjector.kt`
-    fun mediaKind(message: AppMessageRecordFfi): ReplyMediaKind =
-        if (isMedia(message)) replyMediaKindFromMime(imetaField(message, "m")) else ReplyMediaKind.None
+    fun mediaKind(message: AppMessageRecordFfi): ReplyMediaKind =
+        when {
+            message.sticker != null -> ReplyMediaKind.Sticker
+            isMedia(message) -> replyMediaKindFromMime(imetaField(message, "m"))
+            else -> ReplyMediaKind.None
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/dev/ipf/whitenoise/android/core/TimelineProjector.kt` at
line 13, Update MessageProjector.mediaKind(message) to check message.sticker
before the existing imeta-based media resolution and return
ReplyMediaKind.Sticker when it is non-null; preserve the current behavior for
all other media types.

Comment on lines +296 to +314
items(packs.size, key = { packs[it].coordinate }) { index ->
StickerPackCard(
appState = appState,
pack = packs[index],
enabled = !operationInProgress,
onOpen = { preview = packs[index] },
onToggleInstall = {
val selected = packs[index]
scope.launch {
runAction { current ->
if (selected.installed) {
appState.marmotIo { uninstallStickerPack(current, selected.coordinate) }
} else {
appState.marmotIo { installStickerPack(current, selected.coordinate) }
}
reload(current, search.trim().takeIf { it.isNotEmpty() })
}
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant section with line numbers.
git ls-files app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt
wc -l app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt
sed -n '240,360p' app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt

Repository: marmot-protocol/whitenoise-android

Length of output: 6080


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect nearby declarations that affect list identity and callback lifetimes.
rg -n "operationInProgress|reload\\(|runAction|StickerPackCard|items\\(" app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt

Repository: marmot-protocol/whitenoise-android

Length of output: 1642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the full composable around the list and the supporting state definitions.
sed -n '1,240p' app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt

Repository: marmot-protocol/whitenoise-android

Length of output: 10230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find how `packs` is stored and updated.
rg -n "packs\s*=|mutableStateListOf|mutableStateOf|collectAsState|reload\(" app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt

Repository: marmot-protocol/whitenoise-android

Length of output: 1377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the card implementation and the surrounding list item to see whether callbacks are stored or used immediately.
sed -n '391,470p' app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt

Repository: marmot-protocol/whitenoise-android

Length of output: 1643


Capture the item once per row instead of reading packs[index] inside the callbacks.
Those lambdas resolve the current list when they fire, so a reload or shrink can point them at a different pack or an out-of-bounds index.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.kt`
around lines 296 - 314, In the StickerPackCard row within the packs list,
capture the current item once (for example, as a local pack value) before
constructing the callbacks, then use that captured item for the card, preview,
and install/uninstall action. Remove all callback-time reads of packs[index] so
reloads or list changes cannot select a different pack or access an invalid
index.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant