feat: add Sonar-compatible stickers#1442
Conversation
WalkthroughThe 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. ChangesNative sticker contract
Inbound links and pack management
Message state and presentation
Composer and resources
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Ready to review this PR? Stage has broken it down into 11 individual chapters for you: Chapters generated by Stage for commit 7bc8b92 on Jul 15, 2026 10:06pm UTC. |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (6)
app/src/main/res/values-zh/strings.xml (1)
1012-1012: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent 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 valueInconsistent 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 winEnsure complete mutual exclusion for inbound routing targets.
If a profile payload intent is routed while a prior
inboundStickerRequestis still pending (i.e. not yet consumed by the UI), the current logic setsinboundProfilePayloadbut does not clearinboundStickerRequest. 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 valueVerify: duplicate
inFlightStickerAssetsclear, 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/cancelsinFlightStickerAssetstwice (top block and bottom block). The analogousinFlightDownloadsmap 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 aStickerImagecomposable 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/inFlightStickerAssetsare mutated fromstickerAsset()with no explicit dispatcher confinement, whereas this file documents elsewhere thatByteSizeLruCacheis backed by a non-thread-safeLinkedHashMapand access must stay Main-confined (see the comment inevictExpiredMediaCaches). Today's only caller (StickerImage.kt) is Main-confined via a ComposableLaunchedEffect, so this isn't an active bug, but nothing instickerAsset()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 valueSimplify modifier layout directives.
Since the parent
Boxexplicitly constrains the size to196.dp, you can useModifier.fillMaxSize()on the childStickerImageinstead 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 winDecouple
contentDescriptionfrom theLaunchedEffectkey.Currently, if the
contentDescriptionstring changes (e.g., due to a localized string update) or a different string instance is passed on recomposition, theLaunchedEffectcancels and re-triggers. This forces an unnecessary re-fetch and decode of the sticker asset.You can prevent this by removing
contentDescriptionfrom therememberandLaunchedEffectkeys, storing only the loadedalttext 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
⛔ Files ignored due to path filters (4)
app/src/main/jniLibs/arm64-v8a/libmarmot_uniffi.sois excluded by!**/*.soapp/src/main/jniLibs/armeabi-v7a/libmarmot_uniffi.sois excluded by!**/*.soapp/src/main/jniLibs/x86/libmarmot_uniffi.sois excluded by!**/*.soapp/src/main/jniLibs/x86_64/libmarmot_uniffi.sois excluded by!**/*.so
📒 Files selected for processing (48)
app/src/main/AndroidManifest.xmlapp/src/main/java/dev/ipf/marmotkit/marmot_uniffi.ktapp/src/main/java/dev/ipf/whitenoise/android/MainActivity.ktapp/src/main/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExport.ktapp/src/main/java/dev/ipf/whitenoise/android/core/MessageProjector.ktapp/src/main/java/dev/ipf/whitenoise/android/core/StickerLinks.ktapp/src/main/java/dev/ipf/whitenoise/android/core/Stickers.ktapp/src/main/java/dev/ipf/whitenoise/android/core/TimelineProjector.ktapp/src/main/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatter.ktapp/src/main/java/dev/ipf/whitenoise/android/state/AppState.ktapp/src/main/java/dev/ipf/whitenoise/android/state/Controllers.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/WhiteNoiseApp.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/common/CopyBundles.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationScreen.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/ComposerBar.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/EmojiPicker.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/replies/ReplyPreviewCard.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/navigation/AppDestinations.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/navigation/MainShell.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/settings/SettingsScreen.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.ktapp/src/main/marmotkit/MARMOT_VERSIONapp/src/main/res/values-b+zh+Hant/strings.xmlapp/src/main/res/values-de/strings.xmlapp/src/main/res/values-es/strings.xmlapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values-it/strings.xmlapp/src/main/res/values-pt/strings.xmlapp/src/main/res/values-ru/strings.xmlapp/src/main/res/values-tr/strings.xmlapp/src/main/res/values-zh/strings.xmlapp/src/main/res/values/strings.xmlapp/src/test/java/dev/ipf/whitenoise/android/InboundStickerIntentTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExportTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/MessageProjectorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/StickerLinksTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/StickersTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/TimelineProjectorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatterTest.ktapp/src/test/java/dev/ipf/whitenoise/android/state/ChatRowPreviewMarkdownSourceTest.ktapp/src/test/java/dev/ipf/whitenoise/android/state/StickerAssetAccountGuardTest.ktapp/src/test/java/dev/ipf/whitenoise/android/state/TransientRelaySendErrorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/ui/ComposerStickerBehaviorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/ui/navigation/StickerInboundRoutingTest.ktapp/src/test/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksErrorTest.ktdocs/brainstorms/2026-07-15-sonar-compatible-stickers.md
0b51cf3 to
db94b29
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
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. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (4)
app/src/main/jniLibs/arm64-v8a/libmarmot_uniffi.sois excluded by!**/*.soapp/src/main/jniLibs/armeabi-v7a/libmarmot_uniffi.sois excluded by!**/*.soapp/src/main/jniLibs/x86/libmarmot_uniffi.sois excluded by!**/*.soapp/src/main/jniLibs/x86_64/libmarmot_uniffi.sois excluded by!**/*.so
📒 Files selected for processing (48)
app/src/main/AndroidManifest.xmlapp/src/main/java/dev/ipf/marmotkit/marmot_uniffi.ktapp/src/main/java/dev/ipf/whitenoise/android/MainActivity.ktapp/src/main/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExport.ktapp/src/main/java/dev/ipf/whitenoise/android/core/MessageProjector.ktapp/src/main/java/dev/ipf/whitenoise/android/core/StickerLinks.ktapp/src/main/java/dev/ipf/whitenoise/android/core/Stickers.ktapp/src/main/java/dev/ipf/whitenoise/android/core/TimelineProjector.ktapp/src/main/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatter.ktapp/src/main/java/dev/ipf/whitenoise/android/state/AppState.ktapp/src/main/java/dev/ipf/whitenoise/android/state/Controllers.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/WhiteNoiseApp.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/common/CopyBundles.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/ConversationScreen.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/ComposerBar.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/composer/EmojiPicker.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/messages/MessageBubble.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/conversation/replies/ReplyPreviewCard.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/navigation/AppDestinations.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/navigation/MainShell.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/settings/SettingsScreen.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksScreen.ktapp/src/main/java/dev/ipf/whitenoise/android/ui/stickers/StickerImage.ktapp/src/main/marmotkit/MARMOT_VERSIONapp/src/main/res/values-b+zh+Hant/strings.xmlapp/src/main/res/values-de/strings.xmlapp/src/main/res/values-es/strings.xmlapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values-it/strings.xmlapp/src/main/res/values-pt/strings.xmlapp/src/main/res/values-ru/strings.xmlapp/src/main/res/values-tr/strings.xmlapp/src/main/res/values-zh/strings.xmlapp/src/main/res/values/strings.xmlapp/src/test/java/dev/ipf/whitenoise/android/InboundStickerIntentTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/ConversationTranscriptExportTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/MessageProjectorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/StickerLinksTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/StickersTest.ktapp/src/test/java/dev/ipf/whitenoise/android/core/TimelineProjectorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/notifications/LocalNotificationFormatterTest.ktapp/src/test/java/dev/ipf/whitenoise/android/state/ChatRowPreviewMarkdownSourceTest.ktapp/src/test/java/dev/ipf/whitenoise/android/state/StickerAssetAccountGuardTest.ktapp/src/test/java/dev/ipf/whitenoise/android/state/TransientRelaySendErrorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/ui/ComposerStickerBehaviorTest.ktapp/src/test/java/dev/ipf/whitenoise/android/ui/navigation/StickerInboundRoutingTest.ktapp/src/test/java/dev/ipf/whitenoise/android/ui/settings/StickerPacksErrorTest.ktdocs/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 |
There was a problem hiding this comment.
🎯 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 } |
There was a problem hiding this comment.
🎯 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.
| 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() }) | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
🗄️ 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.ktRepository: 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.ktRepository: 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.ktRepository: 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.ktRepository: 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.ktRepository: 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.
Summary
61feaceNative dependency
Privacy and security
Verification
./gradlew :app:testDevDebugUnitTest :app:lintDevDebug :app:ktlintCheck :app:verifyRoborazziDevDebugSummary by CodeRabbit