From d39c6b800a521d6968ed287454d5cc0aa2c7f3a4 Mon Sep 17 00:00:00 2001 From: zxq5 Date: Fri, 25 Sep 2026 00:46:24 +0100 Subject: [PATCH] started working on new cross-platform frontend. some progress made on UI. nowhere near functional. --- .idea/AndroidProjectSystem.xml | 6 + .idea/chatapp.iml | 1 + .idea/dataSources.xml | 9 +- .idea/sqldialects.xml | 6 + android/.idea/deploymentTargetSelector.xml | 3 - android/.idea/misc.xml | 3 +- android/app/build.gradle.kts | 2 +- .../dev/zxq5/chatapp/android/MainActivity.kt | 69 +++-- .../android/feature/chat/ChatViewModel.kt | 5 +- .../chatapp/android/feature/chat/Screen.kt | 5 - .../android/feature/settings/settings.kt | 10 +- .../dev/zxq5/chatapp/android/ui/Screen.kt | 5 + android/gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- frontend/.gitignore | 19 ++ frontend/README.md | 41 +++ frontend/androidApp/build.gradle.kts | 54 ++++ frontend/androidApp/proguard-rules.pro | 21 ++ .../androidApp/src/main/AndroidManifest.xml | 22 ++ .../kotlin/dev/zxq5/chatapp/MainActivity.kt | 25 ++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../src/main/res/values/strings.xml | 3 + frontend/build.gradle.kts | 11 + frontend/desktopApp/build.gradle.kts | 28 ++ .../src/main/kotlin/dev/zxq5/chatapp/main.kt | 13 + frontend/gradle.properties | 12 + frontend/gradle/gradle-daemon-jvm.properties | 13 + frontend/gradle/libs.versions.toml | 63 +++++ frontend/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + frontend/gradlew | 248 ++++++++++++++++++ frontend/gradlew.bat | 82 ++++++ frontend/kotlin-js-store/wasm/yarn.lock | 8 + frontend/settings.gradle.kts | 37 +++ frontend/shared/build.gradle.kts | 103 ++++++++ .../chatapp/SharedLogicAndroidHostTest.kt | 12 + .../dev/zxq5/chatapp/Platform.android.kt | 9 + .../drawable/compose-multiplatform.xml | 44 ++++ .../commonMain/kotlin/dev/zxq5/chatapp/App.kt | 82 ++++++ .../kotlin/dev/zxq5/chatapp/Platform.kt | 7 + .../kotlin/dev/zxq5/chatapp/model/Message.kt | 13 + .../dev/zxq5/chatapp/ui/components/Section.kt | 66 +++++ .../dev/zxq5/chatapp/ui/pages/ContactsPage.kt | 164 ++++++++++++ .../zxq5/chatapp/ui/pages/MessengerPage.kt | 93 +++++++ .../kotlin/dev/zxq5/chatapp/ui/pages/Page.kt | 31 +++ .../dev/zxq5/chatapp/ui/pages/SettingsPage.kt | 172 ++++++++++++ .../dev/zxq5/chatapp/ui/pages/SpacesPage.kt | 12 + .../kotlin/dev/zxq5/chatapp/ui/style/Color.kt | 20 ++ .../kotlin/dev/zxq5/chatapp/ui/style/Theme.kt | 33 +++ .../kotlin/dev/zxq5/chatapp/ui/textfield.kt | 110 ++++++++ .../dev/zxq5/chatapp/util/ActionState.kt | 8 + .../kotlin/dev/zxq5/chatapp/util/DateTime.kt | 35 +++ .../dev/zxq5/chatapp/SharedCommonTest.kt | 12 + .../kotlin/dev/zxq5/chatapp/Platform.js.kt | 14 + .../kotlin/dev/zxq5/chatapp/Platform.jvm.kt | 7 + .../zxq5/chatapp/SharedLogicDesktopTest.kt | 12 + .../dev/zxq5/chatapp/Platform.wasmJs.kt | 7 + .../dev/zxq5/chatapp/SharedLogicWebTest.kt | 12 + frontend/webApp/build.gradle.kts | 28 ++ .../webMain/kotlin/dev/zxq5/chatapp/main.kt | 11 + .../webApp/src/webMain/resources/index.html | 20 ++ .../webApp/src/webMain/resources/styles.css | 7 + 75 files changed, 2144 insertions(+), 46 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/sqldialects.xml delete mode 100644 android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/Screen.kt create mode 100644 android/app/src/main/java/dev/zxq5/chatapp/android/ui/Screen.kt create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/androidApp/build.gradle.kts create mode 100644 frontend/androidApp/proguard-rules.pro create mode 100644 frontend/androidApp/src/main/AndroidManifest.xml create mode 100644 frontend/androidApp/src/main/kotlin/dev/zxq5/chatapp/MainActivity.kt create mode 100644 frontend/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 frontend/androidApp/src/main/res/drawable/ic_launcher_background.xml create mode 100644 frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 frontend/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 frontend/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 frontend/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 frontend/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 frontend/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 frontend/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 frontend/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 frontend/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 frontend/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 frontend/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 frontend/androidApp/src/main/res/values/strings.xml create mode 100644 frontend/build.gradle.kts create mode 100644 frontend/desktopApp/build.gradle.kts create mode 100644 frontend/desktopApp/src/main/kotlin/dev/zxq5/chatapp/main.kt create mode 100644 frontend/gradle.properties create mode 100644 frontend/gradle/gradle-daemon-jvm.properties create mode 100644 frontend/gradle/libs.versions.toml create mode 100644 frontend/gradle/wrapper/gradle-wrapper.jar create mode 100644 frontend/gradle/wrapper/gradle-wrapper.properties create mode 100755 frontend/gradlew create mode 100644 frontend/gradlew.bat create mode 100644 frontend/kotlin-js-store/wasm/yarn.lock create mode 100644 frontend/settings.gradle.kts create mode 100644 frontend/shared/build.gradle.kts create mode 100644 frontend/shared/src/androidHostTest/kotlin/dev/zxq5/chatapp/SharedLogicAndroidHostTest.kt create mode 100644 frontend/shared/src/androidMain/kotlin/dev/zxq5/chatapp/Platform.android.kt create mode 100644 frontend/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/App.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/Platform.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/model/Message.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/components/Section.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/ContactsPage.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/MessengerPage.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/Page.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SettingsPage.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SpacesPage.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Color.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Theme.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/textfield.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/ActionState.kt create mode 100644 frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/DateTime.kt create mode 100644 frontend/shared/src/commonTest/kotlin/dev/zxq5/chatapp/SharedCommonTest.kt create mode 100644 frontend/shared/src/jsMain/kotlin/dev/zxq5/chatapp/Platform.js.kt create mode 100644 frontend/shared/src/jvmMain/kotlin/dev/zxq5/chatapp/Platform.jvm.kt create mode 100644 frontend/shared/src/jvmTest/kotlin/dev/zxq5/chatapp/SharedLogicDesktopTest.kt create mode 100644 frontend/shared/src/wasmJsMain/kotlin/dev/zxq5/chatapp/Platform.wasmJs.kt create mode 100644 frontend/shared/src/webTest/kotlin/dev/zxq5/chatapp/SharedLogicWebTest.kt create mode 100644 frontend/webApp/build.gradle.kts create mode 100644 frontend/webApp/src/webMain/kotlin/dev/zxq5/chatapp/main.kt create mode 100644 frontend/webApp/src/webMain/resources/index.html create mode 100644 frontend/webApp/src/webMain/resources/styles.css diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/chatapp.iml b/.idea/chatapp.iml index 70c0631..cd68f94 100644 --- a/.idea/chatapp.iml +++ b/.idea/chatapp.iml @@ -4,6 +4,7 @@ + diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index a5f05a4..56619f4 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,16 +1,11 @@ - + postgresql true org.postgresql.Driver - jdbc:postgresql://100.118.108.58:5432/chatapp_dev - - - - - + jdbc:postgresql://zqsrv:5432/chatapp_dev $ProjectFileDir$ diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..87de61b --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/deploymentTargetSelector.xml b/android/.idea/deploymentTargetSelector.xml index 94a4997..c29ef3b 100644 --- a/android/.idea/deploymentTargetSelector.xml +++ b/android/.idea/deploymentTargetSelector.xml @@ -13,9 +13,6 @@ - - \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml index b2c751a..991a888 100644 --- a/android/.idea/misc.xml +++ b/android/.idea/misc.xml @@ -1,6 +1,7 @@ + - + diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2a023c5..c0aa0e1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -54,7 +54,7 @@ android { isMinifyEnabled = false isDebuggable = true applicationIdSuffix = ".debug" // lets you install both side by side - buildConfigField("String", "BASE_URL", "\"http://zxq5-x1:8000\"") + buildConfigField("String", "BASE_URL", "\"https://chat.zxq5.dev\"") } } diff --git a/android/app/src/main/java/dev/zxq5/chatapp/android/MainActivity.kt b/android/app/src/main/java/dev/zxq5/chatapp/android/MainActivity.kt index a54f762..f655e14 100644 --- a/android/app/src/main/java/dev/zxq5/chatapp/android/MainActivity.kt +++ b/android/app/src/main/java/dev/zxq5/chatapp/android/MainActivity.kt @@ -12,8 +12,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ChatBubbleOutline -import androidx.compose.material.icons.outlined.PeopleOutline +import androidx.compose.material.icons.outlined.Contacts +import androidx.compose.material.icons.outlined.Forum import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -36,13 +38,14 @@ import dev.zxq5.chatapp.android.feature.auth.AuthScreen import dev.zxq5.chatapp.android.feature.auth.AuthViewModel import dev.zxq5.chatapp.android.feature.chat.ChatScreen import dev.zxq5.chatapp.android.feature.chat.ChatViewModel -import dev.zxq5.chatapp.android.feature.chat.Screen +import dev.zxq5.chatapp.android.ui.Screen import dev.zxq5.chatapp.android.feature.contacts.ContactsScreen import dev.zxq5.chatapp.android.feature.settings.SettingsScreen import dev.zxq5.chatapp.android.feature.settings.SettingsViewModel import dev.zxq5.chatapp.android.ui.theme.ChatappTheme class MainActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,7 +94,7 @@ class MainActivity : ComponentActivity() { ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { when (currentScreen) { - Screen.CHAT -> ChatScreen( + Screen.MESSAGES -> ChatScreen( viewModel = chatViewModel, onNavigateToSettings = { chatViewModel.navigateTo(Screen.SETTINGS) }, onLogout = { @@ -99,6 +102,14 @@ class MainActivity : ComponentActivity() { chatViewModel.clearChat() } ) + Screen.SPACES -> ChatScreen( + viewModel = chatViewModel, + onNavigateToSettings = { chatViewModel.navigateTo(Screen.SETTINGS) }, + onLogout = { + authViewModel.logout() + chatViewModel.clearChat() + } + ) Screen.CONTACTS -> ContactsScreen() Screen.SETTINGS -> SettingsScreen( viewModel = settingsViewModel, @@ -148,38 +159,54 @@ fun BottomDock(currentScreen: Screen, onNavigate: (Screen) -> Unit) { RoundedCornerShape(topStart = 0.dp, topEnd = 0.dp) ) ) { + val colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + indicatorColor = Color.Transparent + ) + NavigationBarItem( - selected = currentScreen == Screen.CHAT, - onClick = { onNavigate(Screen.CHAT) }, + selected = currentScreen == Screen.MESSAGES, + onClick = { onNavigate(Screen.MESSAGES) }, icon = { Icon(Icons.Outlined.ChatBubbleOutline, contentDescription = "Chat") }, label = { Text("chat", style = MaterialTheme.typography.labelSmall) }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - indicatorColor = Color.Transparent - ) + colors = colors ) + NavigationBarItem( selected = currentScreen == Screen.CONTACTS, onClick = { onNavigate(Screen.CONTACTS) }, - icon = { Icon(Icons.Outlined.PeopleOutline, contentDescription = "Contacts") }, + icon = { Icon(Icons.Outlined.Contacts, contentDescription = "Contacts") }, label = { Text("contacts", style = MaterialTheme.typography.labelSmall) }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - indicatorColor = Color.Transparent - ) + colors = colors ) + + NavigationBarItem( + selected = currentScreen == Screen.SPACES, + onClick = { onNavigate(Screen.SPACES) }, + icon = { Icon(Icons.Outlined.Forum, contentDescription = "Spaces") }, + label = { Text("spaces", style = MaterialTheme.typography.labelSmall) }, + colors = colors, + ) + NavigationBarItem( selected = currentScreen == Screen.SETTINGS, onClick = { onNavigate(Screen.SETTINGS) }, icon = { Icon(Icons.Outlined.Settings, contentDescription = "Settings") }, label = { Text("settings", style = MaterialTheme.typography.labelSmall) }, - colors = NavigationBarItemDefaults.colors( - selectedIconColor = MaterialTheme.colorScheme.primary, - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - indicatorColor = Color.Transparent - ) + colors = colors ) } } + +//topBar = { +// TopAppBar( +// colors = TopAppBarDefaults.topAppBarColors( +// containerColor = MaterialTheme.colorScheme.primaryContainer, +// titleContentColor = MaterialTheme.colorScheme.primary, +// ), +// title = { +// Text(if (BuildConfig.DEBUG) "Chatapp (Debug)" else "Chatapp",) +// } +// ) +//}, \ No newline at end of file diff --git a/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/ChatViewModel.kt b/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/ChatViewModel.kt index 9c20c01..23d7040 100644 --- a/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/ChatViewModel.kt +++ b/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/ChatViewModel.kt @@ -3,12 +3,11 @@ package dev.zxq5.chatapp.android.feature.chat import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import dev.zxq5.chatapp.android.api.model.Channel import dev.zxq5.chatapp.android.data.repository.ChatRepository import dev.zxq5.chatapp.android.api.model.Message -import dev.zxq5.chatapp.android.api.model.Space import dev.zxq5.chatapp.android.api.model.SpaceDto import dev.zxq5.chatapp.android.core.service.MessageStreamService +import dev.zxq5.chatapp.android.ui.Screen import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -24,7 +23,7 @@ class ChatViewModel(private val chatRepository: ChatRepository) : ViewModel() { private val _channelId = MutableStateFlow(null) val channelId: StateFlow = _channelId - private val _currentScreen = MutableStateFlow(Screen.CHAT) + private val _currentScreen = MutableStateFlow(Screen.MESSAGES) val currentScreen: StateFlow = _currentScreen private val _currentUserId = MutableStateFlow(null) diff --git a/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/Screen.kt b/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/Screen.kt deleted file mode 100644 index 3be28f6..0000000 --- a/android/app/src/main/java/dev/zxq5/chatapp/android/feature/chat/Screen.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.zxq5.chatapp.android.feature.chat - -enum class Screen { - CHAT, CONTACTS, SETTINGS -} \ No newline at end of file diff --git a/android/app/src/main/java/dev/zxq5/chatapp/android/feature/settings/settings.kt b/android/app/src/main/java/dev/zxq5/chatapp/android/feature/settings/settings.kt index 50f0280..fb6dbd3 100644 --- a/android/app/src/main/java/dev/zxq5/chatapp/android/feature/settings/settings.kt +++ b/android/app/src/main/java/dev/zxq5/chatapp/android/feature/settings/settings.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.sp import android.util.Base64 import android.graphics.BitmapFactory import androidx.compose.ui.text.style.TextAlign +import dev.zxq5.chatapp.android.BuildConfig @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -100,6 +101,11 @@ fun SettingsScreen( .padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + SettingsSection(title = "Build info") { + Text("Build: Chatapp ${if (BuildConfig.DEBUG) "debug" else "release" }", style = MaterialTheme.typography.bodyMedium) + + } + SettingsSection(title = "profile") { var displayName by remember { mutableStateOf("") } var username by remember { mutableStateOf("") } @@ -359,7 +365,7 @@ fun SettingsScreen( @Composable fun SettingsSection( title: String, - color: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + color: Color = MaterialTheme.colorScheme.onSurfaceVariant , content: @Composable () -> Unit ) { var expanded by remember { mutableStateOf(true) } @@ -379,7 +385,7 @@ fun SettingsSection( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Text(title.lowercase(), style = MaterialTheme.typography.labelSmall, color = color) + Text(title.lowercase(), style = MaterialTheme.typography.bodyMedium, color = color) Icon( if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, contentDescription = null, diff --git a/android/app/src/main/java/dev/zxq5/chatapp/android/ui/Screen.kt b/android/app/src/main/java/dev/zxq5/chatapp/android/ui/Screen.kt new file mode 100644 index 0000000..3fa3c83 --- /dev/null +++ b/android/app/src/main/java/dev/zxq5/chatapp/android/ui/Screen.kt @@ -0,0 +1,5 @@ +package dev.zxq5.chatapp.android.ui + +enum class Screen { + MESSAGES, CONTACTS, SETTINGS, SPACES +} \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 0e07f6e..4218f2f 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.1.0" +agp = "9.4.0" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f4f7ab9..7c097e0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,8 @@ #Tue Mar 31 13:46:27 BST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionSha256Sum=bbaeb2fef8710818cf0e261201dab964c572f92b942812df0c3620d62a529a01 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..adfa9bf --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,19 @@ +*.iml +.kotlin +.gradle +**/build/ +xcuserdata +!src/**/build/ +local.properties +.idea +.DS_Store +captures +.externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +node_modules/ diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..11f8a39 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,41 @@ +This is a Kotlin Multiplatform project targeting Android, Web, Desktop (JVM). + +* [/shared](./shared/src) is for code that will be shared across your Compose Multiplatform applications. + It contains several subfolders: + - [commonMain](./shared/src/commonMain/kotlin) is for code that’s common for all targets. + - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. + For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, + the [iosMain](./shared/src/iosMain/kotlin) folder would be the right place for such calls. + Similarly, if you want to edit the Desktop (JVM) specific part, the [jvmMain](./shared/src/jvmMain/kotlin) + folder is the appropriate location. + +### Running the apps + +Use the run configurations provided by the run widget in your IDE's toolbar. You can also use these commands and options: + +- Android app: `./gradlew :androidApp:assembleDebug` +- Desktop app: + - Hot reload: `./gradlew :desktopApp:hotRun --auto` + - Standard run: `./gradlew :desktopApp:run` +- Web app: + - Wasm target (faster, modern browsers): `./gradlew :webApp:wasmJsBrowserDevelopmentRun` + - JS target (slower, supports older browsers): `./gradlew :webApp:jsBrowserDevelopmentRun` + +### Running tests + +Use the run button in your IDE's editor gutter, or run tests using Gradle tasks: + +- Android tests: `./gradlew :shared:testAndroidHostTest` +- Desktop tests: `./gradlew :shared:jvmTest` +- Web tests: + - Wasm target: `./gradlew :shared:wasmJsTest` + - JS target: `./gradlew :shared:jsTest` + +--- + +Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html), +[Compose Multiplatform](https://kotlinlang.org/compose-multiplatform/), +[Kotlin/Wasm](https://kotl.in/wasm/)… + +We would appreciate your feedback on Compose/Web and Kotlin/Wasm in the public Slack channel [#compose-web](https://slack-chats.kotlinlang.org/c/compose-web). +If you face any issues, please report them on [YouTrack](https://youtrack.jetbrains.com/newIssue?project=CMP). \ No newline at end of file diff --git a/frontend/androidApp/build.gradle.kts b/frontend/androidApp/build.gradle.kts new file mode 100644 index 0000000..37f96f1 --- /dev/null +++ b/frontend/androidApp/build.gradle.kts @@ -0,0 +1,54 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeCompiler) +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } +} +dependencies { + implementation(project(":shared")) + + implementation(libs.androidx.activity.compose) + + implementation(libs.compose.uiToolingPreview) + debugImplementation(libs.compose.uiTooling) +} + +android { + namespace = "dev.zxq5.chatapp" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "dev.zxq5.chatapp" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} \ No newline at end of file diff --git a/frontend/androidApp/proguard-rules.pro b/frontend/androidApp/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/frontend/androidApp/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/frontend/androidApp/src/main/AndroidManifest.xml b/frontend/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 0000000..26403a7 --- /dev/null +++ b/frontend/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/androidApp/src/main/kotlin/dev/zxq5/chatapp/MainActivity.kt b/frontend/androidApp/src/main/kotlin/dev/zxq5/chatapp/MainActivity.kt new file mode 100644 index 0000000..87d4b53 --- /dev/null +++ b/frontend/androidApp/src/main/kotlin/dev/zxq5/chatapp/MainActivity.kt @@ -0,0 +1,25 @@ +package dev.zxq5.chatapp + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + setContent { + App() + } + } +} + +@Preview +@Composable +fun AppAndroidPreview() { + App() +} \ No newline at end of file diff --git a/frontend/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/frontend/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/frontend/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/androidApp/src/main/res/drawable/ic_launcher_background.xml b/frontend/androidApp/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..e93e11a --- /dev/null +++ b/frontend/androidApp/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/frontend/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a571e60098c92c2baca8a5df62f2929cbff01b52 GIT binary patch literal 3593 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 P00000NkvXXu0mjfP=x?Y literal 0 HcmV?d00001 diff --git a/frontend/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/frontend/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..61da551c5594a1f9d26193983d2cd69189014603 GIT binary patch literal 5339 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 literal 0 HcmV?d00001 diff --git a/frontend/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/frontend/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..da31a871c8dc461a1ea114510bfac640307b0a90 GIT binary patch literal 7472 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 XLS!Xp1OWg5{GWNc`njxgN@xNA=_V&1 literal 0 HcmV?d00001 diff --git a/frontend/androidApp/src/main/res/values/strings.xml b/frontend/androidApp/src/main/res/values/strings.xml new file mode 100644 index 0000000..6c72cec --- /dev/null +++ b/frontend/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Chatapp + \ No newline at end of file diff --git a/frontend/build.gradle.kts b/frontend/build.gradle.kts new file mode 100644 index 0000000..46c397d --- /dev/null +++ b/frontend/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidMultiplatformLibrary) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + alias(libs.plugins.kotlinJvm) apply false + alias(libs.plugins.kotlinMultiplatform) apply false + alias(libs.plugins.buildkonfig) apply false +} \ No newline at end of file diff --git a/frontend/desktopApp/build.gradle.kts b/frontend/desktopApp/build.gradle.kts new file mode 100644 index 0000000..3efd1d3 --- /dev/null +++ b/frontend/desktopApp/build.gradle.kts @@ -0,0 +1,28 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +dependencies { + implementation(project(":shared")) + + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + + implementation(libs.compose.uiToolingPreview) +} + +compose.desktop { + application { + mainClass = "dev.zxq5.chatapp.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "dev.zxq5.chatapp" + packageVersion = "1.0.0" + } + } +} \ No newline at end of file diff --git a/frontend/desktopApp/src/main/kotlin/dev/zxq5/chatapp/main.kt b/frontend/desktopApp/src/main/kotlin/dev/zxq5/chatapp/main.kt new file mode 100644 index 0000000..4ed313b --- /dev/null +++ b/frontend/desktopApp/src/main/kotlin/dev/zxq5/chatapp/main.kt @@ -0,0 +1,13 @@ +package dev.zxq5.chatapp + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "Chatapp", + ) { + App() + } +} \ No newline at end of file diff --git a/frontend/gradle.properties b/frontend/gradle.properties new file mode 100644 index 0000000..2dc57ad --- /dev/null +++ b/frontend/gradle.properties @@ -0,0 +1,12 @@ +#Kotlin +kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx3072M + +#Gradle +org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true +org.gradle.caching=true + +#Android +android.nonTransitiveRClass=true +android.useAndroidX=true \ No newline at end of file diff --git a/frontend/gradle/gradle-daemon-jvm.properties b/frontend/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..5301309 --- /dev/null +++ b/frontend/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e8f7f2860374272b4c64643d36d8f0dc/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/77785cb3076de1b8f41fcbc6540556c0/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e8f7f2860374272b4c64643d36d8f0dc/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/77785cb3076de1b8f41fcbc6540556c0/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3932798aff0bd10c6286eaaff40c6f0a/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/6fa62408db9bcb00d7e180251d5f068c/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e8f7f2860374272b4c64643d36d8f0dc/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/77785cb3076de1b8f41fcbc6540556c0/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/da24493caf79e03d5c254365c5ad7ac1/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/df839bfdbcd8a8e829b9cf1be59c478b/redirect +toolchainVendor=AZUL +toolchainVersion=21 \ No newline at end of file diff --git a/frontend/gradle/libs.versions.toml b/frontend/gradle/libs.versions.toml new file mode 100644 index 0000000..a4458b0 --- /dev/null +++ b/frontend/gradle/libs.versions.toml @@ -0,0 +1,63 @@ +[versions] +appVersion = "0.1.0" + +agp = "9.4.1" +android-compileSdk = "37" +android-minSdk = "31" +android-targetSdk = "37" +androidx-activity = "1.13.0" +androidx-appcompat = "1.8.0" +androidx-core = "1.19.0" +androidx-espresso = "3.7.0" +androidx-lifecycle = "2.11.0" +androidx-testExt = "1.3.0" +buildkonfigGradlePlugin = "0.22.0" +composeMultiplatform = "1.12.0" +junit = "4.13.2" +kotlin = "2.4.20" +kotlin-wrappers = "2026.9.1" +kotlinGradlePlugin = "2.3.20" +kotlinx-coroutines = "1.11.0" +kotlinxDatetime = "0.8.0" +material3 = "1.12.0-alpha03" +composeMaterialIcons = "1.7.3" +buildkonfig = "0.22.0" +qrose = "1.3.0" +cryptography-core = "0.6.0" + +[libraries] +buildkonfig-gradle-plugin = { module = "com.codingfeline.buildkonfig:buildkonfig-gradle-plugin", version.ref = "buildkonfigGradlePlugin" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } +androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } +compose-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMaterialIcons" } +compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } +compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } +kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } +wrappers-browser = { module = "org.jetbrains.kotlin-wrappers:kotlin-browser", version.ref = "kotlin-wrappers" } +qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" } +cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core", version.ref = "cryptography-core" } +cryptography = { module = "dev.whyoleg.cryptography:cryptography-provider-optimal", version.ref = "cryptography-core"} + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig"} diff --git a/frontend/gradle/wrapper/gradle-wrapper.jar b/frontend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b1b8ef56b44f16b14dc800fa8103a6d89abb526f GIT binary patch literal 48462 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π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 literal 0 HcmV?d00001 diff --git a/frontend/gradle/wrapper/gradle-wrapper.properties b/frontend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1cbec39 --- /dev/null +++ b/frontend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 17 22:10:57 BST 2026 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/frontend/gradlew b/frontend/gradlew new file mode 100755 index 0000000..b9bb139 --- /dev/null +++ b/frontend/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/frontend/gradlew.bat b/frontend/gradlew.bat new file mode 100644 index 0000000..24c62d5 --- /dev/null +++ b/frontend/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/frontend/kotlin-js-store/wasm/yarn.lock b/frontend/kotlin-js-store/wasm/yarn.lock new file mode 100644 index 0000000..5f4567d --- /dev/null +++ b/frontend/kotlin-js-store/wasm/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== diff --git a/frontend/settings.gradle.kts b/frontend/settings.gradle.kts new file mode 100644 index 0000000..52597c2 --- /dev/null +++ b/frontend/settings.gradle.kts @@ -0,0 +1,37 @@ +rootProject.name = "Chatapp" + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +include(":androidApp") +include(":desktopApp") +include(":shared") +include(":webApp") \ No newline at end of file diff --git a/frontend/shared/build.gradle.kts b/frontend/shared/build.gradle.kts new file mode 100644 index 0000000..e659cea --- /dev/null +++ b/frontend/shared/build.gradle.kts @@ -0,0 +1,103 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING +import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.BOOLEAN + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.buildkonfig) +} + +buildkonfig { + packageName = "dev.zxq5.chatapp" + + defaultConfigs { + buildConfigField(STRING, "VERSION_NAME", libs.versions.appVersion.get()) + buildConfigField(BOOLEAN, "IS_DEBUG", "true") // default + } + + // override per target/source set + targetConfigs { + create("android") {} + } +} + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath(libs.kotlin.gradle.plugin) + classpath(libs.buildkonfig.gradle.plugin) + } +} + +kotlin { + jvm() + + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + android { + namespace = "dev.zxq5.chatapp.shared" + compileSdk = libs.versions.android.compileSdk.get().toInt() + minSdk = libs.versions.android.minSdk.get().toInt() + + compilerOptions { + jvmTarget = JvmTarget.JVM_11 + } + androidResources { + enable = true + } + withHostTest { + isIncludeAndroidResources = true + } + withDeviceTestBuilder { + sourceSetTreeName = "test" + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + sourceSets { + androidMain.dependencies { + implementation(libs.compose.uiToolingPreview) + implementation(libs.compose.uiTooling) + } + commonMain.dependencies { + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + implementation(libs.compose.components.resources) + implementation(libs.compose.uiToolingPreview) + implementation(libs.androidx.lifecycle.viewmodelCompose) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.compose.materialIconsExtended) + implementation(libs.qrose) + implementation(libs.cryptography) + implementation(libs.kotlinx.datetime) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } +// jsMain.dependencies { +// implementation(libs.wrappers.browser) +// } + } +} + +dependencies { + androidRuntimeClasspath(libs.compose.uiTooling) +} \ No newline at end of file diff --git a/frontend/shared/src/androidHostTest/kotlin/dev/zxq5/chatapp/SharedLogicAndroidHostTest.kt b/frontend/shared/src/androidHostTest/kotlin/dev/zxq5/chatapp/SharedLogicAndroidHostTest.kt new file mode 100644 index 0000000..7c48586 --- /dev/null +++ b/frontend/shared/src/androidHostTest/kotlin/dev/zxq5/chatapp/SharedLogicAndroidHostTest.kt @@ -0,0 +1,12 @@ +package dev.zxq5.chatapp + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicAndroidHostTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/frontend/shared/src/androidMain/kotlin/dev/zxq5/chatapp/Platform.android.kt b/frontend/shared/src/androidMain/kotlin/dev/zxq5/chatapp/Platform.android.kt new file mode 100644 index 0000000..520fd54 --- /dev/null +++ b/frontend/shared/src/androidMain/kotlin/dev/zxq5/chatapp/Platform.android.kt @@ -0,0 +1,9 @@ +package dev.zxq5.chatapp + +import android.os.Build + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform() \ No newline at end of file diff --git a/frontend/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/frontend/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml new file mode 100644 index 0000000..1ffc948 --- /dev/null +++ b/frontend/shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/App.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/App.kt new file mode 100644 index 0000000..e813a6a --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/App.kt @@ -0,0 +1,82 @@ +package dev.zxq5.chatapp + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import dev.zxq5.chatapp.android.ui.theme.ChatappTheme + +import dev.zxq5.chatapp.ui.pages.Page +import dev.zxq5.chatapp.ui.pages.SettingsPage + +@Composable +@Preview +fun App() { + ChatappTheme { + var currentScreen by remember { mutableStateOf(SettingsPage) } + var authenticated by remember { mutableStateOf(false) } + + if (!authenticated) { + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { NavBar(currentScreen) { screen -> + currentScreen = screen + }}, + topBar = { currentScreen.TopBar() } + ) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + currentScreen.Content() + } + } + } else { + Scaffold() {} + } + } +} + + +@Composable +fun NavBar(currentScreen: Page, onNavigate: (Page) -> Unit) { + NavigationBar( + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 0.dp, + modifier = Modifier + .height(80.dp) + .border( + 0.5.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f), + RoundedCornerShape(topStart = 0.dp, topEnd = 0.dp) + ) + ) { + val colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + indicatorColor = Color.Transparent + ) + + for (entry in Page.entries) { + NavigationBarItem( + selected = currentScreen == entry, + onClick = { onNavigate(entry) }, + icon = { Icon(entry.icon, contentDescription = entry.title) }, + label = { Text(entry.title, style = MaterialTheme.typography.labelSmall) }, + colors = colors + ) + } + } +} + diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/Platform.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/Platform.kt new file mode 100644 index 0000000..ffaebf0 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/Platform.kt @@ -0,0 +1,7 @@ +package dev.zxq5.chatapp + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/model/Message.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/model/Message.kt new file mode 100644 index 0000000..21c8824 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/model/Message.kt @@ -0,0 +1,13 @@ +package dev.zxq5.chatapp.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.time.Instant + +@Serializable +data class Message( + @SerialName("user_id") val userId: Int, + @SerialName("display_name") val displayName: String, + @SerialName("text") val content: String, + val timestamp: Instant +) diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/components/Section.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/components/Section.kt new file mode 100644 index 0000000..ba0b32f --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/components/Section.kt @@ -0,0 +1,66 @@ +package dev.zxq5.chatapp.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +@Composable +fun ExpandingSection( + title: String, + content: @Composable () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxWidth() + .border(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f), RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded } + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(title, style = MaterialTheme.typography.titleLarge) + Icon( + if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + + AnimatedVisibility(visible = expanded) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp).padding(bottom = 8.dp)) { + content() + } + } + } +} \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/ContactsPage.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/ContactsPage.kt new file mode 100644 index 0000000..255a4f1 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/ContactsPage.kt @@ -0,0 +1,164 @@ +package dev.zxq5.chatapp.ui.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Nfc +import androidx.compose.material.icons.outlined.Contacts +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.whyoleg.cryptography.BinarySize.Companion.bits +import dev.whyoleg.cryptography.CryptographyProvider +import dev.whyoleg.cryptography.algorithms.HKDF +import dev.whyoleg.cryptography.algorithms.SHA256 +import dev.whyoleg.cryptography.random.CryptographyRandom +import dev.zxq5.chatapp.ui.AppButton +import dev.zxq5.chatapp.ui.components.ExpandingSection +import dev.zxq5.chatapp.ui.SettingsField +import dev.zxq5.chatapp.util.ActionState +import io.github.alexzhirkevich.qrose.rememberQrCodePainter + +data object ContactsPage: Page(title = "Contacts", icon = Icons.Outlined.Contacts) { + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + var contacts by remember { mutableStateOf(listOf("zxq5", "admin")) } + var showAddSection by remember { mutableStateOf(false) } + val sheetState = rememberBottomSheetState(initialValue = SheetValue.Hidden, enabledValues = setOf( + SheetValue.Expanded, SheetValue.Hidden, SheetValue.PartiallyExpanded + )) + + Scaffold( + floatingActionButton = { + ExtendedFloatingActionButton( + text = { Text("add new contact") }, + icon = { Icon(Icons.Filled.Add, contentDescription = "") }, + onClick = { + showAddSection = true + } + ) + } + ) { padding -> + Column { + for (contact in contacts) { + Card { Text(contact) } + } + } + if (showAddSection) { + ModalBottomSheet( + onDismissRequest = { showAddSection = false }, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Add new contact", style = MaterialTheme.typography.titleLarge) + ExpandingSection("Add Via QR Code") { + val payloadString = createKey().toString() + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = rememberQrCodePainter(payloadString), + contentDescription = "Your contact QR code", + modifier = Modifier + .fillMaxWidth(0.8f) + .aspectRatio(1f) + ) + // TODO onclick! + AppButton(label = "Scan A friend's QR", onClick = {}) + } + } + + ExpandingSection("Add Via NFC (Android)") { + var showNFCDropdown by remember { mutableStateOf(false) } + val innerSheetState = + rememberBottomSheetState(initialValue = SheetValue.Hidden) + + AppButton(label = "Add Friend", onClick = { showNFCDropdown = true }) + + if (showNFCDropdown) { + ModalBottomSheet( + onDismissRequest = { showNFCDropdown = false }, + sheetState = innerSheetState + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth().aspectRatio(1f), + ) { + Icon( + Icons.Filled.Nfc, "NFC Icon", + modifier = Modifier.fillMaxWidth(0.5f).aspectRatio(1f) + ) + } + } + } + } + + ExpandingSection("Add via Username") { + var usernameInput by remember { mutableStateOf("") } + var success by remember { mutableStateOf(ActionState.NotReady) } + SettingsField( + label = "username", + value = usernameInput, + onValueChange = { usernameInput = it }, + buttonLabel = "Add friend", + actionState = success, + onClick = {} + ) + } + } + } + } + } + } +} + +fun createKey(): ByteArray { + val hkdf = CryptographyProvider.Default.get(HKDF) + +// Input key material -- e.g., a shared secret from ECDH + val inputKeyMaterial = ByteArray(32) // placeholder for actual key material + + val salt = CryptographyRandom.nextBytes(32) + + val derivation = hkdf.secretDerivation( + digest = SHA256, + outputSize = 256.bits, + salt = salt, + info = "encryption-key".encodeToByteArray() + ) + + val derivedKey: ByteArray = derivation.deriveSecretToByteArrayBlocking(inputKeyMaterial) + return derivedKey +} \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/MessengerPage.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/MessengerPage.kt new file mode 100644 index 0000000..dd43471 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/MessengerPage.kt @@ -0,0 +1,93 @@ +package dev.zxq5.chatapp.ui.pages + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ChatBubbleOutline +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import dev.zxq5.chatapp.model.Message +import dev.zxq5.chatapp.util.appFormat +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format +import kotlinx.datetime.format.DateTimeFormat +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Clock +import kotlin.time.Instant + +data object MessengerPage: Page(title = "Messages", icon = Icons.Outlined.ChatBubbleOutline) { + @Composable + override fun Content() { + val myUID = 0 + val messages = listOf( + Message(userId = 0, displayName = "zxq5", content = "hello world", timestamp = Clock.System.now()), + Message(userId = 1, displayName = "zxq4", content = "hello world", timestamp = Clock.System.now()), + Message(userId = 2, displayName = "admin", content = "hello world", timestamp = Clock.System.now()) + ) + + Column { + for (message in messages) { + MessageBubble(message, myUID) + } + } + } +} + +@Composable +fun MessageBubble(message: Message, currentUID: Int) { + val time = remember(message.timestamp) { + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).appFormat() + } + + val isMyMessage = message.userId == currentUID + val alignment = if (isMyMessage) Alignment.End else Alignment.Start + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = alignment + ) { + Surface( + color = if (isMyMessage) MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f), + shape = RoundedCornerShape( + topStart = 14.dp, + topEnd = 14.dp, + bottomStart = if (isMyMessage) 14.dp else 4.dp, + bottomEnd = if (isMyMessage) 4.dp else 14.dp + ), + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)) + ) { + Column(modifier = Modifier.padding(horizontal = 11.dp, vertical = 8.dp)) { + if (!isMyMessage) { + Text( + message.displayName?.lowercase() ?: "unknown", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), + modifier = Modifier.padding(bottom = 2.dp) + ) + } + Text( + text = message.content, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.9f) + ) + } + } + Text( + text = time, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) + ) + } +} \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/Page.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/Page.kt new file mode 100644 index 0000000..1146145 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/Page.kt @@ -0,0 +1,31 @@ +package dev.zxq5.chatapp.ui.pages + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector + +sealed class Page(val title: String, val icon: ImageVector) { + @Composable + abstract fun Content() + + @Composable + open fun TopBar() { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text(title) + } + ) + } + + companion object { + val entries by lazy { listOf(MessengerPage, SpacesPage, ContactsPage, SettingsPage) } + } +} + diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SettingsPage.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SettingsPage.kt new file mode 100644 index 0000000..296b68e --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SettingsPage.kt @@ -0,0 +1,172 @@ +package dev.zxq5.chatapp.ui.pages + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import dev.zxq5.chatapp.BuildKonfig +import dev.zxq5.chatapp.ui.AppButton +import dev.zxq5.chatapp.ui.SettingsField +import dev.zxq5.chatapp.ui.components.ExpandingSection +import dev.zxq5.chatapp.util.ActionState + +data object SettingsPage: Page(title = "Settings", icon = Icons.Outlined.Settings) { + @Composable + override fun Content() { + Scaffold { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ExpandingSection("Build Info") { + Text("ChatApp v${BuildKonfig.VERSION_NAME} ${if (BuildKonfig.IS_DEBUG) " (Debug)" else ""}") + } + + ExpandingSection("Profile") { + var displayName by remember { mutableStateOf("") } + var username by remember { mutableStateOf("") } + + SettingsField( + label = "display name", + value = displayName, + onValueChange = { displayName = it }, + buttonLabel = "update", + actionState = ActionState.NotReady, + onClick = {} + ) + + Spacer(Modifier.height(16.dp)) + + SettingsField( + label = "username", + value = username, + onValueChange = { username = it }, + buttonLabel = "update", + actionState = ActionState.NotReady, + onClick = {} + ) + } + + ExpandingSection("Security") { + ChangePasswordDropdown() + + } + + ExpandingSection("Session") { + Text("Logout") + } + + ExpandingSection("Danger Zone") { + Text("Delete Account") + } + } + } + } +} + +class AuthenticationDialog { + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Ui(): Boolean? { + var showDialog by remember { mutableStateOf(true) } + var sheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded) + var password by remember { mutableStateOf("") } + + ModalBottomSheet( + onDismissRequest = { showDialog = false }, + sheetState = sheetState + ) { + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("enter your password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + } + + AppButton( + onClick = {}, // todo + label = "update password", + enabled = password.isNotEmpty() && password.isNotEmpty(), + modifier = Modifier.fillMaxWidth() + ) + + if (!showDialog) return false + if + } +} + + + +@Composable +fun ChangePasswordDropdown() = ExpandingSection("Change Password") { + var oldPassword by remember { mutableStateOf("") } + var newPassword by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + + Text("change password", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(bottom = 8.dp)) + OutlinedTextField( + value = oldPassword, + onValueChange = { oldPassword = it }, + label = { Text("old password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = newPassword, + onValueChange = { newPassword = it }, + label = { Text("new password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + label = { Text("new password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) + Spacer(Modifier.height(12.dp)) + AppButton( + onClick = {}, // todo + label = "update password", + enabled = oldPassword.isNotEmpty() && newPassword.isNotEmpty(), + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SpacesPage.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SpacesPage.kt new file mode 100644 index 0000000..2fb6ce4 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/pages/SpacesPage.kt @@ -0,0 +1,12 @@ +package dev.zxq5.chatapp.ui.pages + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Forum +import androidx.compose.runtime.Composable + +data object SpacesPage: Page(title = "Spaces", icon = Icons.Outlined.Forum) { + @Composable + override fun Content() { + + } +} \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Color.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Color.kt new file mode 100644 index 0000000..fec32eb --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Color.kt @@ -0,0 +1,20 @@ +package dev.zxq5.chatapp.android.ui.theme + +import androidx.compose.ui.graphics.Color + +val Black = Color(0xFF0A0A0A) +val DarkGrey = Color(0xFF0D0D0D) +val Grey = Color(0xFF141414) +val LightGrey = Color(0xFF1E1E1E) +val White = Color(0xFFE8E8E8) + +val TextPrimary = Color(0xFFE8E8E8) +val TextSecondary = Color(0xFF888888) +val TextTertiary = Color(0xFF555555) +val TextMuted = Color(0xFF333333) + +val Border = Color(0xFF1A1A1A) +val BorderLight = Color(0xFF222222) + +val Red = Color(0xFFFF0000) +val Surface = Color(0xFF111111) diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Theme.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Theme.kt new file mode 100644 index 0000000..855f7a9 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/style/Theme.kt @@ -0,0 +1,33 @@ +package dev.zxq5.chatapp.android.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable + +private val DarkColorScheme = darkColorScheme( + primary = White, + onPrimary = Black, + secondary = LightGrey, + onSecondary = White, + tertiary = Grey, + background = Black, + onBackground = White, + surface = Black, + onSurface = White, + surfaceVariant = Grey, + onSurfaceVariant = TextSecondary, + outline = Border, + outlineVariant = BorderLight +) + +@Composable +fun ChatappTheme( + darkTheme: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = DarkColorScheme + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/textfield.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/textfield.kt new file mode 100644 index 0000000..a19bcec --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/ui/textfield.kt @@ -0,0 +1,110 @@ +package dev.zxq5.chatapp.ui + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import dev.zxq5.chatapp.util.ActionState + + +@Composable +fun SettingsField( + label: String, + value: String, + onValueChange: (String) -> Unit, + buttonLabel: String, + actionState: ActionState, + onClick: () -> Unit +) { + Column { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = if (actionState == ActionState.Success) Color.Green else MaterialTheme.colorScheme.primary + ) + ) + Spacer(Modifier.height(8.dp)) + SuccessButton( + onClick = onClick, + label = buttonLabel, + actionState = actionState, + enabled = value.isNotBlank(), + modifier = Modifier.fillMaxWidth() + ) + } +} + + +@Composable +fun SuccessButton( + onClick: () -> Unit, + label: String, + actionState: ActionState, + modifier: Modifier = Modifier, + enabled: Boolean = true, + baseColor: Color = MaterialTheme.colorScheme.primary, + contentColor: Color = MaterialTheme.colorScheme.onPrimary, + successColor: Color = Color.Green.copy(alpha = 0.8f) +) { + val backgroundColor by animateColorAsState( + targetValue = if (actionState == ActionState.Success) successColor else baseColor, + animationSpec = tween(500) + ) + + Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor, + contentColor = if (actionState == ActionState.Success) Color.White else contentColor + ) + ) { + Text(if (actionState == ActionState.Success) "success" else label) + } +} + +@Composable +fun AppButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + label: String = "" +) { + Button( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + enabled = enabled, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant + ), + ) { + Text(label) + } +} \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/ActionState.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/ActionState.kt new file mode 100644 index 0000000..26cec00 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/ActionState.kt @@ -0,0 +1,8 @@ +package dev.zxq5.chatapp.util + +enum class ActionState { + NotReady, + Pending, + Success, + Failure +} \ No newline at end of file diff --git a/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/DateTime.kt b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/DateTime.kt new file mode 100644 index 0000000..9d21ad2 --- /dev/null +++ b/frontend/shared/src/commonMain/kotlin/dev/zxq5/chatapp/util/DateTime.kt @@ -0,0 +1,35 @@ +package dev.zxq5.chatapp.util + +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.format +import kotlinx.datetime.todayIn +import kotlinx.datetime.format.* +import kotlin.time.Clock + +fun LocalDateTime.appFormat(timeZone: TimeZone = TimeZone.currentSystemDefault()): String { + val today = Clock.System.todayIn(timeZone) + + val formatter = (if (this.date == today) { + LocalDateTime.Format { + hour() + char(':') + minute() + } + } else { + LocalDateTime.Format { + day() + char('/') + monthNumber() + char('/') + year() + char(' ') + hour() + char(':') + minute() + } + }) + + return this.format(formatter) +} diff --git a/frontend/shared/src/commonTest/kotlin/dev/zxq5/chatapp/SharedCommonTest.kt b/frontend/shared/src/commonTest/kotlin/dev/zxq5/chatapp/SharedCommonTest.kt new file mode 100644 index 0000000..85102aa --- /dev/null +++ b/frontend/shared/src/commonTest/kotlin/dev/zxq5/chatapp/SharedCommonTest.kt @@ -0,0 +1,12 @@ +package dev.zxq5.chatapp + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedCommonTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/frontend/shared/src/jsMain/kotlin/dev/zxq5/chatapp/Platform.js.kt b/frontend/shared/src/jsMain/kotlin/dev/zxq5/chatapp/Platform.js.kt new file mode 100644 index 0000000..c5de0cd --- /dev/null +++ b/frontend/shared/src/jsMain/kotlin/dev/zxq5/chatapp/Platform.js.kt @@ -0,0 +1,14 @@ +package dev.zxq5.chatapp + +import web.navigator.navigator + +class JsPlatform: Platform { + private val userAgent = navigator.userAgent + private val browserList = listOf("Chrome", "Firefox", "Safari", "Edge") + + override val name: String = userAgent.findAnyOf(browserList, ignoreCase = true) + ?.let { (startIndex) -> userAgent.substring(startIndex).substringBefore(" ") } + ?: "Unknown" +} + +actual fun getPlatform(): Platform = JsPlatform() \ No newline at end of file diff --git a/frontend/shared/src/jvmMain/kotlin/dev/zxq5/chatapp/Platform.jvm.kt b/frontend/shared/src/jvmMain/kotlin/dev/zxq5/chatapp/Platform.jvm.kt new file mode 100644 index 0000000..bc533cb --- /dev/null +++ b/frontend/shared/src/jvmMain/kotlin/dev/zxq5/chatapp/Platform.jvm.kt @@ -0,0 +1,7 @@ +package dev.zxq5.chatapp + +class JVMPlatform: Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JVMPlatform() \ No newline at end of file diff --git a/frontend/shared/src/jvmTest/kotlin/dev/zxq5/chatapp/SharedLogicDesktopTest.kt b/frontend/shared/src/jvmTest/kotlin/dev/zxq5/chatapp/SharedLogicDesktopTest.kt new file mode 100644 index 0000000..87ee05c --- /dev/null +++ b/frontend/shared/src/jvmTest/kotlin/dev/zxq5/chatapp/SharedLogicDesktopTest.kt @@ -0,0 +1,12 @@ +package dev.zxq5.chatapp + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicDesktopTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/frontend/shared/src/wasmJsMain/kotlin/dev/zxq5/chatapp/Platform.wasmJs.kt b/frontend/shared/src/wasmJsMain/kotlin/dev/zxq5/chatapp/Platform.wasmJs.kt new file mode 100644 index 0000000..f11d7c0 --- /dev/null +++ b/frontend/shared/src/wasmJsMain/kotlin/dev/zxq5/chatapp/Platform.wasmJs.kt @@ -0,0 +1,7 @@ +package dev.zxq5.chatapp + +class WasmPlatform: Platform { + override val name: String = "Web with Kotlin/Wasm" +} + +actual fun getPlatform(): Platform = WasmPlatform() \ No newline at end of file diff --git a/frontend/shared/src/webTest/kotlin/dev/zxq5/chatapp/SharedLogicWebTest.kt b/frontend/shared/src/webTest/kotlin/dev/zxq5/chatapp/SharedLogicWebTest.kt new file mode 100644 index 0000000..2b0d4ad --- /dev/null +++ b/frontend/shared/src/webTest/kotlin/dev/zxq5/chatapp/SharedLogicWebTest.kt @@ -0,0 +1,12 @@ +package dev.zxq5.chatapp + +import kotlin.test.Test +import kotlin.test.assertEquals + +class SharedLogicWebTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +} \ No newline at end of file diff --git a/frontend/webApp/build.gradle.kts b/frontend/webApp/build.gradle.kts new file mode 100644 index 0000000..1d8e205 --- /dev/null +++ b/frontend/webApp/build.gradle.kts @@ -0,0 +1,28 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) +} + +kotlin { + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + commonMain.dependencies { + implementation(project(":shared")) + + implementation(libs.compose.ui) + } + } +} \ No newline at end of file diff --git a/frontend/webApp/src/webMain/kotlin/dev/zxq5/chatapp/main.kt b/frontend/webApp/src/webMain/kotlin/dev/zxq5/chatapp/main.kt new file mode 100644 index 0000000..b9485cd --- /dev/null +++ b/frontend/webApp/src/webMain/kotlin/dev/zxq5/chatapp/main.kt @@ -0,0 +1,11 @@ +package dev.zxq5.chatapp + +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + ComposeViewport { + App() + } +} \ No newline at end of file diff --git a/frontend/webApp/src/webMain/resources/index.html b/frontend/webApp/src/webMain/resources/index.html new file mode 100644 index 0000000..36b9083 --- /dev/null +++ b/frontend/webApp/src/webMain/resources/index.html @@ -0,0 +1,20 @@ + + + + + + Chatapp + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/webApp/src/webMain/resources/styles.css b/frontend/webApp/src/webMain/resources/styles.css new file mode 100644 index 0000000..0549b10 --- /dev/null +++ b/frontend/webApp/src/webMain/resources/styles.css @@ -0,0 +1,7 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} \ No newline at end of file