Skip to content

Commit f5de743

Browse files
committed
fix(chat-ui): cover redacted message text where blur is unavailable
Modifier.blur draws nothing below Android 12 and minSdk is 29, so on Android 10 and 11 the redacted excerpt would show in plain text. Cover it with a solid block there instead. Adds a Robolectric test that renders every card state to build/screenshots/message_link_card.png for comparison with the design.
1 parent dc1c8e8 commit f5de743

2 files changed

Lines changed: 130 additions & 5 deletions

File tree

‎apps/flipcash/shared/chat-ui/src/main/kotlin/com/flipcash/shared/chat/ui/MessageLinkCard.kt‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.ui.Alignment
2121
import androidx.compose.ui.Modifier
2222
import androidx.compose.ui.draw.BlurredEdgeTreatment
2323
import androidx.compose.ui.draw.blur
24+
import android.os.Build
2425
import androidx.compose.ui.draw.clip
2526
import androidx.compose.ui.graphics.Color
2627
import androidx.compose.ui.res.pluralStringResource
@@ -231,17 +232,20 @@ private fun Excerpt(excerpt: LinkCard.Message.Excerpt) {
231232
when (body) {
232233
is Body.Text -> Text(
233234
modifier = Modifier.then(
234-
if (body.redacted) {
235-
Modifier
235+
when {
236+
!body.redacted -> Modifier
237+
canBlur -> Modifier
236238
.blur(MessageLinkCardDefaults.REDACTED_BLUR, BlurredEdgeTreatment.Unbounded)
237239
.clearAndSetSemantics {}
238-
} else {
239-
Modifier
240+
// `blur` draws nothing below Android 12; cover the text with a solid block instead.
241+
else -> Modifier
242+
.background(CodeTheme.colors.textSecondary.copy(alpha = 0.2f), RoundedCornerShape(4.dp))
243+
.clearAndSetSemantics {}
240244
}
241245
),
242246
text = body.text,
243247
style = CodeTheme.typography.textMedium,
244-
color = CodeTheme.colors.textMain,
248+
color = if (body.redacted && !canBlur) Color.Transparent else CodeTheme.colors.textMain,
245249
maxLines = MessageLinkCardDefaults.BODY_LINES,
246250
overflow = TextOverflow.Ellipsis,
247251
)
@@ -434,3 +438,6 @@ private fun Preview_MessageLink_LockedDirect() = PreviewMessageCard(State.Locked
434438
private fun Preview_MessageLink_Unresolved() = PreviewMessageCard(State.Unresolved)
435439

436440
// endregion
441+
442+
/** `Modifier.blur` needs RenderEffect, which arrived in Android 12. */
443+
private val canBlur: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.flipcash.shared.chat.ui
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.Canvas
5+
import android.view.View
6+
import androidx.activity.ComponentActivity
7+
import androidx.compose.foundation.background
8+
import androidx.compose.foundation.layout.Arrangement
9+
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.width
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
16+
import androidx.compose.ui.unit.dp
17+
import com.flipcash.app.theme.FlipcashTheme
18+
import com.flipcash.services.models.chat.BlobAccessContext
19+
import com.flipcash.services.models.chat.ChatId
20+
import com.flipcash.shared.chat.models.LinkCard
21+
import com.flipcash.shared.chat.models.LinkCard.Message.Body
22+
import com.flipcash.shared.chat.models.LinkCard.Message.Excerpt
23+
import com.flipcash.shared.chat.models.LinkCard.Message.Kind
24+
import com.flipcash.shared.chat.models.LinkCard.Message.Source
25+
import com.flipcash.shared.chat.models.LinkCard.Message.State
26+
import com.getcode.theme.CodeTheme
27+
import org.junit.Rule
28+
import org.junit.Test
29+
import org.junit.runner.RunWith
30+
import org.robolectric.RobolectricTestRunner
31+
import org.robolectric.annotation.Config
32+
import org.robolectric.annotation.GraphicsMode
33+
import java.io.File
34+
import kotlin.time.Instant
35+
36+
/**
37+
* Renders every message card state to `build/screenshots/message_link_card.png`, to set beside the
38+
* card component set (Figma node 11396:118). Not an assertion test.
39+
*/
40+
@RunWith(RobolectricTestRunner::class)
41+
@GraphicsMode(GraphicsMode.Mode.NATIVE)
42+
@Config(qualifiers = "w402dp-h2200dp-xhdpi")
43+
class MessageLinkCardScreenshotTest {
44+
45+
@get:Rule
46+
val composeRule = createAndroidComposeRule<ComponentActivity>()
47+
48+
private val chatId = ChatId(List(16) { it.toByte() })
49+
private val sentAt = Instant.fromEpochSeconds(1_760_000_000)
50+
51+
private fun source(kind: Kind, title: String = "Moony") =
52+
Source(title = title, kind = kind, picture = null, pictureAccess = BlobAccessContext.ChatProfile(chatId))
53+
54+
private fun excerpt(body: Body, author: String = "Ada") =
55+
Excerpt(author = author, isFromSelf = false, sentAt = sentAt, body = body)
56+
57+
private val text =
58+
Body.Text("The curve flattens out past the first thousand holders, so early buys move the price most.")
59+
60+
private val states = listOf(
61+
State.Loading,
62+
State.Unresolved,
63+
State.Resolved(source(Kind.PublicGroup), excerpt(text), reactionCount = 12),
64+
State.Resolved(source(Kind.DirectMessage, "Ada"), excerpt(Body.Cash("$5.00", tipped = true)), reactionCount = 0),
65+
State.Gated(source(Kind.PublicGroup), excerpt(text), requirement = null),
66+
State.Gated(
67+
source(Kind.PublicGroup),
68+
excerpt(text.copy(redacted = true)),
69+
requirement = LinkCard.GroupInvite.Requirement(amount = "$10", currencyName = "Moony", staffOnly = false),
70+
),
71+
State.LockedGroup(source(Kind.PrivateGroup, "Inner Circle"), memberCount = 42, host = "Ada"),
72+
State.LockedDirect,
73+
)
74+
75+
@Test
76+
fun rendersEveryState() = render(states, "message_link_card.png")
77+
78+
private fun render(states: List<State>, fileName: String) {
79+
composeRule.mainClock.autoAdvance = false
80+
composeRule.setContent {
81+
FlipcashTheme {
82+
Column(
83+
modifier = Modifier
84+
.fillMaxSize()
85+
.background(CodeTheme.colors.background)
86+
.padding(20.dp),
87+
verticalArrangement = Arrangement.spacedBy(16.dp),
88+
) {
89+
states.forEach { state ->
90+
MessageLinkCard(
91+
card = LinkCard.Message(
92+
url = "https://app.flipcash.com/chat/x/m/42",
93+
start = 0,
94+
end = 10,
95+
chatId = chatId,
96+
messageId = 42,
97+
isGroup = true,
98+
state = state,
99+
),
100+
shape = RoundedCornerShape(12.dp),
101+
onOpen = {},
102+
modifier = Modifier.width(300.dp),
103+
)
104+
}
105+
}
106+
}
107+
}
108+
repeat(10) { composeRule.mainClock.advanceTimeByFrame() }
109+
110+
val root: View = composeRule.activity.findViewById(android.R.id.content)
111+
val bitmap = Bitmap.createBitmap(root.width, root.height, Bitmap.Config.ARGB_8888)
112+
root.draw(Canvas(bitmap))
113+
val outDir = File("build/screenshots").apply { mkdirs() }
114+
val file = File(outDir, fileName)
115+
file.outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
116+
println("SCREENSHOT_WRITTEN: ${file.absolutePath} (${bitmap.width}x${bitmap.height})")
117+
}
118+
}

0 commit comments

Comments
 (0)