Browse Source

Redesign chat example (#3161)

* Redesign chat example

* Minor typography improvements

* Code cleanup

* Update examples/chat/iosApp/iosApp/iosApp.swift

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>

* Use JPG instead of PNG

* Turn background in to a JPG
Add raw PXD file.

* Remove TEAM_ID

* Move to sp sizes

* Make Android text consistent with iOS variant

* Prefer light color scheme to prevent unwanted text color switch

* Remove PXD

---------

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>
pull/3193/head
Sebastian Aigner 2 years ago committed by GitHub
parent
commit
dab4531fe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      examples/chat/iosApp/iosApp.xcodeproj/project.pbxproj
  2. 11
      examples/chat/iosApp/iosApp/ComposeInsideSwiftUIScreen.swift
  3. 1
      examples/chat/iosApp/iosApp/GradientTemplate.swift
  4. 12
      examples/chat/iosApp/iosApp/iosApp.swift
  5. 65
      examples/chat/shared/src/commonMain/kotlin/ChatApp.kt
  6. 143
      examples/chat/shared/src/commonMain/kotlin/ChatMessage.kt
  7. 5
      examples/chat/shared/src/commonMain/kotlin/Colors.kt
  8. 27
      examples/chat/shared/src/commonMain/kotlin/Data.kt
  9. 116
      examples/chat/shared/src/commonMain/kotlin/Messages.kt
  10. 9
      examples/chat/shared/src/commonMain/kotlin/SendMessage.kt
  11. BIN
      examples/chat/shared/src/commonMain/resources/background.jpg
  12. BIN
      examples/chat/shared/src/commonMain/resources/stock1.jpg
  13. BIN
      examples/chat/shared/src/commonMain/resources/stock2.jpg
  14. BIN
      examples/chat/shared/src/commonMain/resources/stock3.jpg
  15. BIN
      examples/chat/shared/src/commonMain/resources/stock4.jpg

26
examples/chat/iosApp/iosApp.xcodeproj/project.pbxproj

@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 50; objectVersion = 51;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@ -25,7 +25,7 @@
1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = "<group>"; }; 1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = "<group>"; };
2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = "<group>"; }; 2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = "<group>"; };
3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = "<group>"; }; 3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = "<group>"; };
7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF7B242A565900829871 /* Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chat.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; }; AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
@ -57,7 +57,7 @@
7555FF7C242A565900829871 /* Products */ = { 7555FF7C242A565900829871 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7555FF7B242A565900829871 /* iosApp.app */, 7555FF7B242A565900829871 /* Chat.app */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -112,6 +112,7 @@
7555FF77242A565900829871 /* Sources */, 7555FF77242A565900829871 /* Sources */,
7555FF79242A565900829871 /* Resources */, 7555FF79242A565900829871 /* Resources */,
9964867F0862B4D9FB6ABFC7 /* Frameworks */, 9964867F0862B4D9FB6ABFC7 /* Frameworks */,
BF14C3248E150D55CBFB145F /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -119,7 +120,7 @@
); );
name = iosApp; name = iosApp;
productName = iosApp; productName = iosApp;
productReference = 7555FF7B242A565900829871 /* iosApp.app */; productReference = 7555FF7B242A565900829871 /* Chat.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@ -166,6 +167,23 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
BF14C3248E150D55CBFB145F /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
showEnvVarsInLog = 0;
};
E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = { E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;

11
examples/chat/iosApp/iosApp/ComposeInsideSwiftUIScreen.swift

@ -14,7 +14,7 @@ struct ComposeInsideSwiftUIScreen: View {
struct ComposeLayer: View { struct ComposeLayer: View {
var body: some View { var body: some View {
GradientTemplate(title: "Compose inside SwiftUI") { GradientTemplate(title: "The Composers Chat") {
ComposeViewControllerToSwiftUI() ComposeViewControllerToSwiftUI()
.ignoresSafeArea(.keyboard) // Compose have own keyboard handler .ignoresSafeArea(.keyboard) // Compose have own keyboard handler
} }
@ -22,7 +22,7 @@ struct ComposeLayer: View {
} }
struct TextInputLayer: View { struct TextInputLayer: View {
@State private var textState: String = "text message" @State private var textState: String = ""
@FocusState private var textFieldFocused: Bool @FocusState private var textFieldFocused: Bool
var body: some View { var body: some View {
@ -39,12 +39,11 @@ struct TextInputLayer: View {
textState = "" textState = ""
}) { }) {
HStack { HStack {
Image(systemName: "play.fill") Image(systemName: "arrow.up.circle.fill")
Text("Send") }.tint(Color(red: 0.671, green: 0.365, blue: 0.792))
}.tint(.white)
} }
} }
}.padding(10).background(RoundedRectangle(cornerRadius: 10).fill(gradient).opacity(0.8)).padding(6) }.padding(15).background(RoundedRectangle(cornerRadius: 200).fill(.white).opacity(0.95)).padding(15)
} }
} }
} }

1
examples/chat/iosApp/iosApp/GradientTemplate.swift

@ -22,6 +22,5 @@ struct GradientTemplate<Content: View>: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.statusBar(hidden: false) .statusBar(hidden: false)
} }
.toolbar(.visible, for: .tabBar)
} }
} }

12
examples/chat/iosApp/iosApp/iosApp.swift

@ -1,7 +1,10 @@
import SwiftUI import SwiftUI
let gradient = LinearGradient( let gradient = LinearGradient(
colors: gradient3Colors(), colors: [
Color(red: 0.933, green: 0.937, blue: 0.953),
Color(red: 0.902, green: 0.941, blue: 0.949)
],
startPoint: .topLeading, endPoint: .bottomTrailing startPoint: .topLeading, endPoint: .bottomTrailing
) )
@ -11,12 +14,13 @@ struct iOSApp: App {
WindowGroup { WindowGroup {
TabView { TabView {
ComposeInsideSwiftUIScreen() ComposeInsideSwiftUIScreen()
.tabItem { Label("Compose", systemImage: "square.and.pencil") } .tabItem { Label("Group Chat", systemImage: "rectangle.3.group.bubble.left") }
YetAnotherSwiftUIScreen() YetAnotherSwiftUIScreen()
.tabItem { Label("SwiftUI", systemImage: "list.dash") } .tabItem { Label("Settings", systemImage: "gear") }
}.accentColor(.white).preferredColorScheme(.dark) }
.accentColor(Color(red: 0.671, green: 0.365, blue: 0.792)).preferredColorScheme(.light)
} }
} }
} }

65
examples/chat/shared/src/commonMain/kotlin/ChatApp.kt

@ -1,19 +1,46 @@
import androidx.compose.foundation.layout.* import androidx.compose.foundation.Image
import androidx.compose.material.* import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.* import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
val myUser = User("Me") val myUser = User("Me", picture = null)
val friends = listOf(User("Alex"), User("Lily"), User("Sam")) val friends = listOf(
User("Alex", picture = "stock1.jpg"),
User("Casey", picture = "stock2.jpg"),
User("Sam", picture = "stock3.jpg")
)
val friendMessages = listOf( val friendMessages = listOf(
"Hi, have a nice day!", "How's everybody doing today?",
"Nice to see you!", "I've been meaning to chat!",
"Multiline\ntext\nmessage" "When do we hang out next? 😋",
"We really need to catch up!",
"It's been too long!",
"I can't\nbelieve\nit! 😱",
"Did you see that ludicrous\ndisplay last night?",
"We should meet up in person!",
"How about a round of pinball?",
"I'd love to:\n🍔 Eat something\n🎥 Watch a movie, maybe?\nWDYT?"
) )
val store = CoroutineScope(SupervisorJob()).createStore() val store = CoroutineScope(SupervisorJob()).createStore()
@ -23,7 +50,7 @@ fun ChatAppWithScaffold(displayTextField: Boolean = true) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("Chat sample") }, title = { Text("The Composers Chat") },
backgroundColor = MaterialTheme.colors.background, backgroundColor = MaterialTheme.colors.background,
) )
}) { }) {
@ -32,12 +59,14 @@ fun ChatAppWithScaffold(displayTextField: Boolean = true) {
} }
} }
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
fun ChatApp(displayTextField: Boolean = true) { fun ChatApp(displayTextField: Boolean = true) {
val state by store.stateFlow.collectAsState() val state by store.stateFlow.collectAsState()
Theme { Theme {
Surface { Surface {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Image(painterResource("background.jpg"), null, contentScale = ContentScale.Crop)
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@ -58,13 +87,21 @@ fun ChatApp(displayTextField: Boolean = true) {
} }
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
var lastFriend = friends.random()
var lastMessage = friendMessages.random()
while (true) { while (true) {
val thisFriend = friends.random()
val thisMessage = friendMessages.random()
if(thisFriend == lastFriend) continue
if(thisMessage == lastMessage) continue
lastFriend = thisFriend
lastMessage = thisMessage
store.send( store.send(
Action.SendMessage( Action.SendMessage(
message = Message( message = Message(
user = friends.random(), user = thisFriend,
timeMs = timestampMs(), timeMs = timestampMs(),
text = friendMessages.random() text = thisMessage
) )
) )
) )
@ -76,11 +113,13 @@ fun ChatApp(displayTextField: Boolean = true) {
@Composable @Composable
fun Theme(content: @Composable () -> Unit) { fun Theme(content: @Composable () -> Unit) {
MaterialTheme( MaterialTheme(
colors = darkColors( colors = lightColors(
surface = Color(ChatColors.SURFACE), surface = Color(ChatColors.SURFACE),
background = Color(ChatColors.BACKGROUND), background = Color(ChatColors.TOP_GRADIENT.last()),
), ),
) { ) {
ProvideTextStyle(LocalTextStyle.current.copy(letterSpacing = 0.sp)) {
content() content()
} }
} }
}

143
examples/chat/shared/src/commonMain/kotlin/ChatMessage.kt

@ -0,0 +1,143 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun Triangle(risingToTheRight: Boolean, background: Color) {
Box(
Modifier
.padding(bottom = 10.dp, start = 0.dp)
.clip(TriangleEdgeShape(risingToTheRight))
.background(background)
.size(6.dp)
)
}
@Composable
inline fun ChatMessage(isMyMessage: Boolean, message: Message) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = if (isMyMessage) Alignment.CenterEnd else Alignment.CenterStart
) {
Row(verticalAlignment = Alignment.Bottom) {
if (!isMyMessage) {
Column {
UserPic(message.user)
}
Spacer(Modifier.size(2.dp))
Column {
Triangle(true, ChatColors.OTHERS_MESSAGE)
}
}
Column {
Box(
Modifier.clip(
RoundedCornerShape(
10.dp,
10.dp,
if (!isMyMessage) 10.dp else 0.dp,
if (!isMyMessage) 0.dp else 10.dp
)
)
.background(color = if (!isMyMessage) ChatColors.OTHERS_MESSAGE else ChatColors.MY_MESSAGE)
.padding(start = 10.dp, top = 5.dp, end = 10.dp, bottom = 5.dp),
) {
Column {
if(!isMyMessage) {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = message.user.name,
style = MaterialTheme.typography.body1.copy(
fontWeight = FontWeight.SemiBold,
letterSpacing = 0.sp,
fontSize = 14.sp
),
color = message.user.color
)
}
}
Spacer(Modifier.size(3.dp))
Text(
text = message.text,
style = MaterialTheme.typography.body1.copy(
fontSize = 18.sp,
letterSpacing = 0.sp
)
)
Spacer(Modifier.size(4.dp))
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.align(Alignment.End)
) {
Text(
text = timeToString(message.timeMs),
textAlign = TextAlign.End,
style = MaterialTheme.typography.subtitle1.copy(fontSize = 10.sp),
color = ChatColors.TIME_TEXT
)
}
}
}
Box(Modifier.size(10.dp))
}
if(isMyMessage) {
Column {
Triangle(false, ChatColors.MY_MESSAGE)
}
}
}
}
}
// Adapted from https://stackoverflow.com/questions/65965852/jetpack-compose-create-chat-bubble-with-arrow-and-border-elevation
class TriangleEdgeShape(val risingToTheRight: Boolean) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val trianglePath = if(risingToTheRight) {
Path().apply {
moveTo(x = 0f, y = size.height)
lineTo(x = size.width, y = 0f)
lineTo(x = size.width, y = size.height)
}
} else {
Path().apply {
moveTo(x = 0f, y = 0f)
lineTo(x = size.width, y = size.height)
lineTo(x = 0f, y = size.height)
}
}
return Outline.Generic(path = trianglePath)
}
}

5
examples/chat/shared/src/commonMain/kotlin/Colors.kt

@ -1,3 +1,4 @@
import androidx.compose.ui.graphics.Color
object ChatColors { object ChatColors {
val GRADIENT_3 = listOf(0xFF7F52FF, 0xFFC811E2, 0xFFE54857) val GRADIENT_3 = listOf(0xFF7F52FF, 0xFFC811E2, 0xFFE54857)
@ -5,4 +6,8 @@ object ChatColors {
val PRIMARY = 0xFFAA77EE val PRIMARY = 0xFFAA77EE
val SURFACE = 0xFFCC99FF val SURFACE = 0xFFCC99FF
val BACKGROUND = 0xFF663388 val BACKGROUND = 0xFF663388
val TOP_GRADIENT = listOf(0xFFEEEFF3, 0xFFE6F0F2)
val MY_MESSAGE = Color(0xFFE5FEFB)
val OTHERS_MESSAGE = Color.White
val TIME_TEXT = Color(0xFF979797)
} }

27
examples/chat/shared/src/commonMain/kotlin/Data.kt

@ -1,5 +1,6 @@
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt
data class Message private constructor( data class Message private constructor(
val user: User, val user: User,
@ -21,9 +22,25 @@ data class Message private constructor(
data class User( data class User(
val name: String, val name: String,
val pictureColor: Color = Color( val color: Color = ColorProvider.getColor(),
red = Random.nextInt(0xff), val picture: String?
green = Random.nextInt(0xff),
blue = Random.nextInt(0xff)
),
) )
object ColorProvider {
val colors = mutableListOf(
0xFFEA3468,
0xFFB634EA,
0xFF349BEA,
)
val allColors = colors.toList()
fun getColor(): Color {
if(colors.size == 0) {
colors.addAll(allColors)
}
println(colors.lastIndex)
val idx = Random.nextInt(colors.indices)
val color = colors[idx]
colors.removeAt(idx)
return Color(color)
}
}

116
examples/chat/shared/src/commonMain/kotlin/Messages.kt

@ -1,30 +1,26 @@
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource
@Composable @Composable
internal inline fun Messages(messages: List<Message>) { internal inline fun Messages(messages: List<Message>) {
@ -35,97 +31,39 @@ internal inline fun Messages(messages: List<Message>) {
} }
} }
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize().padding(start = 4.dp, end = 4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
state = listState, state = listState,
) { ) {
messages.forEach { message -> item { Spacer(Modifier.size(20.dp)) }
item(key = message.id) { items(messages, key = { it.id }) {
ChatMessage(isMyMessage = message.user == myUser, message) ChatMessage(isMyMessage = it.user == myUser, it)
}
} }
item { item {
Box(Modifier.height(50.dp)) Box(Modifier.height(70.dp))
} }
} }
} }
@OptIn(ExperimentalResourceApi::class)
@Composable @Composable
private inline fun ChatMessage(isMyMessage: Boolean, message: Message) { fun UserPic(user: User) {
val focusManager = LocalFocusManager.current val imageSize = 48f
Box( val painter = user.picture?.let {
modifier = Modifier.fillMaxWidth(), painterResource(it)
contentAlignment = if (isMyMessage) Alignment.CenterEnd else Alignment.CenterStart } ?: object : Painter() {
) { override val intrinsicSize: Size = Size(imageSize, imageSize)
Surface( override fun DrawScope.onDraw() {
modifier = Modifier.padding(4.dp), drawRect(user.color, size = Size(imageSize * 4, imageSize * 4))
shape = RoundedCornerShape(size = 20.dp),
elevation = 8.dp
) {
Box(
Modifier.background(brush = Brush.horizontalGradient(
ChatColors.GRADIENT_2.map { Color(it) })
).padding(10.dp),
) {
Row(verticalAlignment = Alignment.Top) {
if (!isMyMessage) {
UserPic(message.user)
Spacer(Modifier.size(8.dp))
}
Column {
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = message.user.name,
style = MaterialTheme.typography.h5
)
Spacer(Modifier.size(10.dp))
Text(
text = timeToString(message.timeMs),
style = MaterialTheme.typography.h6
)
}
Text(
text = message.text
)
}
if (isMyMessage) {
Spacer(Modifier.size(8.dp))
UserPic(message.user)
}
}
}
if (!isMyMessage) {
var liked by remember { mutableStateOf(false) }
Icon(
modifier = Modifier.align(Alignment.BottomEnd)
.clickable {
liked = !liked
focusManager.clearFocus(true)
}
.padding(3.dp),
imageVector = if (liked) Icons.Filled.Favorite else Icons.Outlined.Favorite,
contentDescription = "Like",
tint = if (liked) Color.Red else Color.Gray
)
}
}
} }
} }
@Composable
private fun UserPic(user: User) {
val imageSize = 64f
Image( Image(
modifier = Modifier modifier = Modifier
.size(imageSize.dp) .size(imageSize.dp)
.clip(CircleShape), .clip(CircleShape),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
painter = object : Painter() { painter = painter,
override val intrinsicSize: Size = Size(imageSize, imageSize)
override fun DrawScope.onDraw() {
drawRect(user.pictureColor, size = Size(imageSize * 4, imageSize * 4))
}
},
contentDescription = "User picture" contentDescription = "User picture"
) )
} }

9
examples/chat/shared/src/commonMain/kotlin/SendMessage.kt

@ -7,13 +7,17 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.* import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
@ -23,9 +27,10 @@ fun SendMessage(sendMessage: (String) -> Unit) {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.padding(10.dp), .padding(10.dp),
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White),
value = inputText, value = inputText,
placeholder = { placeholder = {
Text("type message here") Text("Type message...")
}, },
onValueChange = { onValueChange = {
inputText = it inputText = it

BIN
examples/chat/shared/src/commonMain/resources/background.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

BIN
examples/chat/shared/src/commonMain/resources/stock1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
examples/chat/shared/src/commonMain/resources/stock2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
examples/chat/shared/src/commonMain/resources/stock3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
examples/chat/shared/src/commonMain/resources/stock4.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Loading…
Cancel
Save