Skip to content

Commit 3e0c7bc

Browse files
feat(design-system): add MessageConversationCounterBadge components (#10642)
2 parents 4b8faf6 + 976ff8a commit 3e0c7bc

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package net.thunderbird.feature.mail.message.list.ui.component.molecule
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.tooling.preview.PreviewLightDark
10+
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark
11+
import app.k9mail.core.ui.compose.designsystem.atom.text.TextDisplaySmall
12+
import app.k9mail.core.ui.compose.designsystem.atom.text.TextHeadlineSmall
13+
import app.k9mail.core.ui.compose.theme2.MainTheme
14+
15+
@PreviewLightDark
16+
@Composable
17+
private fun Preview() {
18+
PreviewWithThemesLightDark {
19+
Column(
20+
verticalArrangement = Arrangement.spacedBy(MainTheme.spacings.default),
21+
modifier = Modifier.padding(MainTheme.spacings.triple),
22+
) {
23+
TextDisplaySmall("Message Counter Preview:")
24+
TextHeadlineSmall("New Message: ")
25+
Row(horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default)) {
26+
NewMessageConversationCounterBadge(count = 1)
27+
NewMessageConversationCounterBadge(count = 7)
28+
NewMessageConversationCounterBadge(count = 10)
29+
NewMessageConversationCounterBadge(count = 25)
30+
NewMessageConversationCounterBadge(count = 100)
31+
}
32+
TextHeadlineSmall("Read Message: ")
33+
Row(horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default)) {
34+
ReadMessageConversationCounterBadge(count = 1)
35+
ReadMessageConversationCounterBadge(count = 7)
36+
ReadMessageConversationCounterBadge(count = 10)
37+
ReadMessageConversationCounterBadge(count = 25)
38+
ReadMessageConversationCounterBadge(count = 100)
39+
}
40+
TextHeadlineSmall("Unread Message: ")
41+
Row(horizontalArrangement = Arrangement.spacedBy(MainTheme.spacings.default)) {
42+
UnreadMessageConversationCounterBadge(count = 1)
43+
UnreadMessageConversationCounterBadge(count = 7)
44+
UnreadMessageConversationCounterBadge(count = 10)
45+
UnreadMessageConversationCounterBadge(count = 25)
46+
UnreadMessageConversationCounterBadge(count = 100)
47+
}
48+
}
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package net.thunderbird.feature.mail.message.list.ui.component.molecule
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.padding
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.graphics.Color
11+
import androidx.compose.ui.text.SpanStyle
12+
import androidx.compose.ui.text.buildAnnotatedString
13+
import androidx.compose.ui.text.font.FontWeight
14+
import androidx.compose.ui.text.withStyle
15+
import androidx.compose.ui.unit.dp
16+
import app.k9mail.core.ui.compose.designsystem.atom.text.TextLabelSmall
17+
import app.k9mail.core.ui.compose.theme2.MainTheme
18+
19+
internal const val MESSAGE_CONVERSATION_COUNTER_BADGE_PADDING = 3
20+
21+
/**
22+
* A composable that displays a message conversation counter badge with a customizable appearance.
23+
*
24+
* This component renders a counter within a styled surface container, displaying the count value
25+
* up to a specified limit. When the count exceeds the limit, it shows the limit value followed by
26+
* a "+" symbol (e.g., "99+").
27+
*
28+
* The counter uses a large shape from the theme and includes a 1dp border. The text is rendered
29+
* using a small label style with half-spacing padding.
30+
*
31+
* @param count The numeric value to display in the counter badge
32+
* @param color The color scheme defining the container, content, and border colors for the counter
33+
* @param modifier The modifier to be applied to the component
34+
* @param limit The maximum value to display before showing a "+" suffix; defaults to 99
35+
*/
36+
@Composable
37+
internal fun MessageConversationCounterBadge(
38+
count: Int,
39+
color: MessageConversationCounterBadgeColor,
40+
modifier: Modifier = Modifier,
41+
limit: Int = MessageConversationCounterBadgeDefaults.CONVERSATION_COUNTER_LIMIT,
42+
) {
43+
Box(
44+
modifier = modifier
45+
.background(
46+
color = color.containerColor,
47+
shape = MainTheme.shapes.large,
48+
)
49+
.border(
50+
width = 1.dp,
51+
color = color.borderColor ?: color.containerColor,
52+
shape = MainTheme.shapes.large,
53+
)
54+
.padding(horizontal = MainTheme.spacings.half, vertical = MESSAGE_CONVERSATION_COUNTER_BADGE_PADDING.dp),
55+
contentAlignment = Alignment.Center,
56+
) {
57+
TextLabelSmall(
58+
text = buildAnnotatedString {
59+
withStyle(SpanStyle(fontWeight = FontWeight.Normal)) {
60+
append(count.coerceAtMost(limit).toString())
61+
if (count > limit) {
62+
append("+")
63+
}
64+
}
65+
},
66+
color = color.contentColor,
67+
)
68+
}
69+
}
70+
71+
/**
72+
* A composable that displays a counter badge specifically styled for new messages in a conversation.
73+
*
74+
* This is a convenience wrapper around MessageConversationCounterBadge that applies the default
75+
* new message color scheme using primary theme colors. The counter displays the provided count
76+
* value and automatically adds a "+" suffix when the count exceeds the default limit.
77+
*
78+
* @param count The number of new messages to display in the badge
79+
* @param modifier The modifier to be applied to the component
80+
*/
81+
@Composable
82+
fun NewMessageConversationCounterBadge(count: Int, modifier: Modifier = Modifier) {
83+
MessageConversationCounterBadge(
84+
count = count,
85+
color = MessageConversationCounterBadgeDefaults.newMessageColor(),
86+
modifier = modifier,
87+
)
88+
}
89+
90+
/**
91+
* A composable that displays a counter badge specifically styled for read messages in a conversation.
92+
*
93+
* This is a convenience wrapper around MessageConversationCounterBadge that applies the default
94+
* read message color scheme using primary theme colors. The counter displays the provided count
95+
* value and automatically adds a "+" suffix when the count exceeds the default limit.
96+
*
97+
* @param count The number of read messages to display in the badge
98+
* @param modifier The modifier to be applied to the component
99+
*/
100+
@Composable
101+
fun ReadMessageConversationCounterBadge(count: Int, modifier: Modifier = Modifier) {
102+
MessageConversationCounterBadge(
103+
count = count,
104+
color = MessageConversationCounterBadgeDefaults.readMessageColor(),
105+
modifier = modifier,
106+
)
107+
}
108+
109+
/**
110+
* A composable that displays a counter badge specifically styled for unread messages in a conversation.
111+
*
112+
* This is a convenience wrapper around MessageConversationCounterBadge that applies the default
113+
* unread message color scheme using primary theme colors. The counter displays the provided count
114+
* value and automatically adds a "+" suffix when the count exceeds the default limit.
115+
*
116+
* @param count The number of unread messages to display in the badge
117+
* @param modifier The modifier to be applied to the component
118+
*/
119+
@Composable
120+
fun UnreadMessageConversationCounterBadge(count: Int, modifier: Modifier = Modifier) {
121+
MessageConversationCounterBadge(
122+
count = count,
123+
color = MessageConversationCounterBadgeDefaults.unreadMessageColor(),
124+
modifier = modifier,
125+
)
126+
}
127+
128+
/**
129+
* Provides default color configurations for message counter components.
130+
*
131+
* This object contains factory methods that create MessageConversationCounterBadgeColor instances with predefined
132+
* color schemes based on the application's theme. Each method returns a different color configuration
133+
* suitable for different message counter states.
134+
*/
135+
object MessageConversationCounterBadgeDefaults {
136+
const val CONVERSATION_COUNTER_LIMIT = 99
137+
138+
/**
139+
* Creates a [MessageConversationCounterBadgeColor] that represent a new message item counter.
140+
*
141+
* @param containerColor The container color of this [MessageConversationCounterBadge].
142+
* @param contentColor The content color of this [MessageConversationCounterBadge].
143+
* @param borderColor The border color of this [MessageConversationCounterBadge].
144+
*/
145+
@Composable
146+
fun newMessageColor(
147+
containerColor: Color = MainTheme.colors.primary,
148+
contentColor: Color = MainTheme.colors.onPrimary,
149+
borderColor: Color? = null,
150+
): MessageConversationCounterBadgeColor = MessageConversationCounterBadgeColor(
151+
contentColor = contentColor,
152+
containerColor = containerColor,
153+
borderColor = borderColor,
154+
)
155+
156+
/**
157+
* Creates a [MessageConversationCounterBadgeColor] that represent a read message item counter.
158+
*
159+
* @param containerColor The container color of this [MessageConversationCounterBadge].
160+
* @param contentColor The content color of this [MessageConversationCounterBadge].
161+
* @param borderColor The border color of this [MessageConversationCounterBadge].
162+
*/
163+
@Composable
164+
fun readMessageColor(
165+
containerColor: Color = MainTheme.colors.surfaceContainerLow,
166+
contentColor: Color = MainTheme.colors.onSurface,
167+
borderColor: Color? = MainTheme.colors.outline,
168+
): MessageConversationCounterBadgeColor = MessageConversationCounterBadgeColor(
169+
contentColor = contentColor,
170+
containerColor = containerColor,
171+
borderColor = borderColor,
172+
)
173+
174+
/**
175+
* Creates a [MessageConversationCounterBadgeColor] that represent a read message item counter.
176+
*
177+
* @param containerColor The container color of this [MessageConversationCounterBadge].
178+
* @param contentColor The content color of this [MessageConversationCounterBadge].
179+
* @param borderColor The border color of this [MessageConversationCounterBadge].
180+
*/
181+
@Composable
182+
fun unreadMessageColor(
183+
containerColor: Color = MainTheme.colors.inverseSurface,
184+
contentColor: Color = MainTheme.colors.inverseOnSurface,
185+
borderColor: Color? = null,
186+
): MessageConversationCounterBadgeColor = MessageConversationCounterBadgeColor(
187+
contentColor = contentColor,
188+
containerColor = containerColor,
189+
borderColor = borderColor,
190+
)
191+
}
192+
193+
/**
194+
* Defines the color scheme for a message counter component.
195+
*
196+
* This data class encapsulates the colors used to render a message counter,
197+
* including the text/content color, background color, and an optional border color.
198+
*
199+
* @property containerColor The color used for the counter's background
200+
* @property contentColor The color used for the counter's content (typically text or icons)
201+
* @property borderColor The optional color used for the counter's border; when `null`, no border is applied
202+
*/
203+
data class MessageConversationCounterBadgeColor(
204+
val containerColor: Color,
205+
val contentColor: Color,
206+
val borderColor: Color? = null,
207+
)

0 commit comments

Comments
 (0)