├── .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
--------------------------------------------------------------------------------