Skip to content

Commit e029cc6

Browse files
authored
Introduce contextual menu content & item (#6091)
* Introduce contextual menu * Extract contextual menu elevation to StreamTokens * Rename main composable to ContextualMenu
1 parent 020205f commit e029cc6

File tree

4 files changed

+192
-9
lines changed

4 files changed

+192
-9
lines changed

stream-chat-android-compose/api/stream-chat-android-compose.api

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,15 @@ public final class io/getstream/chat/android/compose/ui/components/channels/Unre
15581558
public static final fun UnreadCountIndicator-FNF3uiM (ILandroidx/compose/ui/Modifier;JLandroidx/compose/runtime/Composer;II)V
15591559
}
15601560

1561+
public final class io/getstream/chat/android/compose/ui/components/common/ComposableSingletons$ContextualMenuKt {
1562+
public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/common/ComposableSingletons$ContextualMenuKt;
1563+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
1564+
public static field lambda-2 Lkotlin/jvm/functions/Function2;
1565+
public fun <init> ()V
1566+
public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3;
1567+
public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2;
1568+
}
1569+
15611570
public final class io/getstream/chat/android/compose/ui/components/common/MenuOptionItemKt {
15621571
public static final fun MenuOptionItem-Y_kIC3U (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function3;Ljava/lang/String;JLandroidx/compose/ui/text/TextStyle;FLandroidx/compose/ui/Alignment$Vertical;Landroidx/compose/foundation/layout/Arrangement$Horizontal;Landroidx/compose/runtime/Composer;II)V
15631572
}
@@ -3574,8 +3583,8 @@ public final class io/getstream/chat/android/compose/ui/theme/ReactionOptionsThe
35743583
public final class io/getstream/chat/android/compose/ui/theme/StreamColors {
35753584
public static final field $stable I
35763585
public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamColors$Companion;
3577-
public synthetic fun <init> (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
3578-
public synthetic fun <init> (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
3586+
public synthetic fun <init> (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
3587+
public synthetic fun <init> (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V
35793588
public final fun component1-0d7_KjU ()J
35803589
public final fun component10-0d7_KjU ()J
35813590
public final fun component11-0d7_KjU ()J
@@ -3657,9 +3666,11 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors {
36573666
public final fun component80-0d7_KjU ()J
36583667
public final fun component81-0d7_KjU ()J
36593668
public final fun component82-0d7_KjU ()J
3669+
public final fun component83-0d7_KjU ()J
3670+
public final fun component84-0d7_KjU ()J
36603671
public final fun component9-0d7_KjU ()J
3661-
public final fun copy-yKHj9xE (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors;
3662-
public static synthetic fun copy-yKHj9xE$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors;
3672+
public final fun copy-Hw5wJPc (JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors;
3673+
public static synthetic fun copy-Hw5wJPc$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJIIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors;
36633674
public fun equals (Ljava/lang/Object;)Z
36643675
public final fun getAccentError-0d7_KjU ()J
36653676
public final fun getAccentNeutral-0d7_KjU ()J
@@ -3676,6 +3687,7 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors {
36763687
public final fun getAvatarPaletteText3-0d7_KjU ()J
36773688
public final fun getAvatarPaletteText4-0d7_KjU ()J
36783689
public final fun getAvatarPaletteText5-0d7_KjU ()J
3690+
public final fun getBackgroundElevationElevation2-0d7_KjU ()J
36793691
public final fun getBarsBackground-0d7_KjU ()J
36803692
public final fun getBorderCoreImage-0d7_KjU ()J
36813693
public final fun getBorderCoreOnDark-0d7_KjU ()J
@@ -3739,6 +3751,7 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors {
37393751
public final fun getTextHighEmphasisInverse-0d7_KjU ()J
37403752
public final fun getTextLowEmphasis-0d7_KjU ()J
37413753
public final fun getTextPrimary-0d7_KjU ()J
3754+
public final fun getTextSecondary-0d7_KjU ()J
37423755
public final fun getThreadSeparatorGradientEnd-0d7_KjU ()J
37433756
public final fun getThreadSeparatorGradientStart-0d7_KjU ()J
37443757
public final fun getVideoBackgroundMediaGalleryPicker-0d7_KjU ()J
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.compose.ui.components.common
18+
19+
import androidx.compose.foundation.BorderStroke
20+
import androidx.compose.foundation.layout.Arrangement
21+
import androidx.compose.foundation.layout.Column
22+
import androidx.compose.foundation.layout.ColumnScope
23+
import androidx.compose.foundation.layout.IntrinsicSize
24+
import androidx.compose.foundation.layout.Row
25+
import androidx.compose.foundation.layout.defaultMinSize
26+
import androidx.compose.foundation.layout.fillMaxWidth
27+
import androidx.compose.foundation.layout.padding
28+
import androidx.compose.foundation.layout.size
29+
import androidx.compose.foundation.layout.width
30+
import androidx.compose.foundation.shape.RoundedCornerShape
31+
import androidx.compose.material3.HorizontalDivider
32+
import androidx.compose.material3.Icon
33+
import androidx.compose.material3.Surface
34+
import androidx.compose.material3.Text
35+
import androidx.compose.runtime.Composable
36+
import androidx.compose.ui.Alignment
37+
import androidx.compose.ui.Modifier
38+
import androidx.compose.ui.draw.clip
39+
import androidx.compose.ui.graphics.painter.Painter
40+
import androidx.compose.ui.res.painterResource
41+
import androidx.compose.ui.tooling.preview.Preview
42+
import androidx.compose.ui.unit.dp
43+
import io.getstream.chat.android.compose.R
44+
import io.getstream.chat.android.compose.ui.theme.ChatTheme
45+
import io.getstream.chat.android.compose.ui.theme.StreamTokens
46+
import io.getstream.chat.android.compose.ui.util.clickable
47+
48+
@Composable
49+
internal fun ContextualMenu(
50+
modifier: Modifier = Modifier,
51+
content: @Composable ColumnScope.() -> Unit,
52+
) {
53+
val colors = ChatTheme.colors
54+
55+
Surface(
56+
modifier = modifier,
57+
shape = RoundedCornerShape(StreamTokens.radiusLg),
58+
color = colors.backgroundElevationElevation2,
59+
shadowElevation = StreamTokens.elevation3,
60+
border = BorderStroke(StreamTokens.borderStrokeSubtle, colors.borderCoreSurfaceSubtle),
61+
) {
62+
Column(content = content)
63+
}
64+
}
65+
66+
@Composable
67+
internal fun ContextualMenuItem(
68+
label: String,
69+
leadingIcon: Painter? = null,
70+
trailingIcon: Painter? = null,
71+
destructive: Boolean = false,
72+
enabled: Boolean = true,
73+
onClick: () -> Unit,
74+
) {
75+
val colors = ChatTheme.colors
76+
77+
Row(
78+
modifier = Modifier
79+
.defaultMinSize(minWidth = 250.dp, minHeight = 40.dp)
80+
.width(IntrinsicSize.Min)
81+
.clip(RoundedCornerShape(StreamTokens.radiusMd))
82+
.clickable(enabled = enabled, onClick = onClick)
83+
.padding(horizontal = StreamTokens.spacingSm),
84+
verticalAlignment = Alignment.CenterVertically,
85+
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs),
86+
) {
87+
val (textColor, iconColor) = when {
88+
!enabled -> colors.stateTextDisabled to colors.stateTextDisabled
89+
destructive -> colors.accentError to colors.accentError
90+
else -> colors.textPrimary to colors.textSecondary
91+
}
92+
93+
leadingIcon?.let {
94+
Icon(
95+
painter = it,
96+
contentDescription = null,
97+
tint = iconColor,
98+
modifier = Modifier.size(20.dp),
99+
)
100+
}
101+
102+
Text(
103+
text = label,
104+
color = textColor,
105+
modifier = Modifier.weight(1f),
106+
)
107+
108+
trailingIcon?.let {
109+
Icon(
110+
painter = it,
111+
contentDescription = null,
112+
tint = iconColor,
113+
modifier = Modifier.size(20.dp),
114+
)
115+
}
116+
}
117+
}
118+
119+
@Composable
120+
internal fun ContextualMenuDivider(modifier: Modifier = Modifier) {
121+
HorizontalDivider(
122+
modifier = modifier
123+
.fillMaxWidth(1f)
124+
.padding(vertical = StreamTokens.spacing2xs),
125+
color = ChatTheme.colors.borderCoreSurfaceSubtle,
126+
)
127+
}
128+
129+
@Preview(showBackground = true)
130+
@Composable
131+
private fun ContextualMenuPreview() {
132+
ChatTheme {
133+
ContextualMenu(
134+
Modifier
135+
.padding(32.dp)
136+
.width(IntrinsicSize.Min),
137+
) {
138+
MenuItemPreview(enabled = true, destructive = false)
139+
MenuItemPreview(enabled = false, destructive = false)
140+
MenuItemPreview(enabled = false, destructive = true)
141+
ContextualMenuDivider()
142+
MenuItemPreview(enabled = true, destructive = true)
143+
}
144+
}
145+
}
146+
147+
@Composable
148+
private fun MenuItemPreview(enabled: Boolean, destructive: Boolean) {
149+
ContextualMenuItem(
150+
label = "{{ label }}",
151+
destructive = destructive,
152+
enabled = enabled,
153+
leadingIcon = painterResource(R.drawable.stream_compose_ic_copy),
154+
trailingIcon = painterResource(R.drawable.stream_compose_ic_checkmark),
155+
) {}
156+
}

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ import io.getstream.chat.android.compose.R
6161
* given it contains more than can be displayed in the message list media attachment preview.
6262
* @param showMoreCountText The color of the text displaying how many more media attachments the message contains,
6363
* given it contains more than can be displayed in the message list media attachment preview.
64-
* @param ownMessageQuotedBackground Changes the background color of the quoted message contained in a reply sent by the current user.
65-
* @param otherMessageQuotedBackground Changes the background color of the quoted message contained in a reply sent by other users.
66-
* @param ownMessageQuotedText Changes the text color of the quoted message contained in a reply sent by the current user. [textHighEmphasis] by default.
67-
* @param otherMessageQuotedText Changes the text color of the quoted message contained in a reply sent by other users. [textHighEmphasis] by default.
6864
* @param accentError Used for destructive actions and error states.
6965
* @param accentNeutral Used for neutral accent for low-priority badges.
7066
* @param accentSuccess Used for success states and positive actions.
@@ -84,8 +80,10 @@ import io.getstream.chat.android.compose.R
8480
* @param borderCoreSurfaceSubtle Used for very light separators.
8581
* @param borderCorePrimary Used for selected or active state border.
8682
* @param textPrimary Used for main text color.
83+
* @param textSecondary Used for secondary text color with lower emphasis.
8784
* @param stateBgDisabled Used for disabled background for inputs, buttons, or chips.
8885
* @param stateTextDisabled Used for disabled text and icon color.
86+
* @param backgroundElevationElevation2 Used for elevated surface backgrounds at elevation level 2.
8987
* @param buttonStyleGhostBg Used for ghost button background.
9088
* @param buttonStyleGhostBorder Used for ghost button border.
9189
* @param buttonStyleGhostTextPrimary Used for primary ghost button text.
@@ -105,6 +103,11 @@ import io.getstream.chat.android.compose.R
105103
* @param buttonTypeSecondaryTextDisabled Used for disabled secondary button text.
106104
* @param chatBgIncoming Used for incoming message bubble background.
107105
* @param chatBgOutgoing Used for outgoing message bubble background.
106+
* @param chatBgAttachmentIncoming Used for incoming message attachment background.
107+
* @param chatBgAttachmentOutgoing Used for outgoing message attachment background.
108+
* @param chatReplyIndicatorIncoming Used for the reply indicator color in incoming messages.
109+
* @param chatReplyIndicatorOutgoing Used for the reply indicator color in outgoing messages.
110+
* @param chatTextMessage Used for message text color in chat bubbles.
108111
* @param controlRemoveBg Used for remove control background.
109112
* @param controlRemoveBorder Used for remove control border.
110113
* @param controlRemoveIcon Used for remove control icon.
@@ -173,9 +176,10 @@ public data class StreamColors(
173176
public val borderCoreSurfaceSubtle: Color,
174177
public val borderCorePrimary: Color,
175178
public val textPrimary: Color,
179+
public val textSecondary: Color,
176180
public val stateBgDisabled: Color,
177181
public val stateTextDisabled: Color,
178-
182+
public val backgroundElevationElevation2: Color,
179183
public val buttonStyleGhostBg: Color,
180184
public val buttonStyleGhostBorder: Color,
181185
public val buttonStyleGhostTextPrimary: Color = accentPrimary,
@@ -253,11 +257,13 @@ public data class StreamColors(
253257
accentNeutral = StreamPrimitiveColors.slate500,
254258
accentSuccess = StreamPrimitiveColors.green500,
255259
accentPrimary = StreamPrimitiveColors.blue500,
260+
backgroundElevationElevation2 = StreamPrimitiveColors.baseWhite,
256261
borderCoreImage = StreamPrimitiveColors.baseBlack.copy(alpha = .1f),
257262
borderCoreOnDark = StreamPrimitiveColors.baseWhite,
258263
borderCoreSurfaceSubtle = StreamPrimitiveColors.slate200,
259264
borderCorePrimary = StreamPrimitiveColors.blue600,
260265
textPrimary = StreamPrimitiveColors.slate900,
266+
textSecondary = StreamPrimitiveColors.slate700,
261267
stateBgDisabled = StreamPrimitiveColors.slate200,
262268
stateTextDisabled = StreamPrimitiveColors.slate400,
263269
buttonStyleGhostBg = StreamPrimitiveColors.baseTransparent,
@@ -331,11 +337,13 @@ public data class StreamColors(
331337
accentNeutral = StreamPrimitiveColors.neutral500,
332338
accentSuccess = StreamPrimitiveColors.green400,
333339
accentPrimary = StreamPrimitiveColors.blue400,
340+
backgroundElevationElevation2 = StreamPrimitiveColors.neutral800,
334341
borderCoreImage = StreamPrimitiveColors.baseWhite.copy(alpha = .2f),
335342
borderCoreOnDark = StreamPrimitiveColors.baseWhite,
336343
borderCoreSurfaceSubtle = StreamPrimitiveColors.neutral700,
337344
borderCorePrimary = StreamPrimitiveColors.blue300,
338345
textPrimary = StreamPrimitiveColors.neutral50,
346+
textSecondary = StreamPrimitiveColors.neutral300,
339347
stateBgDisabled = StreamPrimitiveColors.slate800,
340348
stateTextDisabled = StreamPrimitiveColors.slate600,
341349
buttonStyleGhostBg = StreamPrimitiveColors.baseTransparent,
@@ -386,6 +394,7 @@ internal object StreamPrimitiveColors {
386394
val green500 = Color(0xFF00E2A1)
387395
val green800 = Color(0xFF006548)
388396
val neutral50 = Color(0xFFF7F7F7)
397+
val neutral300 = Color(0xFFC1C1C1)
389398
val neutral500 = Color(0xFF7F7F7F)
390399
val neutral700 = Color(0xFF4A4A4A)
391400
val neutral800 = Color(0xFF383838)
@@ -399,6 +408,7 @@ internal object StreamPrimitiveColors {
399408
val slate400 = Color(0xFFB8BEC4)
400409
val slate500 = Color(0xFF9EA4AA)
401410
val slate600 = Color(0xFF838990)
411+
val slate700 = Color(0xFF4A4A4A)
402412
val slate800 = Color(0xFF50565D)
403413
val slate900 = Color(0xFF1E252B)
404414
val yellow100 = Color(0xFFFFF1C2)

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamTokens.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import androidx.compose.ui.unit.dp
2222
import androidx.compose.ui.unit.sp
2323

2424
internal object StreamTokens {
25+
val borderStrokeSubtle = 1.2.dp
26+
27+
val elevation3 = 4.dp
28+
2529
val radius2xs = CornerSize(2.dp)
2630
val radiusXs = CornerSize(4.dp)
2731
val radiusSm = CornerSize(6.dp)

0 commit comments

Comments
 (0)