├── .gitignore ├── README.md ├── androidApp ├── build.gradle.kts └── src │ └── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── myapplication │ │ └── MainActivity.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ └── strings.xml ├── build.gradle.kts ├── cleanup.sh ├── desktopApp ├── build.gradle.kts └── src │ ├── commonMain │ └── resources │ │ └── bg.png │ └── jvmMain │ └── kotlin │ └── main.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── Podfile ├── iosApp.xcodeproj │ └── project.pbxproj ├── iosApp.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── settings.gradle.kts └── shared ├── build.gradle.kts ├── shared.podspec └── src ├── androidMain ├── AndroidManifest.xml └── kotlin │ ├── main.android.kt │ ├── remote │ └── RemoteSettings.kt │ └── viewmodel │ └── CommonViewModel.kt ├── commonMain ├── kotlin │ ├── App.kt │ ├── model │ │ └── ChatEvent.kt │ ├── remote │ │ ├── ChatService.kt │ │ └── ChatServiceImpl.kt │ ├── ui │ │ ├── ChatScreen.kt │ │ ├── CreateMessage.kt │ │ ├── MessageList.kt │ │ ├── Navigation.kt │ │ ├── TypingUsers.kt │ │ ├── WelcomeScreen.kt │ │ ├── model │ │ │ └── Message.kt │ │ └── theme │ │ │ └── Theme.kt │ └── viewmodel │ │ ├── ChatViewModel.kt │ │ └── CommonViewModel.kt └── resources │ └── bg.png ├── desktopMain └── kotlin │ ├── main.desktop.kt │ ├── remote │ └── RemoteSettings.kt │ └── viewmodel │ └── CommonViewModel.kt ├── iosMain └── kotlin │ ├── main.ios.kt │ ├── remote │ └── RemoteSettings.kt │ └── viewmodel │ └── CommonViewModel.kt └── main └── res └── drawable └── bg.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .DS_Store 5 | build 6 | captures 7 | .externalNativeBuild 8 | .cxx 9 | local.properties 10 | xcuserdata 11 | iosApp/Podfile.lock 12 | iosApp/Pods/* 13 | iosApp/iosApp.xcworkspace/* 14 | iosApp/iosApp.xcodeproj/* 15 | !iosApp/iosApp.xcodeproj/project.pbxproj 16 | shared/shared.podspec -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KCChatApp 2 | 3 | This is a demo project for the [KotlinX Libraries](https://www.youtube.com/watch?v=lzLNmMXfmfo&list=PLlFc5cFwUnmwcJ7ZXyMmS70A9QFyUu1HI&index=67) presentation at KotlinConf'23 4 | 5 | The chat application is implemented using [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) for Desktop (JVM), Android, and iOS targets. 6 | The [server application](https://github.com/svtk/KCChatAppServer) is implemented using with [Ktor](https://ktor.io) framework. 7 | 8 | 9 | -------------------------------------------------------------------------------- /androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.application") 4 | id("org.jetbrains.compose") 5 | } 6 | 7 | kotlin { 8 | android() 9 | sourceSets { 10 | val androidMain by getting { 11 | dependencies { 12 | implementation(project(":shared")) 13 | } 14 | } 15 | } 16 | } 17 | 18 | android { 19 | compileSdk = 33 20 | defaultConfig { 21 | applicationId = "com.myapplication.MyApplication" 22 | minSdk = 26 23 | targetSdk = 33 24 | versionCode = 1 25 | versionName = "1.0" 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_11 29 | targetCompatibility = JavaVersion.VERSION_11 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/kotlin/com/myapplication/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.myapplication 2 | 3 | import MainView 4 | import android.os.Bundle 5 | import androidx.activity.compose.setContent 6 | import androidx.appcompat.app.AppCompatActivity 7 | 8 | class MainActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | 12 | setContent { 13 | MainView() 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My application 3 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | kotlin("jvm") apply false 5 | kotlin("multiplatform") apply false 6 | kotlin("android") apply false 7 | id("com.android.application") apply false 8 | id("com.android.library") apply false 9 | id("org.jetbrains.compose") apply false 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf .idea 3 | ./gradlew clean 4 | rm -rf .gradle 5 | rm -rf build 6 | rm -rf */build 7 | rm -rf iosApp/iosApp.xcworkspace 8 | rm -rf iosApp/Pods 9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace 10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata 11 | -------------------------------------------------------------------------------- /desktopApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("org.jetbrains.compose") 6 | } 7 | 8 | kotlin { 9 | jvm {} 10 | sourceSets { 11 | val jvmMain by getting { 12 | dependencies { 13 | implementation(compose.desktop.currentOs) 14 | implementation(project(":shared")) 15 | } 16 | } 17 | } 18 | } 19 | 20 | compose.desktop { 21 | application { 22 | mainClass = "MainKt" 23 | 24 | nativeDistributions { 25 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 26 | packageName = "KotlinMultiplatformComposeDesktopApplication" 27 | packageVersion = "1.0.0" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /desktopApp/src/commonMain/resources/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/desktopApp/src/commonMain/resources/bg.png -------------------------------------------------------------------------------- /desktopApp/src/jvmMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.dp 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import androidx.compose.ui.window.rememberWindowState 5 | 6 | fun main() = application { 7 | Window( 8 | onCloseRequest = ::exitApplication, 9 | title = "Chat", 10 | state = rememberWindowState(width = 400.dp, height = 800.dp), 11 | ) { 12 | MainView() 13 | } 14 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | xcodeproj=./iosApp 3 | kotlin.native.cocoapods.generate.wrapper=true 4 | android.useAndroidX=true 5 | org.gradle.jvmargs=-Xmx3g 6 | org.jetbrains.compose.experimental.jscanvas.enabled=true 7 | org.jetbrains.compose.experimental.macos.enabled=true 8 | org.jetbrains.compose.experimental.uikit.enabled=true 9 | kotlin.native.cacheKind=none 10 | kotlin.native.useEmbeddableCompilerJar=true 11 | kotlin.mpp.androidSourceSetLayoutVersion=2 12 | # Enable kotlin/native experimental memory model 13 | kotlin.native.binary.memoryModel=experimental 14 | kotlin.version=1.8.0 15 | agp.version=7.4.1 16 | compose.version=1.4.0-alpha01-dev954 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.myapplication.MyApplication 3 | APP_NAME=My application 4 | -------------------------------------------------------------------------------- /iosApp/Podfile: -------------------------------------------------------------------------------- 1 | target 'iosApp' do 2 | use_frameworks! 3 | platform :ios, '14.1' 4 | pod 'shared', :path => '../shared' 5 | end -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 11 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 12 | CFDB58B53BB94DE262B13C24 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 17 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 18 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 19 | 4FF3202A603A284706412EDC /* 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 = ""; }; 20 | 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 7555FF7B242A565900829871 /* My application.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "My application.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 25 | FF8CA3F5360CEAB49D74065F /* 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 = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | F85CB1118929364A9C6EFABC /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | CFDB58B53BB94DE262B13C24 /* Pods_iosApp.framework in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 058557D7273AAEEB004C7B11 /* Preview Content */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, 44 | ); 45 | path = "Preview Content"; 46 | sourceTree = ""; 47 | }; 48 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 6B1049432C0C2B312090ABF6 /* Pods_iosApp.framework */, 52 | ); 53 | name = Frameworks; 54 | sourceTree = ""; 55 | }; 56 | 7555FF72242A565900829871 = { 57 | isa = PBXGroup; 58 | children = ( 59 | AB1DB47929225F7C00F7AF9C /* Configuration */, 60 | 7555FF7D242A565900829871 /* iosApp */, 61 | 7555FF7C242A565900829871 /* Products */, 62 | FEFF387C0A8D172AA4D59CAE /* Pods */, 63 | 42799AB246E5F90AF97AA0EF /* Frameworks */, 64 | ); 65 | sourceTree = ""; 66 | }; 67 | 7555FF7C242A565900829871 /* Products */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 7555FF7B242A565900829871 /* My application.app */, 71 | ); 72 | name = Products; 73 | sourceTree = ""; 74 | }; 75 | 7555FF7D242A565900829871 /* iosApp */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 058557BA273AAA24004C7B11 /* Assets.xcassets */, 79 | 7555FF82242A565900829871 /* ContentView.swift */, 80 | 7555FF8C242A565B00829871 /* Info.plist */, 81 | 2152FB032600AC8F00CF470E /* iOSApp.swift */, 82 | 058557D7273AAEEB004C7B11 /* Preview Content */, 83 | ); 84 | path = iosApp; 85 | sourceTree = ""; 86 | }; 87 | AB1DB47929225F7C00F7AF9C /* Configuration */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | AB3632DC29227652001CCB65 /* Config.xcconfig */, 91 | ); 92 | path = Configuration; 93 | sourceTree = ""; 94 | }; 95 | FEFF387C0A8D172AA4D59CAE /* Pods */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */, 99 | FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */, 100 | ); 101 | path = Pods; 102 | sourceTree = ""; 103 | }; 104 | /* End PBXGroup section */ 105 | 106 | /* Begin PBXNativeTarget section */ 107 | 7555FF7A242A565900829871 /* iosApp */ = { 108 | isa = PBXNativeTarget; 109 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; 110 | buildPhases = ( 111 | 98D614C51D2DA07C614CC46E /* [CP] Check Pods Manifest.lock */, 112 | 7555FF77242A565900829871 /* Sources */, 113 | 7555FF79242A565900829871 /* Resources */, 114 | F85CB1118929364A9C6EFABC /* Frameworks */, 115 | 7E4237F5C18E1F25C5F35D17 /* [CP] Copy Pods Resources */, 116 | ); 117 | buildRules = ( 118 | ); 119 | dependencies = ( 120 | ); 121 | name = iosApp; 122 | productName = iosApp; 123 | productReference = 7555FF7B242A565900829871 /* My application.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | /* End PBXNativeTarget section */ 127 | 128 | /* Begin PBXProject section */ 129 | 7555FF73242A565900829871 /* Project object */ = { 130 | isa = PBXProject; 131 | attributes = { 132 | LastSwiftUpdateCheck = 1130; 133 | LastUpgradeCheck = 1130; 134 | ORGANIZATIONNAME = orgName; 135 | TargetAttributes = { 136 | 7555FF7A242A565900829871 = { 137 | CreatedOnToolsVersion = 11.3.1; 138 | }; 139 | }; 140 | }; 141 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; 142 | compatibilityVersion = "Xcode 9.3"; 143 | developmentRegion = en; 144 | hasScannedForEncodings = 0; 145 | knownRegions = ( 146 | en, 147 | Base, 148 | ); 149 | mainGroup = 7555FF72242A565900829871; 150 | productRefGroup = 7555FF7C242A565900829871 /* Products */; 151 | projectDirPath = ""; 152 | projectRoot = ""; 153 | targets = ( 154 | 7555FF7A242A565900829871 /* iosApp */, 155 | ); 156 | }; 157 | /* End PBXProject section */ 158 | 159 | /* Begin PBXResourcesBuildPhase section */ 160 | 7555FF79242A565900829871 /* Resources */ = { 161 | isa = PBXResourcesBuildPhase; 162 | buildActionMask = 2147483647; 163 | files = ( 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXResourcesBuildPhase section */ 168 | 169 | /* Begin PBXShellScriptBuildPhase section */ 170 | 7E4237F5C18E1F25C5F35D17 /* [CP] Copy Pods Resources */ = { 171 | isa = PBXShellScriptBuildPhase; 172 | buildActionMask = 2147483647; 173 | files = ( 174 | ); 175 | inputFileListPaths = ( 176 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", 177 | ); 178 | name = "[CP] Copy Pods Resources"; 179 | outputFileListPaths = ( 180 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; 185 | showEnvVarsInLog = 0; 186 | }; 187 | 98D614C51D2DA07C614CC46E /* [CP] Check Pods Manifest.lock */ = { 188 | isa = PBXShellScriptBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | ); 192 | inputFileListPaths = ( 193 | ); 194 | inputPaths = ( 195 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 196 | "${PODS_ROOT}/Manifest.lock", 197 | ); 198 | name = "[CP] Check Pods Manifest.lock"; 199 | outputFileListPaths = ( 200 | ); 201 | outputPaths = ( 202 | "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | shellPath = /bin/sh; 206 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 207 | showEnvVarsInLog = 0; 208 | }; 209 | /* End PBXShellScriptBuildPhase section */ 210 | 211 | /* Begin PBXSourcesBuildPhase section */ 212 | 7555FF77242A565900829871 /* Sources */ = { 213 | isa = PBXSourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 217 | 7555FF83242A565900829871 /* ContentView.swift in Sources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | /* End PBXSourcesBuildPhase section */ 222 | 223 | /* Begin XCBuildConfiguration section */ 224 | 7555FFA3242A565B00829871 /* Debug */ = { 225 | isa = XCBuildConfiguration; 226 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 227 | buildSettings = { 228 | ALWAYS_SEARCH_USER_PATHS = NO; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 232 | CLANG_CXX_LIBRARY = "libc++"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_ENABLE_OBJC_WEAK = YES; 236 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 237 | CLANG_WARN_BOOL_CONVERSION = YES; 238 | CLANG_WARN_COMMA = YES; 239 | CLANG_WARN_CONSTANT_CONVERSION = YES; 240 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 249 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 251 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 252 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 253 | CLANG_WARN_STRICT_PROTOTYPES = YES; 254 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 255 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 256 | CLANG_WARN_UNREACHABLE_CODE = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | COPY_PHASE_STRIP = NO; 259 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | ENABLE_TESTABILITY = YES; 262 | GCC_C_LANGUAGE_STANDARD = gnu11; 263 | GCC_DYNAMIC_NO_PIC = NO; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_OPTIMIZATION_LEVEL = 0; 266 | GCC_PREPROCESSOR_DEFINITIONS = ( 267 | "DEBUG=1", 268 | "$(inherited)", 269 | ); 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 277 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 278 | MTL_FAST_MATH = YES; 279 | ONLY_ACTIVE_ARCH = YES; 280 | SDKROOT = iphoneos; 281 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 282 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 283 | }; 284 | name = Debug; 285 | }; 286 | 7555FFA4242A565B00829871 /* Release */ = { 287 | isa = XCBuildConfiguration; 288 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 289 | buildSettings = { 290 | ALWAYS_SEARCH_USER_PATHS = NO; 291 | CLANG_ANALYZER_NONNULL = YES; 292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 294 | CLANG_CXX_LIBRARY = "libc++"; 295 | CLANG_ENABLE_MODULES = YES; 296 | CLANG_ENABLE_OBJC_ARC = YES; 297 | CLANG_ENABLE_OBJC_WEAK = YES; 298 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 299 | CLANG_WARN_BOOL_CONVERSION = YES; 300 | CLANG_WARN_COMMA = YES; 301 | CLANG_WARN_CONSTANT_CONVERSION = YES; 302 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 303 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 304 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 305 | CLANG_WARN_EMPTY_BODY = YES; 306 | CLANG_WARN_ENUM_CONVERSION = YES; 307 | CLANG_WARN_INFINITE_RECURSION = YES; 308 | CLANG_WARN_INT_CONVERSION = YES; 309 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 310 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 311 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 312 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 313 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 314 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 315 | CLANG_WARN_STRICT_PROTOTYPES = YES; 316 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 317 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 318 | CLANG_WARN_UNREACHABLE_CODE = YES; 319 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 320 | COPY_PHASE_STRIP = NO; 321 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 322 | ENABLE_NS_ASSERTIONS = NO; 323 | ENABLE_STRICT_OBJC_MSGSEND = YES; 324 | GCC_C_LANGUAGE_STANDARD = gnu11; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 333 | MTL_ENABLE_DEBUG_INFO = NO; 334 | MTL_FAST_MATH = YES; 335 | SDKROOT = iphoneos; 336 | SWIFT_COMPILATION_MODE = wholemodule; 337 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 338 | VALIDATE_PRODUCT = YES; 339 | }; 340 | name = Release; 341 | }; 342 | 7555FFA6242A565B00829871 /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | baseConfigurationReference = 4FF3202A603A284706412EDC /* Pods-iosApp.debug.xcconfig */; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | CODE_SIGN_IDENTITY = "Apple Development"; 348 | CODE_SIGN_STYLE = Automatic; 349 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 350 | DEVELOPMENT_TEAM = ZRGM9C766Q; 351 | ENABLE_PREVIEWS = YES; 352 | EXCLUDED_ARCHS = ""; 353 | INFOPLIST_FILE = iosApp/Info.plist; 354 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 355 | LD_RUNPATH_SEARCH_PATHS = ( 356 | "$(inherited)", 357 | "@executable_path/Frameworks", 358 | ); 359 | PRODUCT_BUNDLE_IDENTIFIER = com.jetbrains.ChatApp; 360 | PRODUCT_NAME = "${APP_NAME}"; 361 | PROVISIONING_PROFILE_SPECIFIER = ""; 362 | SWIFT_VERSION = 5.0; 363 | TARGETED_DEVICE_FAMILY = "1,2"; 364 | }; 365 | name = Debug; 366 | }; 367 | 7555FFA7242A565B00829871 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | baseConfigurationReference = FF8CA3F5360CEAB49D74065F /* Pods-iosApp.release.xcconfig */; 370 | buildSettings = { 371 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 372 | CODE_SIGN_IDENTITY = "Apple Development"; 373 | CODE_SIGN_STYLE = Automatic; 374 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 375 | DEVELOPMENT_TEAM = ZRGM9C766Q; 376 | ENABLE_PREVIEWS = YES; 377 | EXCLUDED_ARCHS = ""; 378 | INFOPLIST_FILE = iosApp/Info.plist; 379 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 380 | LD_RUNPATH_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "@executable_path/Frameworks", 383 | ); 384 | PRODUCT_BUNDLE_IDENTIFIER = com.jetbrains.ChatApp; 385 | PRODUCT_NAME = "${APP_NAME}"; 386 | PROVISIONING_PROFILE_SPECIFIER = ""; 387 | SWIFT_VERSION = 5.0; 388 | TARGETED_DEVICE_FAMILY = "1,2"; 389 | }; 390 | name = Release; 391 | }; 392 | /* End XCBuildConfiguration section */ 393 | 394 | /* Begin XCConfigurationList section */ 395 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 396 | isa = XCConfigurationList; 397 | buildConfigurations = ( 398 | 7555FFA3242A565B00829871 /* Debug */, 399 | 7555FFA4242A565B00829871 /* Release */, 400 | ); 401 | defaultConfigurationIsVisible = 0; 402 | defaultConfigurationName = Release; 403 | }; 404 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 405 | isa = XCConfigurationList; 406 | buildConfigurations = ( 407 | 7555FFA6242A565B00829871 /* Debug */, 408 | 7555FFA7242A565B00829871 /* Release */, 409 | ); 410 | defaultConfigurationIsVisible = 0; 411 | defaultConfigurationName = Release; 412 | }; 413 | /* End XCConfigurationList section */ 414 | }; 415 | rootObject = 7555FF73242A565900829871 /* Project object */; 416 | } 417 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import shared 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | Main_iosKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UILaunchScreen 29 | 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 5 | google() 6 | } 7 | 8 | plugins { 9 | val kotlinVersion = extra["kotlin.version"] as String 10 | val agpVersion = extra["agp.version"] as String 11 | val composeVersion = extra["compose.version"] as String 12 | 13 | kotlin("jvm").version(kotlinVersion) 14 | kotlin("multiplatform").version(kotlinVersion) 15 | kotlin("android").version(kotlinVersion) 16 | id("com.android.base").version(agpVersion) 17 | id("com.android.application").version(agpVersion) 18 | id("com.android.library").version(agpVersion) 19 | id("org.jetbrains.compose").version(composeVersion) 20 | } 21 | } 22 | 23 | rootProject.name = "KCChatApp" 24 | 25 | include(":androidApp") 26 | include(":shared") 27 | include(":desktopApp") 28 | -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("OPT_IN_IS_NOT_ENABLED") 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | kotlin("native.cocoapods") 6 | id("com.android.library") 7 | id("org.jetbrains.compose") 8 | kotlin("plugin.serialization") version "1.8.0" 9 | } 10 | 11 | version = "1.0-SNAPSHOT" 12 | 13 | kotlin { 14 | android() 15 | 16 | jvm("desktop") 17 | 18 | ios() 19 | iosSimulatorArm64() 20 | 21 | cocoapods { 22 | summary = "Shared code for the KCChatApp sample" 23 | homepage = "https://github.com/svtk/KCChatApp" 24 | ios.deploymentTarget = "14.1" 25 | podfile = project.file("../iosApp/Podfile") 26 | framework { 27 | baseName = "shared" 28 | isStatic = true 29 | } 30 | extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" 31 | } 32 | 33 | sourceSets { 34 | val ktorVersion = "2.2.3" 35 | val coroutinesVersion = "1.6.4" 36 | 37 | val commonMain by getting { 38 | dependencies { 39 | implementation(compose.runtime) 40 | implementation(compose.foundation) 41 | implementation(compose.material) 42 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) 43 | implementation(compose.components.resources) 44 | 45 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") 46 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") 47 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") 48 | implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5") 49 | 50 | implementation("io.ktor:ktor-client-core:$ktorVersion") 51 | implementation("io.ktor:ktor-client-cio:$ktorVersion") 52 | implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") 53 | implementation("io.ktor:ktor-client-serialization:$ktorVersion") 54 | implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") 55 | implementation("io.ktor:ktor-client-websockets:$ktorVersion") 56 | } 57 | } 58 | val androidMain by getting { 59 | dependencies { 60 | api("androidx.activity:activity-compose:1.6.1") 61 | api("androidx.appcompat:appcompat:1.6.1") 62 | api("androidx.core:core-ktx:1.9.0") 63 | 64 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") 65 | implementation("io.ktor:ktor-client-okhttp:$ktorVersion") 66 | } 67 | } 68 | val iosMain by getting { 69 | dependencies { 70 | implementation("io.ktor:ktor-client-darwin:$ktorVersion") 71 | } 72 | } 73 | val iosSimulatorArm64Main by getting { 74 | dependsOn(iosMain) 75 | } 76 | val desktopMain by getting { 77 | dependencies { 78 | implementation(compose.desktop.common) 79 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$coroutinesVersion") 80 | } 81 | } 82 | } 83 | } 84 | 85 | android { 86 | compileSdk = 33 87 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 88 | sourceSets["main"].res.srcDirs("src/androidMain/res") 89 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 90 | 91 | defaultConfig { 92 | minSdk = 26 93 | targetSdk = 33 94 | } 95 | compileOptions { 96 | sourceCompatibility = JavaVersion.VERSION_11 97 | targetCompatibility = JavaVersion.VERSION_11 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /shared/shared.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'shared' 3 | spec.version = '1.0-SNAPSHOT' 4 | spec.homepage = 'https://github.com/svtk/KCChatApp' 5 | spec.source = { :http=> ''} 6 | spec.authors = '' 7 | spec.license = '' 8 | spec.summary = 'Shared code for the KCChatApp sample' 9 | spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework' 10 | spec.libraries = 'c++' 11 | spec.ios.deployment_target = '14.1' 12 | 13 | 14 | spec.pod_target_xcconfig = { 15 | 'KOTLIN_PROJECT_PATH' => ':shared', 16 | 'PRODUCT_MODULE_NAME' => 'shared', 17 | } 18 | 19 | spec.script_phases = [ 20 | { 21 | :name => 'Build shared', 22 | :execution_position => :before_compile, 23 | :shell_path => '/bin/sh', 24 | :script => <<-SCRIPT 25 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then 26 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" 27 | exit 0 28 | fi 29 | set -ev 30 | REPO_ROOT="$PODS_TARGET_SRCROOT" 31 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ 32 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ 33 | -Pkotlin.native.cocoapods.archs="$ARCHS" \ 34 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" 35 | SCRIPT 36 | } 37 | ] 38 | spec.resources = ['src/commonMain/resources/**', 'src/iosMain/resources/**'] 39 | end -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/main.android.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | 3 | @Composable fun MainView() = App() 4 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/remote/RemoteSettings.kt: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | // to run on an emulator 4 | actual val CHAT_HOST = "10.0.2.2" -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/viewmodel/CommonViewModel.kt: -------------------------------------------------------------------------------- 1 | package viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.CoroutineScope 5 | import androidx.lifecycle.viewModelScope as androidViewModelScope 6 | 7 | actual abstract class CommonViewModel actual constructor(): ViewModel() { 8 | actual val viewModelScope: CoroutineScope = androidViewModelScope 9 | 10 | actual override fun onCleared() { 11 | super.onCleared() 12 | } 13 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import ui.Navigation 3 | import ui.theme.ChatAppTheme 4 | import viewmodel.ChatViewModel 5 | 6 | 7 | @Composable 8 | internal fun App() { 9 | ChatAppTheme { 10 | val chatViewModel = ChatViewModel() 11 | Navigation(chatViewModel) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/model/ChatEvent.kt: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | sealed interface ChatEvent { 8 | val username: String 9 | val timestamp: Instant 10 | } 11 | 12 | @Serializable 13 | data class MessageEvent( 14 | override val username: String, 15 | val messageText: String, 16 | override val timestamp: Instant, 17 | ): ChatEvent 18 | 19 | @Serializable 20 | data class TypingEvent( 21 | override val username: String, 22 | override val timestamp: Instant, 23 | ): ChatEvent -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/remote/ChatService.kt: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import model.ChatEvent 4 | import kotlinx.coroutines.flow.* 5 | 6 | interface ChatService { 7 | suspend fun openSession(username: String) 8 | 9 | fun observeEvents(): Flow 10 | 11 | suspend fun sendEvent(event: ChatEvent) 12 | 13 | suspend fun closeSession() 14 | 15 | companion object { 16 | const val CHAT_PORT = 9010 17 | const val CHAT_WS_PATH = "/chat" 18 | } 19 | } 20 | 21 | expect val CHAT_HOST: String -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/remote/ChatServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.contentnegotiation.* 5 | import io.ktor.client.plugins.websocket.* 6 | import io.ktor.http.* 7 | import io.ktor.serialization.kotlinx.* 8 | import io.ktor.serialization.kotlinx.json.* 9 | import io.ktor.websocket.* 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.flow 12 | import kotlinx.serialization.json.Json 13 | import model.ChatEvent 14 | import remote.ChatService.Companion.CHAT_PORT 15 | import remote.ChatService.Companion.CHAT_WS_PATH 16 | 17 | class ChatServiceImpl : ChatService { 18 | private val client = HttpClient { 19 | install(ContentNegotiation) { 20 | json(Json { 21 | prettyPrint = true 22 | isLenient = true 23 | }) 24 | } 25 | install(WebSockets) { 26 | contentConverter = KotlinxWebsocketSerializationConverter(Json) 27 | } 28 | } 29 | private var socket: DefaultClientWebSocketSession? = null 30 | private lateinit var username: String 31 | 32 | override suspend fun openSession(username: String) { 33 | try { 34 | this.username = username 35 | socket = client.webSocketSession( 36 | method = HttpMethod.Get, 37 | host = CHAT_HOST, 38 | port = CHAT_PORT, 39 | path = CHAT_WS_PATH 40 | ) 41 | } catch (e: Exception) { 42 | e.printStackTrace() 43 | } 44 | } 45 | 46 | override fun observeEvents(): Flow = flow { 47 | while (true) { 48 | emit(socket!!.receiveDeserialized()) 49 | } 50 | } 51 | 52 | override suspend fun sendEvent(event: ChatEvent) { 53 | try { 54 | socket?.sendSerialized(event) 55 | } catch (e: Exception) { 56 | println("Error while sending: " + e.message) 57 | } 58 | } 59 | 60 | override suspend fun closeSession() { 61 | socket?.close() 62 | } 63 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/ChatScreen.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.collectAsState 10 | import androidx.compose.ui.Modifier 11 | import kotlinx.collections.immutable.ImmutableList 12 | import kotlinx.collections.immutable.persistentListOf 13 | import kotlinx.collections.immutable.persistentSetOf 14 | import ui.model.Message 15 | import viewmodel.ChatViewModel 16 | 17 | @Composable 18 | internal fun ChatScreen(chatViewModel: ChatViewModel) { 19 | ChatScreen( 20 | messages = chatViewModel.messagesFlow.collectAsState(persistentListOf()).value, 21 | username = chatViewModel.username.value, 22 | typingUsers = chatViewModel.typingUsers.collectAsState(persistentSetOf()).value, 23 | onMessageSent = chatViewModel::sendMessage, 24 | onUserIsTyping = chatViewModel::startTyping, 25 | ) 26 | } 27 | 28 | @Composable 29 | internal fun ChatScreen( 30 | messages: ImmutableList, 31 | username: String?, 32 | typingUsers: Set, 33 | onMessageSent: (String) -> Unit, 34 | onUserIsTyping: () -> Unit, 35 | ) { 36 | ChatSurface { 37 | Column( 38 | modifier = Modifier.fillMaxSize() 39 | ) { 40 | Box(Modifier.weight(1f)) { 41 | MessageList( 42 | messages = messages, 43 | username = username, 44 | ) 45 | } 46 | Column( 47 | modifier = Modifier.background(MaterialTheme.colors.background) 48 | ) { 49 | TypingUsers( 50 | typingUsers = typingUsers, 51 | ) 52 | CreateMessage( 53 | onMessageSent = onMessageSent, 54 | onTyping = onUserIsTyping, 55 | ) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/CreateMessage.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.* 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Send 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | internal fun CreateMessage( 14 | onMessageSent: (String) -> Unit, 15 | onTyping: () -> Unit 16 | ) { 17 | var message by remember { mutableStateOf("") } 18 | OutlinedTextField( 19 | modifier = Modifier.fillMaxWidth() 20 | .padding(horizontal = 12.dp) 21 | .padding(bottom = 16.dp), 22 | value = message, 23 | onValueChange = { 24 | message = it 25 | onTyping() 26 | }, 27 | label = { Text("Message") }, 28 | trailingIcon = { 29 | if (message.isNotBlank()) { 30 | IconButton( 31 | onClick = { 32 | onMessageSent(message) 33 | message = "" 34 | }, 35 | ) { 36 | Icon( 37 | imageVector = Icons.Filled.Send, 38 | contentDescription = "Send", 39 | tint = MaterialTheme.colors.primary, 40 | ) 41 | } 42 | } 43 | } 44 | ) 45 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/MessageList.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.rememberLazyListState 6 | import androidx.compose.foundation.shape.CornerSize 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.Card 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | import kotlinx.collections.immutable.ImmutableList 18 | import kotlinx.collections.immutable.toPersistentList 19 | import ui.model.Message 20 | import ui.model.timeText 21 | import kotlin.time.Duration.Companion.seconds 22 | 23 | @Composable 24 | internal fun MessageList( 25 | messages: ImmutableList, 26 | username: String?, 27 | ) { 28 | // println("Rendering message list for $username, last message: ${messages.lastOrNull()?.text}") 29 | 30 | val listState = rememberLazyListState() 31 | if (messages.isNotEmpty()) { 32 | LaunchedEffect(messages.last()) { 33 | listState.animateScrollToItem(messages.lastIndex, scrollOffset = 2) 34 | } 35 | } 36 | LazyColumn( 37 | modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp).padding(top = 6.dp), 38 | verticalArrangement = Arrangement.spacedBy(10.dp), 39 | state = listState 40 | ) { 41 | val grouped = messages.groupConsecutiveBy { first, second -> 42 | first.username == second.username && 43 | second.timestamp - first.timestamp < 10.seconds 44 | } 45 | grouped.forEach { group -> 46 | item { 47 | MessageCard( 48 | group.toPersistentList(), 49 | isMyMessage = group.first().username == username 50 | ) 51 | } 52 | } 53 | item { 54 | Spacer(modifier = Modifier.height(10.dp)) 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | private fun MessageCard( 61 | messages: ImmutableList, 62 | isMyMessage: Boolean, 63 | ) { 64 | Box( 65 | contentAlignment = if (isMyMessage) Alignment.CenterEnd else Alignment.CenterStart, 66 | modifier = Modifier 67 | .run { 68 | if (isMyMessage) padding(start = 60.dp) else padding(end = 60.dp) 69 | } 70 | .fillMaxWidth() 71 | ) { 72 | val shape = RoundedCornerShape(16.dp).run { 73 | val cornerSize = CornerSize(4.dp) 74 | if (isMyMessage) copy(topEnd = cornerSize) else copy(topStart = cornerSize) 75 | } 76 | val color = if (isMyMessage) MaterialTheme.colors.primary else MaterialTheme.colors.secondary 77 | Card( 78 | shape = shape, 79 | ) { 80 | Column { 81 | Text( 82 | text = messages.first().username, 83 | color = color, 84 | style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Bold), 85 | modifier = Modifier.padding(top = 10.dp, start = 10.dp, end = 20.dp), 86 | ) 87 | for (message in messages) { 88 | Text( 89 | text = message.text, 90 | modifier = Modifier.padding(top = 5.dp, start = 10.dp, end = 15.dp) 91 | ) 92 | } 93 | Text( 94 | text = messages.last().timeText(), 95 | modifier = Modifier 96 | .align(Alignment.End) 97 | .padding(top = 5.dp, start = 20.dp, end = 10.dp, bottom = 3.dp), 98 | color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f), 99 | style = MaterialTheme.typography.overline, 100 | ) 101 | } 102 | } 103 | } 104 | } 105 | 106 | 107 | fun Iterable.groupConsecutiveBy(groupIdentifier: (T, T) -> Boolean) = 108 | if (!this.any()) 109 | emptyList() 110 | else this 111 | .drop(1) 112 | .fold(mutableListOf(mutableListOf(this.first()))) { groups, t -> 113 | groups.last().apply { 114 | if (groupIdentifier(last(), t)) { 115 | add(t) 116 | } else { 117 | groups.add(mutableListOf(t)) 118 | } 119 | } 120 | groups 121 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/Navigation.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.runtime.* 5 | import androidx.compose.runtime.saveable.rememberSaveable 6 | import viewmodel.ChatViewModel 7 | 8 | @Composable 9 | internal fun Navigation(chatViewModel: ChatViewModel) { 10 | var loggedIn by rememberSaveable { mutableStateOf(false) } 11 | var username by rememberSaveable { mutableStateOf("") } 12 | MaterialTheme { 13 | if (loggedIn) { 14 | LaunchedEffect(true) { 15 | chatViewModel.connectToChat(username) 16 | } 17 | ChatScreen(chatViewModel) 18 | } else { 19 | WelcomeScreen(onJoinClick = { name -> 20 | loggedIn = true 21 | username = name 22 | }) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/TypingUsers.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | internal fun TypingUsers(typingUsers: Set) { 16 | val text = if (typingUsers.isEmpty()) { 17 | "" 18 | } else if (typingUsers.size == 1) { 19 | "${typingUsers.single()} is typing" 20 | } else if (typingUsers.size == 2) { 21 | val (first, second) = typingUsers.toList() 22 | "$first and $second are typing" 23 | } else { 24 | val list = typingUsers.toList() 25 | "${list.take(list.size - 1).joinToString()}, and ${list.last()} are typing" 26 | } 27 | Box( 28 | modifier = Modifier.fillMaxWidth().padding(top = 10.dp), 29 | contentAlignment = Alignment.Center, 30 | ) { 31 | Text( 32 | text = text, 33 | color = MaterialTheme.colors.onBackground.copy(alpha = 0.6f), 34 | style = MaterialTheme.typography.overline.copy(fontWeight = FontWeight.Bold) 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/WelcomeScreen.kt: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.saveable.rememberSaveable 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Brush 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.layout.ContentScale 17 | import androidx.compose.ui.unit.dp 18 | import org.jetbrains.compose.resources.ExperimentalResourceApi 19 | import org.jetbrains.compose.resources.painterResource 20 | 21 | @Composable 22 | internal fun WelcomeScreen(onJoinClick: (String) -> Unit) { 23 | ChatSurface { 24 | Box( 25 | modifier = Modifier.fillMaxSize(), 26 | contentAlignment = Alignment.Center 27 | ) { 28 | Card { 29 | var username by rememberSaveable { mutableStateOf("") } 30 | Column(modifier = Modifier.padding(20.dp)) { 31 | OutlinedTextField( 32 | modifier = Modifier.width(200.dp), 33 | value = username, 34 | onValueChange = { username = it }, 35 | label = { Text(text = "Username") }, 36 | ) 37 | Spacer(modifier = Modifier.size(10.dp)) 38 | Button( 39 | modifier = Modifier.width(200.dp), 40 | onClick = { onJoinClick(username) }, 41 | enabled = username.isNotBlank(), 42 | ) { 43 | Text(text = "Join the chat") 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | @OptIn(ExperimentalResourceApi::class) 52 | @Composable 53 | internal fun ChatSurface( 54 | modifier: Modifier = Modifier, 55 | content: @Composable () -> Unit 56 | ) { 57 | Surface(modifier) { 58 | Image( 59 | painter = painterResource("bg.png"), 60 | contentDescription = "Background", 61 | contentScale = ContentScale.Crop, 62 | modifier = Modifier.fillMaxSize(), 63 | ) 64 | Box( 65 | modifier = Modifier.fillMaxSize() 66 | ) { 67 | content() 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/model/Message.kt: -------------------------------------------------------------------------------- 1 | package ui.model 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.datetime.* 5 | import kotlinx.serialization.Serializable 6 | import model.MessageEvent 7 | 8 | @Immutable 9 | @Serializable 10 | data class Message( 11 | val username: String, 12 | val text: String, 13 | val timestamp: Instant, 14 | ) 15 | 16 | val MessageEvent.message 17 | get() = Message( 18 | username = username, 19 | text = messageText, 20 | timestamp = timestamp, 21 | ) 22 | 23 | fun Message.timeText(): String { 24 | val localDateTime = timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) 25 | val time = localDateTime.run { LocalTime(hour, minute) } 26 | 27 | val date = localDateTime.date 28 | val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) 29 | if (date == today) return "$time" 30 | 31 | val month = localDateTime.month.name.lowercase() 32 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 33 | .substring(0..2) 34 | return "$month ${localDateTime.dayOfMonth}, $time" 35 | 36 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package ui.theme 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.material.lightColors 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | 8 | val indigoMain = Color(0xff3949ab) 9 | val indigoLight = Color(0xffaab6fe) 10 | val orangeDark = Color(0xffc75b39) 11 | 12 | private val ColorPalette = lightColors( 13 | primary = indigoMain, 14 | primaryVariant = indigoLight, 15 | secondary = orangeDark, 16 | secondaryVariant = orangeDark, 17 | background = Color.White, 18 | onSecondary = Color.White, 19 | /* Other default colors to override 20 | surface = Color.White, 21 | onPrimary = Color.White, 22 | onBackground = Color.Black, 23 | onSurface = Color.Black, 24 | */ 25 | ) 26 | 27 | @Composable 28 | internal fun ChatAppTheme( 29 | content: @Composable () -> Unit 30 | ) { 31 | MaterialTheme( 32 | colors = ColorPalette, 33 | content = content 34 | ) 35 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/viewmodel/ChatViewModel.kt: -------------------------------------------------------------------------------- 1 | package viewmodel 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import kotlinx.collections.immutable.* 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.flow.* 8 | import kotlinx.coroutines.launch 9 | import kotlinx.datetime.Clock 10 | import kotlinx.datetime.Instant 11 | import model.MessageEvent 12 | import model.TypingEvent 13 | import remote.ChatService 14 | import remote.ChatServiceImpl 15 | import ui.model.Message 16 | import ui.model.message 17 | import kotlin.time.Duration.Companion.seconds 18 | 19 | class ChatViewModel : CommonViewModel() { 20 | private val chatService: ChatService = ChatServiceImpl() 21 | private var _username = mutableStateOf(null) 22 | val username: State = _username 23 | 24 | private val _messagesFlow: MutableStateFlow> = 25 | MutableStateFlow(persistentListOf()) 26 | val messagesFlow: StateFlow> get() = _messagesFlow 27 | 28 | private var lastTypingTimestamp: Instant? = null 29 | private val _typingEvents: MutableStateFlow> = MutableStateFlow(listOf()) 30 | val typingUsers: Flow> 31 | get() = _typingEvents 32 | .map { it.map(TypingEvent::username).toImmutableSet() } 33 | 34 | fun connectToChat(username: String) { 35 | _username.value = username 36 | viewModelScope.launch { 37 | chatService.openSession(username) 38 | chatService.observeEvents() 39 | .collect { event -> 40 | when (event) { 41 | is MessageEvent -> { 42 | _messagesFlow.update { plist -> plist + event.message } 43 | _typingEvents.update { events -> 44 | // cancelling typing events from a user if this user has written a message 45 | events.filter { it.username != event.username } 46 | } 47 | } 48 | 49 | is TypingEvent -> { 50 | if (event.username != username) { 51 | _typingEvents.update { it + event } 52 | } 53 | viewModelScope.launch { 54 | delay(3000) 55 | _typingEvents.update { it - event } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | fun disconnect() { 64 | viewModelScope.launch { 65 | chatService.closeSession() 66 | } 67 | } 68 | 69 | fun sendMessage(message: String) { 70 | viewModelScope.launch { 71 | val name = username.value ?: return@launch 72 | chatService.sendEvent( 73 | MessageEvent( 74 | username = name, 75 | messageText = message, 76 | timestamp = Clock.System.now() 77 | ) 78 | ) 79 | } 80 | } 81 | 82 | fun startTyping() { 83 | viewModelScope.launch { 84 | val name = username.value ?: return@launch 85 | val now = Clock.System.now() 86 | val lastTimestamp = lastTypingTimestamp 87 | // Swallowing repeating typing events 88 | if (lastTimestamp == null || lastTimestamp + 3.seconds < now) { 89 | lastTypingTimestamp = now 90 | chatService.sendEvent( 91 | TypingEvent(username = name, timestamp = now) 92 | ) 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/viewmodel/CommonViewModel.kt: -------------------------------------------------------------------------------- 1 | package viewmodel 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | 5 | expect abstract class CommonViewModel() { 6 | val viewModelScope: CoroutineScope 7 | protected open fun onCleared() 8 | } -------------------------------------------------------------------------------- /shared/src/commonMain/resources/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/shared/src/commonMain/resources/bg.png -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/main.desktop.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.desktop.ui.tooling.preview.Preview 2 | import androidx.compose.runtime.Composable 3 | 4 | @Composable fun MainView() = App() 5 | 6 | @Preview 7 | @Composable 8 | fun AppPreview() { 9 | App() 10 | } -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/remote/RemoteSettings.kt: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | actual val CHAT_HOST = "0.0.0.0" -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/viewmodel/CommonViewModel.kt: -------------------------------------------------------------------------------- 1 | package viewmodel 2 | 3 | import kotlinx.coroutines.MainScope 4 | import kotlinx.coroutines.cancel 5 | 6 | actual abstract class CommonViewModel { 7 | 8 | actual val viewModelScope = MainScope() 9 | 10 | protected actual open fun onCleared() { 11 | } 12 | 13 | fun clear() { 14 | onCleared() 15 | viewModelScope.cancel() 16 | } 17 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/main.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | 3 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/remote/RemoteSettings.kt: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | actual val CHAT_HOST = "0.0.0.0" -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/viewmodel/CommonViewModel.kt: -------------------------------------------------------------------------------- 1 | package viewmodel 2 | 3 | import kotlinx.coroutines.MainScope 4 | import kotlinx.coroutines.cancel 5 | 6 | actual abstract class CommonViewModel { 7 | 8 | actual val viewModelScope = MainScope() 9 | 10 | protected actual open fun onCleared() { 11 | } 12 | 13 | fun clear() { 14 | onCleared() 15 | viewModelScope.cancel() 16 | } 17 | } -------------------------------------------------------------------------------- /shared/src/main/res/drawable/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svtk/KCChatApp/3bf97be02d294583b0933d28372e31403475e2fe/shared/src/main/res/drawable/bg.png --------------------------------------------------------------------------------