WeChat (iOS) — Jetpack Compose Implementation Guide
Companion to DESIGN.md — the framework-neutral spec. This file ports WeChat’s visual language to Android with Jetpack Compose (Material 3): a color token object, a Typography set, paste-ready @Composables, Modifiers, motion, and haptics.
Why a Compose guide for an iOS-referenced app? The DESIGN.md tokens are platform-neutral. This file keeps the visual identity (WeChat’s gray system, the super-app Discover grouped-list hub, pale-green tailed bubbles, rounded-square avatars, the red-packet card, the universal red dot) while making everything idiomatic Android —
NavigationBarinstead of a UITabBar, a custom tailed bubbleShape,sp/dpinstead ofpt. (WeChat’s real Android app is itself this UI; this maps the iOS spec onto modern Compose.)
Assumes Compose BOM 2024.09+, Kotlin 2.0+, minSdk 24, Material 3, and Coil 2.6+ for avatars.
1. Color Tokens
// ui/theme/WeChatColors.kt
import androidx.compose.ui.graphics.Color
object WeChatColors {
// Backdrop & canvas
val Backdrop = Color(0xFFEDEDED) // chat + grouped-list base
val Canvas = Color(0xFFFFFFFF)
val Surface = Color(0xFFF7F7F7)
val Divider = Color(0xFFD9D9D9)
val PressedRow = Color(0xFFECECEC)
// Text
val TextPrimary = Color(0xFF181818)
val TextSecondary = Color(0xFF888888)
val TextTertiary = Color(0xFFB2B2B2)
// Brand
val Green = Color(0xFF07C160)
val GreenPressed = Color(0xFF06A050)
// Message
val Incoming = Color(0xFFFFFFFF)
val Outgoing = Color(0xFF95EC69) // pale chat-green — NOT the brand green
// Semantic
val RedDot = Color(0xFFFA5151)
val LinkBlue = Color(0xFF576B95)
val Gold = Color(0xFFFBE3B3)
// Discover icon chips (data-driven)
val Moments = Color(0xFF3F7DD5)
val Scan = Color(0xFF2EA0F8)
val Channels = Color(0xFFFA9D3B)
val Mini = Color(0xFF3CC51F)
val Search = Color(0xFF5C7CFA)
val Nearby = Color(0xFFF76F34)
val TopStories = Color(0xFFEB6F6F)
// Dark (system-dark users)
val DarkCanvas = Color(0xFF1A1A1A)
val DarkBackdrop = Color(0xFF111111)
val DarkIncoming = Color(0xFF2C2C2C)
val DarkOutgoing = Color(0xFF3EB575)
val DarkTextPrimary = Color(0xFFE8E8E8)
val DarkTextSecondary = Color(0xFF9A9A9A)
}
Wire it into Material 3 schemes so ripples, dividers, and stock component colors inherit the brand. WeChat is light-first; provide a dark scheme for system-dark users.
// ui/theme/Theme.kt
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.foundation.isSystemInDarkTheme
private val WeChatLight = lightColorScheme(
primary = WeChatColors.Green,
onPrimary = Color.White,
background = WeChatColors.Backdrop,
onBackground = WeChatColors.TextPrimary,
surface = WeChatColors.Canvas,
onSurface = WeChatColors.TextPrimary,
surfaceVariant = WeChatColors.Surface,
outline = WeChatColors.Divider,
error = WeChatColors.RedDot,
)
private val WeChatDark = darkColorScheme(
primary = WeChatColors.Green,
onPrimary = Color.White,
background = WeChatColors.DarkBackdrop,
onBackground = WeChatColors.DarkTextPrimary,
surface = WeChatColors.DarkCanvas,
onSurface = WeChatColors.DarkTextPrimary,
surfaceVariant = WeChatColors.DarkCanvas,
outline = Color(0xFF333333),
error = WeChatColors.RedDot,
)
@Composable
fun WeChatTheme(content: @Composable () -> Unit) =
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) WeChatDark else WeChatLight,
typography = WeChatTypography,
content = content,
)
2. Typography
WeChat uses PingFang SC (Apple CJK face — not on Android) / Noto Sans CJK at weights 400/600. On Android, drop Noto Sans CJK in res/font/.
// ui/theme/WeChatType.kt
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Noto Sans CJK is the Android stand-in for PingFang SC
val WeChatSans = FontFamily(
Font(R.font.noto_sans_cjk_regular, FontWeight.Normal), // 400
Font(R.font.noto_sans_cjk_medium, FontWeight.SemiBold), // 600 (Noto "Medium")
)
// Named ramp — mirrors DESIGN.md §3 exactly (pt → sp 1:1)
object WeChatText {
val NavTitle = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 22.sp)
val ContactName = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 17.sp, lineHeight = 22.sp)
val RowTitle = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 17.sp, lineHeight = 22.sp)
val MessageBody = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 17.sp, lineHeight = 24.sp) // CJK-friendly 1.4
val Section = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 17.sp)
val Preview = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 18.sp)
val Timestamp = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 16.sp)
val TimeSep = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 14.sp)
val Button = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 17.sp, lineHeight = 17.sp)
val ButtonSec = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 16.sp)
val Tab = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 10.sp, lineHeight = 12.sp)
val Badge = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 11.sp, lineHeight = 11.sp)
val MomentsName = TextStyle(WeChatSans, fontWeight = FontWeight.SemiBold, fontSize = 15.sp, lineHeight = 20.sp)
val Caption = TextStyle(WeChatSans, fontWeight = FontWeight.Normal, fontSize = 13.sp, lineHeight = 17.sp)
}
// Map onto Material 3 slots so stock components inherit the brand
val WeChatTypography = Typography(
titleLarge = WeChatText.NavTitle,
bodyLarge = WeChatText.MessageBody,
titleMedium = WeChatText.ContactName,
bodyMedium = WeChatText.Preview,
labelSmall = WeChatText.Tab,
)
3. Signature Components
Discover Grouped-List Hub (Signature — the super-app)
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun DiscoverRow(
icon: ImageVector,
chipColor: Color,
title: String,
redDot: Boolean = false,
) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.background(if (pressed) WeChatColors.PressedRow else WeChatColors.Canvas)
.clickable(interaction, indication = null) { }
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(
Modifier.size(28.dp).clip(RoundedCornerShape(8.dp)).background(chipColor),
contentAlignment = Alignment.Center,
) { Icon(icon, contentDescription = null, tint = Color.White, modifier = Modifier.size(15.dp)) }
Text(title, style = WeChatText.RowTitle, color = WeChatColors.TextPrimary)
Spacer(Modifier.weight(1f))
if (redDot) Box(Modifier.size(8.dp).clip(RoundedCornerShape(4.dp)).background(WeChatColors.RedDot))
Spacer(Modifier.width(8.dp))
Icon(Icons.Filled.ChevronRight, contentDescription = null,
tint = WeChatColors.TextTertiary, modifier = Modifier.size(18.dp))
}
}
@Composable
fun DiscoverScreen() {
LazyColumn(
Modifier.fillMaxSize().background(WeChatColors.Backdrop),
contentPadding = PaddingValues(vertical = 12.dp),
) {
item { Cluster { DiscoverRow(Icons.Filled.PhotoLibrary, WeChatColors.Moments, "Moments", redDot = true) } }
item {
Cluster {
DiscoverRow(Icons.Filled.PlayCircle, WeChatColors.Channels, "Channels")
RowDivider()
DiscoverRow(Icons.Filled.QrCodeScanner, WeChatColors.Scan, "Scan")
RowDivider()
DiscoverRow(Icons.Filled.Article, WeChatColors.TopStories, "Top Stories")
}
}
item {
Cluster {
DiscoverRow(Icons.Filled.Search, WeChatColors.Search, "Search")
RowDivider()
DiscoverRow(Icons.Filled.LocationOn, WeChatColors.Nearby, "People Nearby")
}
}
item { Cluster { DiscoverRow(Icons.Filled.GridView, WeChatColors.Mini, "Mini Programs") } }
}
}
@Composable
private fun Cluster(content: @Composable ColumnScope.() -> Unit) {
Column(
Modifier.fillMaxWidth().background(WeChatColors.Canvas).padding(bottom = 0.dp),
content = content,
)
Spacer(Modifier.height(12.dp)) // gray gutter between clusters
}
@Composable
private fun RowDivider() =
HorizontalDivider(Modifier.padding(start = 56.dp), color = WeChatColors.Divider)
Tailed Message Bubble + Rounded-Square Avatar (Signature)
import androidx.compose.foundation.shape.GenericShape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.LayoutDirection
@Composable
fun WCBubble(text: String, isOutgoing: Boolean) {
Row(
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = if (isOutgoing) Arrangement.End else Arrangement.Start,
verticalAlignment = Alignment.Top,
) {
if (!isOutgoing) { Avatar(); Spacer(Modifier.width(8.dp)) }
Text(
text,
style = WeChatText.MessageBody,
color = WeChatColors.TextPrimary,
modifier = Modifier
.widthIn(max = 270.dp)
.clip(bubbleTailShape(isOutgoing))
.background(if (isOutgoing) WeChatColors.Outgoing else WeChatColors.Incoming)
.padding(
start = if (isOutgoing) 12.dp else 16.dp, // extra room for the leading tail
end = if (isOutgoing) 16.dp else 12.dp,
top = 9.dp, bottom = 9.dp,
),
)
if (isOutgoing) { Spacer(Modifier.width(8.dp)); Avatar() }
}
}
// Rounded-SQUARE avatar — WeChat's constant brand tell (never a circle)
@Composable
private fun Avatar() =
Box(Modifier.size(40.dp).clip(RoundedCornerShape(8.dp)).background(WeChatColors.Surface))
// 6dp body radius + a small solid triangular tail toward the sender's avatar
private fun bubbleTailShape(isOutgoing: Boolean) = GenericShape { size, _ ->
val r = 6f; val tail = 12f; val ty = 22f
if (isOutgoing) {
addRoundRect(androidx.compose.ui.geometry.RoundRect(
0f, 0f, size.width - tail, size.height,
androidx.compose.ui.geometry.CornerRadius(r)))
moveTo(size.width - tail, ty - 6f)
lineTo(size.width, ty)
lineTo(size.width - tail, ty + 6f)
close()
} else {
addRoundRect(androidx.compose.ui.geometry.RoundRect(
tail, 0f, size.width, size.height,
androidx.compose.ui.geometry.CornerRadius(r)))
moveTo(tail, ty - 6f)
lineTo(0f, ty)
lineTo(tail, ty + 6f)
close()
}
}
Chat List Row (rounded-square avatar + red badge)
@Composable
fun ChatRow(name: String, preview: String, time: String, unread: Int) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Row(
Modifier
.fillMaxWidth()
.height(64.dp)
.background(if (pressed) WeChatColors.PressedRow else WeChatColors.Canvas)
.clickable(interaction, indication = null) { }
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(Modifier.size(48.dp).clip(RoundedCornerShape(8.dp)).background(WeChatColors.Surface)) // rounded square
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(name, style = WeChatText.ContactName, color = WeChatColors.TextPrimary)
Text(preview, style = WeChatText.Preview, color = WeChatColors.TextSecondary, maxLines = 1)
}
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(time, style = WeChatText.Timestamp, color = WeChatColors.TextSecondary)
if (unread > 0) {
Box(
Modifier.defaultMinSize(18.dp, 18.dp).clip(RoundedCornerShape(9.dp))
.background(WeChatColors.RedDot).padding(horizontal = 5.dp),
contentAlignment = Alignment.Center,
) { Text("$unread", style = WeChatText.Badge, color = Color.White) }
}
}
}
}
Red Packet (Hóngbāo) Card (Signature)
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.filled.MailOutline
import androidx.compose.material.icons.filled.Paid
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
@Composable
fun RedPacketCard(sender: String) {
var opened by remember { mutableStateOf(false) }
val flip by animateFloatAsState(if (opened) 360f else 0f, tween(600), label = "seal")
Column(
Modifier
.width(260.dp)
.shadow(4.dp, RoundedCornerShape(8.dp), spotColor = Color.Black.copy(alpha = 0.12f))
.clip(RoundedCornerShape(8.dp))
.background(Brush.verticalGradient(listOf(WeChatColors.RedDot, Color(0xFFC72E29))))
.clickable { opened = true }
.padding(vertical = 22.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Box(
Modifier.size(44.dp).clip(CircleShape).background(WeChatColors.Gold)
.graphicsLayer { rotationY = flip },
contentAlignment = Alignment.Center,
) {
Icon(if (opened) Icons.Filled.MailOutline else Icons.Filled.Paid,
contentDescription = "Red packet from $sender, open",
tint = Color(0xFFC72E29), modifier = Modifier.size(22.dp))
}
Text("Best wishes!", style = WeChatText.Button, color = Color.White)
Text("From $sender", style = WeChatText.Caption, color = Color.White.copy(alpha = 0.85f))
}
}
Composer
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.GraphicEq
import androidx.compose.material.icons.outlined.EmojiEmotions
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
@Composable
fun ComposerBar() {
var text by remember { mutableStateOf("") }
val hasText = text.trim().isNotEmpty()
val haptics = LocalHapticFeedback.current
Row(
Modifier.fillMaxWidth().background(WeChatColors.Backdrop).padding(8.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(Icons.Filled.GraphicEq, contentDescription = "Voice",
tint = WeChatColors.TextPrimary, modifier = Modifier.size(26.dp))
BasicTextField(
value = text, onValueChange = { text = it },
textStyle = WeChatText.MessageBody.copy(color = WeChatColors.TextPrimary),
modifier = Modifier
.weight(1f)
.heightIn(min = 36.dp)
.clip(RoundedCornerShape(6.dp))
.background(WeChatColors.Canvas)
.padding(horizontal = 10.dp, vertical = 8.dp),
)
Icon(Icons.Outlined.EmojiEmotions, contentDescription = "Stickers",
tint = WeChatColors.TextPrimary, modifier = Modifier.size(26.dp))
if (hasText) {
Box(
Modifier
.clip(RoundedCornerShape(4.dp))
.background(WeChatColors.Green)
.clickable {
haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
text = ""
}
.padding(horizontal = 14.dp, vertical = 8.dp),
) { Text("Send", style = WeChatText.ButtonSec, color = Color.White) }
} else {
Icon(Icons.Filled.Add, contentDescription = "More",
tint = WeChatColors.TextPrimary, modifier = Modifier.size(26.dp))
}
}
}
4. Time Separator
@Composable
fun TimeSeparator(label: String) {
Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {
Text(label, style = WeChatText.TimeSep, color = WeChatColors.TextSecondary)
}
}
5. Bottom Navigation
Material 3 NavigationBar (Chats / Contacts / Discover / Me). Android has no live blur; use a very-light #F7F7F7 surface with a hairline. Active tint is WeChat Green.
import androidx.compose.material3.*
import androidx.compose.material.icons.filled.*
@Composable
fun WeChatBottomBar(selected: Int, onSelect: (Int) -> Unit) {
NavigationBar(containerColor = WeChatColors.Surface, tonalElevation = 0.dp) {
val items = listOf(
"Chats" to Icons.Filled.Chat,
"Contacts" to Icons.Filled.Contacts,
"Discover" to Icons.Filled.Explore,
"Me" to Icons.Filled.Person,
)
items.forEachIndexed { i, (label, icon) ->
NavigationBarItem(
selected = selected == i,
onClick = { onSelect(i) },
icon = { Icon(icon, contentDescription = label, modifier = Modifier.size(26.dp)) },
label = { Text(label, style = WeChatText.Tab) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = WeChatColors.Green,
selectedTextColor = WeChatColors.Green,
unselectedIconColor = Color(0xFF999999),
unselectedTextColor = Color(0xFF999999),
indicatorColor = Color.Transparent, // WeChat has no Material pill
),
)
}
}
}
6. Motion
| Moment | Compose recipe |
|---|---|
| Red-packet open (the one flourish) | animateFloatAsState rotationY 0 → 360 over tween(600) |
| Send (text) | AnimatedVisibility + fadeIn() + scaleIn(initialScale = 0.98f), 150ms |
| Press states | collectIsPressedAsState toggling row/button background, ~120ms |
| Attachment grid present | AnimatedVisibility + slideInVertically { it }, 260ms; “+” rotate to 45° |
| Tab switch | none (instant) |
| Red dot | render/unrender with no animation — authoritative, not playful |
// Attachment "+" → "×" rotation
val rot by animateFloatAsState(if (gridOpen) 45f else 0f, tween(260), label = "plusRot")
Icon(Icons.Filled.Add, contentDescription = "More",
modifier = Modifier.size(26.dp).graphicsLayer { rotationZ = rot },
tint = WeChatColors.TextPrimary)
Haptics: prefer LocalHapticFeedback. For the send tap use LocalView.current.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) (API 30+) to approximate iOS’s light impact. Keep haptics sparse — WeChat’s restraint extends to feedback.
7. Icons
The closest first-party set is androidx.compose.material:material-icons-extended. Export WeChat’s exact Discover glyphs as vector drawables for parity if needed.
| Purpose | SF Symbol (iOS) | Material Icon (Compose) |
|---|---|---|
| Voice | waveform.circle |
Icons.Filled.GraphicEq |
| Sticker | face.smiling |
Icons.Outlined.EmojiEmotions |
| Plus / attachment | plus.circle |
Icons.Filled.Add |
| Moments (Discover) | photo.on.rectangle |
Icons.Filled.PhotoLibrary |
| Scan (Discover) | qrcode.viewfinder |
Icons.Filled.QrCodeScanner |
| Channels (Discover) | play.rectangle.fill |
Icons.Filled.PlayCircle |
| Mini Programs (Discover) | square.grid.2x2.fill |
Icons.Filled.GridView |
| Search (Discover) | magnifyingglass |
Icons.Filled.Search |
| Nearby (Discover) | location.fill |
Icons.Filled.LocationOn |
| Chevron (rows) | chevron.right |
Icons.Filled.ChevronRight |
| Red packet | yensign.circle.fill / envelope.open.fill |
Icons.Filled.Paid / Icons.Filled.MailOutline |
| Chats (tab) | bubble.left.fill |
Icons.Filled.Chat |
| Contacts (tab) | person.crop.square.fill |
Icons.Filled.Contacts |
| Discover (tab) | safari.fill |
Icons.Filled.Explore |
| Me (tab) | person.fill |
Icons.Filled.Person |
8. Minimum SDK & Accessibility Notes
- minSdk 24 (Compose floor is 21; comfortable at 24).
compileSdk 34+, Compose BOM2024.09+, Kotlin2.0+. - Edge-to-edge: call
enableEdgeToEdge(); the gray/white system wants dark system-bar icons. ApplyScaffoldinsets so the composer clears gesture nav and rises with the IME (Modifier.imePadding()). - Discover is a grouped list, not a dashboard — white rows on the
#EDEDEDbase, 12dp gray gutters between clusters, hairline dividers within. This screen is the identity. - Rounded-square avatars: every avatar is
RoundedCornerShape(8.dp)on a square — neverCircleShape. A constant brand tell. - CJK: PingFang SC is iOS-only; on Android bundle Noto Sans CJK in
res/font/and keeplineHeightgenerous (≈1.4 on 17sp body) so dense CJK isn’t cramped. - Font scaling:
sphonors the user’s font scale — keep it on names, rows, and bodies (they share the 17sp base). Pin layout-sensitive text (tab labels, badge counts, time separators) via a fixed-density wrapper. - TalkBack: announce the Discover row’s red dot as “has new updates”; each row is one button. Label the red-packet seal “Red packet from <sender>, double-tap to open”. Merge a chat row’s name + preview with
Modifier.semantics(mergeDescendants = true). - Touch targets: Material guidance is 48.dp minimum. Give the compact “Send” button and composer glyphs a 48.dp effective hit via padding; Discover rows are a full 56.dp and chat rows 64.dp.
- Contrast:
#181818on#95EC69,#FFFFFF, and#EDEDEDpasses WCAG AAA;#888888on white passes AA at 14sp+ — validate#888888time separators on the#EDEDEDbackdrop. - Reduce motion: gate the red-packet
rotationYbehind the system animator scale and crossfade to the opened state; the red dot must never animate. - Dynamic color: do not enable Material You
dynamicColorScheme()— WeChat Green and the pale-green bubble are fixed brand colors regardless of wallpaper.