Browse Source

experimental chat example (#2148)

pull/2174/head
dima.avdeev 2 years ago committed by GitHub
parent
commit
3129fa1d53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      experimental/examples/chat-mpp/.gitignore
  2. 23
      experimental/examples/chat-mpp/README.md
  3. 215
      experimental/examples/chat-mpp/build.gradle.kts
  4. 13
      experimental/examples/chat-mpp/gradle.properties
  5. BIN
      experimental/examples/chat-mpp/gradle/wrapper/gradle-wrapper.jar
  6. 5
      experimental/examples/chat-mpp/gradle/wrapper/gradle-wrapper.properties
  7. 185
      experimental/examples/chat-mpp/gradlew
  8. 23
      experimental/examples/chat-mpp/settings.gradle.kts
  9. 22
      experimental/examples/chat-mpp/src/androidMain/AndroidManifest.xml
  10. 17
      experimental/examples/chat-mpp/src/androidMain/kotlin/MainActivity.kt
  11. 3
      experimental/examples/chat-mpp/src/androidMain/kotlin/currentTime.android.kt
  12. 3
      experimental/examples/chat-mpp/src/androidMain/res/res/values/strings.xml
  13. 3
      experimental/examples/chat-mpp/src/androidMain/res/values/strings.xml
  14. 62
      experimental/examples/chat-mpp/src/commonMain/kotlin/ChatApp.kt
  15. 29
      experimental/examples/chat-mpp/src/commonMain/kotlin/Data.kt
  16. 125
      experimental/examples/chat-mpp/src/commonMain/kotlin/Messages.kt
  17. 16
      experimental/examples/chat-mpp/src/commonMain/kotlin/Reducer.kt
  18. 54
      experimental/examples/chat-mpp/src/commonMain/kotlin/SendMessage.kt
  19. 35
      experimental/examples/chat-mpp/src/commonMain/kotlin/Store.kt
  20. 22
      experimental/examples/chat-mpp/src/commonMain/kotlin/currentTime.common.kt
  21. 3
      experimental/examples/chat-mpp/src/desktopMain/kotlin/currentTime.desktop.kt
  22. 20
      experimental/examples/chat-mpp/src/desktopMain/kotlin/main.desktop.kt
  23. 5
      experimental/examples/chat-mpp/src/jsMain/kotlin/currentTime.js.kt
  24. 16
      experimental/examples/chat-mpp/src/jsMain/kotlin/main.js.kt
  25. 15
      experimental/examples/chat-mpp/src/jsMain/resources/index.html
  26. 8
      experimental/examples/chat-mpp/src/jsMain/resources/styles.css
  27. 6
      experimental/examples/chat-mpp/src/macosMain/kotlin/currentTime.macos.kt
  28. 11
      experimental/examples/chat-mpp/src/macosMain/kotlin/main.macos.kt
  29. 6
      experimental/examples/chat-mpp/src/uikitMain/kotlin/currentTime.uikit.kt
  30. 37
      experimental/examples/chat-mpp/src/uikitMain/kotlin/main.uikit.kt
  31. 1
      experimental/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt
  32. 1
      experimental/examples/minesweeper/src/uikitMain/kotlin/main.uikit.kt

2
experimental/examples/chat-mpp/.gitignore vendored

@ -0,0 +1,2 @@
local.properties
.idea

23
experimental/examples/chat-mpp/README.md

@ -0,0 +1,23 @@
# Chat example app
## Run on Android:
- connect device or emulator
- `./gradlew installDebug`
- open app
## Run on Desktop jvm
`./gradlew run`
## Run native on MacOS
`./gradlew runDebugExecutableMacosX64` (Works on Intel processors)
## Run web assembly in browser
`./gradlew jsBrowserDevelopmentRun`
## Run on iOS simulator
`./gradlew iosDeployIPhone8Debug`
`./gradlew iosDeployIPadDebug`
## Run on iOS device
- Read about iOS target in [falling-balls-mpp/README.md](../falling-balls-mpp/README.md)
- `./gradlew iosDeployDeviceRelease`

215
experimental/examples/chat-mpp/build.gradle.kts

@ -0,0 +1,215 @@
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.compose.experimental.dsl.IOSDevices
plugins {
id("com.android.application")
kotlin("multiplatform")
id("org.jetbrains.compose")
}
version = "1.0-SNAPSHOT"
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
kotlin {
android()
jvm("desktop")
js(IR) {
browser()
binaries.executable()
}
macosX64 {
binaries {
executable {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal"
)
}
}
}
macosArm64 {
binaries {
executable {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal"
)
}
}
}
iosX64("uikitX64") {
binaries {
executable() {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
)
}
}
}
iosArm64("uikitArm64") {
binaries {
executable() {
entryPoint = "main"
freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
)
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.ui)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.runtime)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val androidMain by getting {
dependsOn(commonMain)
kotlin.srcDirs("src/jvmMain/kotlin")
dependencies {
api("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.activity:activity-compose:1.4.0")
}
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
val jsMain by getting {
dependencies {
implementation(compose.web.core)
}
}
val nativeMain by creating {
dependsOn(commonMain)
}
val macosMain by creating {
dependsOn(nativeMain)
}
val macosX64Main by getting {
dependsOn(macosMain)
}
val macosArm64Main by getting {
dependsOn(macosMain)
}
val uikitMain by creating {
dependsOn(nativeMain)
}
val uikitX64Main by getting {
dependsOn(uikitMain)
}
val uikitArm64Main by getting {
dependsOn(uikitMain)
}
}
}
compose.desktop {
application {
mainClass = "Main_desktopKt"
}
}
compose.experimental {
web.application {}
uikit.application {
bundleIdPrefix = "org.jetbrains"
projectName = "Chat"
deployConfigurations {
simulator("IPhone8") {
//Usage: ./gradlew iosDeployIPhone8Debug
device = IOSDevices.IPHONE_8
}
simulator("IPad") {
//Usage: ./gradlew iosDeployIPadDebug
device = IOSDevices.IPAD_MINI_6th_Gen
}
connectedDevice("Device") {
//First need specify your teamId here, or in local.properties (compose.ios.teamId=***)
//teamId="***"
//Usage: ./gradlew iosDeployDeviceRelease
}
}
}
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}
compose.desktop.nativeApplication {
targets(kotlin.targets.getByName("macosX64"))
distributions {
targetFormats(TargetFormat.Dmg)
packageName = "Chat"
packageVersion = "1.0.0"
}
}
// a temporary workaround for a bug in jsRun invocation - see https://youtrack.jetbrains.com/issue/KT-48273
afterEvaluate {
rootProject.extensions.configure<NodeJsRootExtension> {
versions.webpackDevServer.version = "4.0.0"
versions.webpackCli.version = "4.9.0"
nodeVersion = "16.0.0"
}
}
// TODO: remove when https://youtrack.jetbrains.com/issue/KT-50778 fixed
project.tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile::class.java).configureEach {
kotlinOptions.freeCompilerArgs += listOf(
"-Xir-dce-runtime-diagnostic=log"
)
}
android {
compileSdk = 31
defaultConfig {
minSdk = 21
targetSdk = 31
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
sourceSets {
named("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
res.srcDirs("src/androidMain/res", "src/commonMain/resources")
}
}
}

13
experimental/examples/chat-mpp/gradle.properties

@ -0,0 +1,13 @@
org.gradle.jvmargs=-Xmx3g
kotlin.code.style=official
kotlin.native.cacheKind=none
kotlin.native.useEmbeddableCompilerJar=true
kotlin.native.enableDependencyPropagation=false
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.binary.memoryModel=experimental
compose.desktop.verbose=true
android.useAndroidX=true
compose.version=1.2.0-alpha01-dev725
kotlin.version=1.6.21
agp.version=7.0.4
kotlin.js.webpack.major.version=4

BIN
experimental/examples/chat-mpp/gradle/wrapper/gradle-wrapper.jar vendored

Binary file not shown.

5
experimental/examples/chat-mpp/gradle/wrapper/gradle-wrapper.properties vendored

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
experimental/examples/chat-mpp/gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

23
experimental/examples/chat-mpp/settings.gradle.kts

@ -0,0 +1,23 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}
plugins {
val kotlinVersion = extra["kotlin.version"] as String
val agpVersion = extra["agp.version"] as String
val composeVersion = extra["compose.version"] as String
kotlin("jvm").version(kotlinVersion)
kotlin("multiplatform").version(kotlinVersion)
kotlin("android").version(kotlinVersion)
id("com.android.base").version(agpVersion)
id("com.android.application").version(agpVersion)
id("com.android.library").version(agpVersion)
id("org.jetbrains.compose").version(composeVersion)
}
}
rootProject.name = "chat-mpp"

22
experimental/examples/chat-mpp/src/androidMain/AndroidManifest.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jetbrains.chat">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:exported="true"
android:name="MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

17
experimental/examples/chat-mpp/src/androidMain/kotlin/MainActivity.kt

@ -0,0 +1,17 @@
package org.jetbrains.chat
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.io.FileOutputStream
import ChatApp
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ChatApp()
}
}
}

3
experimental/examples/chat-mpp/src/androidMain/kotlin/currentTime.android.kt

@ -0,0 +1,3 @@
actual fun timestampMs(): Long {
return System.currentTimeMillis()
}

3
experimental/examples/chat-mpp/src/androidMain/res/res/values/strings.xml

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Chat app</string>
</resources>

3
experimental/examples/chat-mpp/src/androidMain/res/values/strings.xml

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Chat app</string>
</resources>

62
experimental/examples/chat-mpp/src/commonMain/kotlin/ChatApp.kt

@ -0,0 +1,62 @@
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import kotlinx.coroutines.delay
val myUser = User("Me")
val friends = listOf(User("Alex"), User("Lily"), User("Sam"))
val friendMessages = listOf(
"Hi, have a nice day!",
"Nice to see you!",
"Multiline\ntext\nmessage"
)
@Composable
fun ChatApp() {
val coroutineScope = rememberCoroutineScope()
val store = remember { coroutineScope.createStore() }
val state by store.stateFlow.collectAsState()
MaterialTheme {
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Chat sample") }
)
}
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(Modifier.weight(1f)) {
Messages(state.messages)
}
SendMessage { text ->
store.send(
Action.SendMessage(
Message(myUser, timeMs = timestampMs(), text)
)
)
}
}
}
}
}
LaunchedEffect(Unit) {
while (true) {
store.send(
Action.SendMessage(
message = Message(
user = friends.random(),
timeMs = timestampMs(),
text = friendMessages.random()
)
)
)
delay(5000)
}
}
}

29
experimental/examples/chat-mpp/src/commonMain/kotlin/Data.kt

@ -0,0 +1,29 @@
import androidx.compose.ui.graphics.Color
import kotlin.random.Random
data class Message private constructor(
val user: User,
val timeMs: Long,
val text: String,
val id: Long
) {
constructor(
user: User,
timeMs: Long,
text: String
) : this(
user = user,
timeMs = timeMs,
text = text,
id = Random.nextLong()
)
}
data class User(
val name: String,
val pictureColor: Color = Color(
red = Random.nextInt(0xff),
green = Random.nextInt(0xff),
blue = Random.nextInt(0xff)
),
)

125
experimental/examples/chat-mpp/src/commonMain/kotlin/Messages.kt

@ -0,0 +1,125 @@
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
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.mutableStateOf
import androidx.compose.runtime.*
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
@Composable
internal inline fun Messages(messages: List<Message>) {
val listState = rememberLazyListState()
if (messages.isNotEmpty()) {
LaunchedEffect(messages.last()) {
listState.animateScrollToItem(messages.lastIndex, scrollOffset = 2)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
state = listState,
) {
messages.forEach { message ->
item(key = message.id) {
ChatMessage(isMyMessage = message.user == myUser, message)
}
}
// items(messages, key = { it.id }) { message -> //TODO not working in JS
// ChatMessage(isMyMessage = message.user == myUser, message)
// }
}
}
@Composable
private inline fun ChatMessage(isMyMessage: Boolean, message: Message) {
Box(modifier = Modifier.fillMaxWidth()) {
Surface(
modifier = Modifier.padding(4.dp)
.align(if (isMyMessage) Alignment.CenterStart else Alignment.CenterEnd),
shape = RoundedCornerShape(size = 20.dp),
elevation = 8.dp
) {
Box(
Modifier.background(brush = Brush.horizontalGradient(listOf(Color(0xff8888ff), Color(0xffddddff))))
.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
}
.padding(4.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(
modifier = Modifier
.size(imageSize.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
painter = object : Painter() {
override val intrinsicSize: Size = Size(imageSize, imageSize)
override fun DrawScope.onDraw() {
drawRect(user.pictureColor, size = Size(imageSize * 4, imageSize * 4))
}
},
contentDescription = "User picture"
)
}

16
experimental/examples/chat-mpp/src/commonMain/kotlin/Reducer.kt

@ -0,0 +1,16 @@
sealed interface Action {
data class SendMessage(val message: Message) : Action
}
data class State(
val messages: List<Message> = emptyList()
)
fun chatReducer(state: State, action: Action): State =
when (action) {
is Action.SendMessage -> {
state.copy(
messages = (state.messages + action.message).takeLast(100)
)
}
}

54
experimental/examples/chat-mpp/src/commonMain/kotlin/SendMessage.kt

@ -0,0 +1,54 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun SendMessage(sendMessage: (String) -> Unit) {
var inputText by remember { mutableStateOf("") }
TextField(
modifier = Modifier.fillMaxWidth()
.background(MaterialTheme.colors.background)
.padding(10.dp),
value = inputText,
placeholder = {
Text("type message here")
},
onValueChange = {
inputText = it
},
trailingIcon = {
if (inputText.isNotEmpty()) {
Row(
modifier = Modifier
.clickable {
sendMessage(inputText)
inputText = ""
}
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.Send,
contentDescription = "Send",
tint = MaterialTheme.colors.primary
)
Text("Send")
}
}
}
)
}

35
experimental/examples/chat-mpp/src/commonMain/kotlin/Store.kt

@ -0,0 +1,35 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
interface Store {
fun send(action: Action)
val stateFlow: StateFlow<State>
val state get() = stateFlow.value
}
fun CoroutineScope.createStore(): Store {
val mutableStateFlow = MutableStateFlow(State())
val channel: Channel<Action> = Channel(Channel.UNLIMITED)
return object : Store {
init {
launch {
channel.consumeAsFlow().collect { action ->
mutableStateFlow.value = chatReducer(mutableStateFlow.value, action)
}
}
}
override fun send(action: Action) {
launch {
channel.send(action)
}
}
override val stateFlow: StateFlow<State> = mutableStateFlow
}
}

22
experimental/examples/chat-mpp/src/commonMain/kotlin/currentTime.common.kt

@ -0,0 +1,22 @@
fun timeToString(timestampMs: Long): String {
val seconds = timestampMs
val minutes = seconds / 1000 / 60
val hours = minutes / 24
val m = minutes % 60
val h = hours % 24
val mm = if (m < 10) {
"0$m"
} else {
m.toString()
}
val hh = if (h < 10) {
"0$h"
} else {
h.toString()
}
return "$hh:$mm"
}
expect fun timestampMs(): Long

3
experimental/examples/chat-mpp/src/desktopMain/kotlin/currentTime.desktop.kt

@ -0,0 +1,3 @@
actual fun timestampMs(): Long {
return System.currentTimeMillis()
}

20
experimental/examples/chat-mpp/src/desktopMain/kotlin/main.desktop.kt

@ -0,0 +1,20 @@
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.singleWindowApplication
fun main() =
singleWindowApplication(
title = "Chat",
state = WindowState(size = DpSize(500.dp, 800.dp))
) {
ChatApp()
}
@Preview
@Composable
fun ChatPreview() {
ChatApp()
}

5
experimental/examples/chat-mpp/src/jsMain/kotlin/currentTime.js.kt

@ -0,0 +1,5 @@
import kotlin.js.Date
actual fun timestampMs(): Long {
return Date.now().toLong()
}

16
experimental/examples/chat-mpp/src/jsMain/kotlin/main.js.kt

@ -0,0 +1,16 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import org.jetbrains.skiko.wasm.onWasmReady
fun main() {
onWasmReady {
Window("Chat") {
Column(modifier = Modifier.fillMaxSize()) {
ChatApp()
}
}
}
}

15
experimental/examples/chat-mpp/src/jsMain/resources/index.html

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>compose multiplatform web demo</title>
<script src="skiko.js"> </script>
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div>
<canvas id="ComposeTarget" width="600" height="600"></canvas>
</div>
<script src="chat-mpp.js"> </script>
</body>
</html>

8
experimental/examples/chat-mpp/src/jsMain/resources/styles.css

@ -0,0 +1,8 @@
#root {
width: 100%;
height: 100vh;
}
#root > .compose-web-column > div {
position: relative;
}

6
experimental/examples/chat-mpp/src/macosMain/kotlin/currentTime.macos.kt

@ -0,0 +1,6 @@
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
actual fun timestampMs(): Long {
return NSDate().timeIntervalSince1970().toLong()
}

11
experimental/examples/chat-mpp/src/macosMain/kotlin/main.macos.kt

@ -0,0 +1,11 @@
import androidx.compose.ui.window.Window
import platform.AppKit.NSApp
import platform.AppKit.NSApplication
fun main() {
NSApplication.sharedApplication()
Window("Chat App") {
ChatApp()
}
NSApp?.run()
}

6
experimental/examples/chat-mpp/src/uikitMain/kotlin/currentTime.uikit.kt

@ -0,0 +1,6 @@
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
actual fun timestampMs(): Long {
return (NSDate().timeIntervalSince1970() * 1000).toLong()
}

37
experimental/examples/chat-mpp/src/uikitMain/kotlin/main.uikit.kt

@ -0,0 +1,37 @@
import androidx.compose.ui.window.Application
import kotlinx.cinterop.*
import platform.UIKit.*
import platform.Foundation.*
fun main() {
val args = emptyArray<String>()
memScoped {
val argc = args.size + 1
val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues()
autoreleasepool {
UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate))
}
}
}
class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol {
companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta
@ObjCObjectBase.OverrideInit
constructor() : super()
private var _window: UIWindow? = null
override fun window() = _window
override fun setWindow(window: UIWindow?) {
_window = window
}
override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map<Any?, *>?): Boolean {
window = UIWindow(frame = UIScreen.mainScreen.bounds)
window!!.rootViewController = Application("Chat") {
ChatApp()
}
window!!.makeKeyAndVisible()
return true
}
}

1
experimental/examples/falling-balls-mpp/src/uikitMain/kotlin/main.uikit.kt

@ -3,7 +3,6 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/ */
// Use `xcodegen` first, then `open ./ComposeFallingBalls.xcodeproj` and then Run button in XCode.
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height

1
experimental/examples/minesweeper/src/uikitMain/kotlin/main.uikit.kt

@ -3,7 +3,6 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/ */
// Use `xcodegen` first, then `open ./ComposeMinesweeper.xcodeproj` and then Run button in XCode.
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height

Loading…
Cancel
Save