From 222c7ec8ee1bcd7074b8c8609344a1a46a82059f Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 6 Dec 2024 13:18:18 +0600 Subject: [PATCH 1/7] 22.0.0 --- gradle.properties | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 381249d..4aa009b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ kotlin.daemon.jvmargs=-Xmx3g -Xms500m kotlin_version=2.1.0 -telegram_bot_api_version=21.0.0 -micro_utils_version=0.23.1 +telegram_bot_api_version=22.0.0 +micro_utils_version=0.23.2 serialization_version=1.7.3 -ktor_version=3.0.1 +ktor_version=3.0.2 From 1c437690e48597859d1302f44c121810b0225f3e Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Fri, 6 Dec 2024 16:32:47 +0600 Subject: [PATCH 2/7] migrate webapp --- WebApp/src/jsMain/kotlin/main.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index 6802855..2d6ce57 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -2,6 +2,7 @@ import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.webapps.* import dev.inmo.tgbotapi.webapps.cloud.* +import dev.inmo.tgbotapi.webapps.events.* import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackStyle import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackType import dev.inmo.tgbotapi.webapps.popup.* @@ -298,7 +299,7 @@ fun main() { document.body ?.log("Theme changed: ${webApp.themeParams}") } onViewportChanged { - document.body ?.log("Viewport changed: ${it.isStateStable}") + document.body ?.log("Viewport changed: ${it}") } backButton.apply { onClick { From 76f151586efdaca129174f3f7b4e196fbd192d00 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 7 Dec 2024 11:36:39 +0600 Subject: [PATCH 3/7] start migration to compose in webapp --- WebApp/build.gradle | 6 + WebApp/src/jsMain/kotlin/main.kt | 249 ++++++++++++++----------- WebApp/src/jsMain/resources/index.html | 1 + build.gradle | 5 + gradle.properties | 1 + 5 files changed, 154 insertions(+), 108 deletions(-) diff --git a/WebApp/build.gradle b/WebApp/build.gradle index 53e310e..8700d47 100644 --- a/WebApp/build.gradle +++ b/WebApp/build.gradle @@ -11,6 +11,9 @@ buildscript { plugins { id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.plugin.serialization" + + id "org.jetbrains.kotlin.plugin.compose" version "$kotlin_version" + id "org.jetbrains.compose" version "$compose_version" } apply plugin: 'application' @@ -27,12 +30,14 @@ kotlin { dependencies { implementation kotlin('stdlib') implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" + implementation compose.runtime } } jsMain { dependencies { implementation "dev.inmo:tgbotapi.webapps:$telegram_bot_api_version" + implementation compose.web.core } } @@ -41,6 +46,7 @@ kotlin { implementation "dev.inmo:tgbotapi:$telegram_bot_api_version" implementation "dev.inmo:micro_utils.ktor.server:$micro_utils_version" implementation "io.ktor:ktor-server-cio:$ktor_version" + implementation compose.desktop.currentOs } } } diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index 2d6ce57..8bdeb7f 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -1,3 +1,4 @@ +import androidx.compose.runtime.* import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.webapps.* @@ -18,6 +19,10 @@ import kotlinx.dom.appendElement import kotlinx.dom.appendText import kotlinx.dom.clear import kotlinx.serialization.json.Json +import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.P +import org.jetbrains.compose.web.dom.Text +import org.jetbrains.compose.web.renderComposable import org.w3c.dom.* import kotlin.random.Random import kotlin.random.nextUBytes @@ -33,118 +38,146 @@ fun main() { val client = HttpClient() val baseUrl = window.location.origin.removeSuffix("/") + renderComposable("root") { + val scope = rememberCoroutineScope() + val isSafeState = remember { mutableStateOf(null) } + val logsState = remember { mutableStateListOf() } + LaunchedEffect(baseUrl) { + val response = client.post("$baseUrl/check") { + setBody( + Json.encodeToString( + WebAppDataWrapper.serializer(), + WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash) + ) + ) + } + val dataIsSafe = response.bodyAsText().toBoolean() + + document.body ?.log( + if (dataIsSafe) { + "Data is safe" + } else { + "Data is unsafe" + } + ) + + document.body ?.log( + webApp.initDataUnsafe.chat.toString() + ) + } + + Text( + when (isSafeState.value) { + null -> "Checking safe state..." + true -> "Data is safe" + false -> "Data is unsafe" + } + ) + Text(webApp.initDataUnsafe.chat.toString()) + + Button({ + onClick { + scope.launchSafelyWithoutExceptions { + handleResult({ "Clicked" }) { + client.post("${window.location.origin.removeSuffix("/")}/inline") { + parameter(webAppQueryIdField, it) + setBody(TextContent("Clicked", ContentType.Text.Plain)) + logsState.add(url.build().toString()) + }.coroutineContext.job.join() + } + } + } + }) { + Text("Answer in chat button") + } + + P() + Text("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}") + + P() + Text("Alerts:") + Button({ + onClick { + webApp.showPopup( + PopupParams( + "It is sample title of default button", + "It is sample message of default button", + DefaultPopupButton("default", "Default button"), + OkPopupButton("ok"), + DestructivePopupButton("destructive", "Destructive button") + ) + ) { + logsState.add( + when (it) { + "default" -> "You have clicked default button in popup" + "ok" -> "You have clicked ok button in popup" + "destructive" -> "You have clicked destructive button in popup" + else -> "I can't imagine where you take button with id $it" + } + ) + } + } + }) { + Text("Popup") + } + Button({ + onClick { + webApp.showAlert( + "This is alert message" + ) { + logsState.add( + "You have closed alert" + ) + } + } + }) { + Text("Alert") + } + + P() + Button({ + onClick { + webApp.requestWriteAccess() + } + }) { + Text("Request write access without callback") + } + Button({ + onClick { + webApp.requestWriteAccess { + logsState.add("Write access request result: $it") + } + } + }) { + Text("Request write access with callback") + } + + P() + Button({ + onClick { + webApp.requestContact() + } + }) { + Text("Request contact without callback") + } + Button({ + onClick { + webApp.requestContact { logsState.add("Contact request result: $it") } + } + }) { + Text("Request contact with callback") + } + P() + + logsState.forEach { + P { Text(it) } + } + } + window.onload = { val scope = CoroutineScope(Dispatchers.Default) runCatching { - scope.launchSafelyWithoutExceptions { - val response = client.post("$baseUrl/check") { - setBody( - Json.encodeToString( - WebAppDataWrapper.serializer(), - WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash) - ) - ) - } - val dataIsSafe = response.bodyAsText().toBoolean() - - document.body ?.log( - if (dataIsSafe) { - "Data is safe" - } else { - "Data is unsafe" - } - ) - - document.body ?.log( - webApp.initDataUnsafe.chat.toString() - ) - } - - document.body ?.appendElement("button") { - addEventListener("click", { - scope.launchSafelyWithoutExceptions { - handleResult({ "Clicked" }) { - client.post("${window.location.origin.removeSuffix("/")}/inline") { - parameter(webAppQueryIdField, it) - setBody(TextContent("Clicked", ContentType.Text.Plain)) - document.body ?.log(url.build().toString()) - }.coroutineContext.job.join() - } - } - }) - appendText("Answer in chat button") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - document.body ?.appendText("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}") - - document.body ?.appendElement("p", {}) - document.body ?.appendText("Alerts:") - - document.body ?.appendElement("button") { - addEventListener("click", { - webApp.showPopup( - PopupParams( - "It is sample title of default button", - "It is sample message of default button", - DefaultPopupButton("default", "Default button"), - OkPopupButton("ok"), - DestructivePopupButton("destructive", "Destructive button") - ) - ) { - document.body ?.log( - when (it) { - "default" -> "You have clicked default button in popup" - "ok" -> "You have clicked ok button in popup" - "destructive" -> "You have clicked destructive button in popup" - else -> "I can't imagine where you take button with id $it" - } - ) - } - }) - appendText("Popup") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("button") { - addEventListener("click", { - webApp.showAlert( - "This is alert message" - ) { - document.body ?.log( - "You have closed alert" - ) - } - }) - appendText("Alert") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - addEventListener("click", { webApp.requestWriteAccess() }) - appendText("Request write access without callback") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("button") { - addEventListener("click", { webApp.requestWriteAccess { document.body ?.log("Write access request result: $it") } }) - appendText("Request write access with callback") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - addEventListener("click", { webApp.requestContact() }) - appendText("Request contact without callback") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("button") { - addEventListener("click", { webApp.requestContact { document.body ?.log("Contact request result: $it") } }) - appendText("Request contact with callback") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - document.body ?.appendElement("button") { addEventListener("click", { webApp.showConfirm( diff --git a/WebApp/src/jsMain/resources/index.html b/WebApp/src/jsMain/resources/index.html index ab9d94d..7d253ac 100644 --- a/WebApp/src/jsMain/resources/index.html +++ b/WebApp/src/jsMain/resources/index.html @@ -10,6 +10,7 @@ Web App Example +
diff --git a/build.gradle b/build.gradle index 40b825e..ddbaec4 100644 --- a/build.gradle +++ b/build.gradle @@ -29,3 +29,8 @@ allprojects { maven { url "https://nexus.inmo.dev/repository/maven-releases/" } } } + +// Fix of https://youtrack.jetbrains.com/issue/KTOR-7912/Module-not-found-errors-when-executing-browserProductionWebpack-task-since-3.0.2 +rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin.class) { + rootProject.kotlinYarn.resolution("ws", "8.18.0") +} diff --git a/gradle.properties b/gradle.properties index 4aa009b..df2afb2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,3 +10,4 @@ telegram_bot_api_version=22.0.0 micro_utils_version=0.23.2 serialization_version=1.7.3 ktor_version=3.0.2 +compose_version=1.7.1 From d12e9aa0320efef904e847b11c0a4e72c85a4996 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 8 Dec 2024 10:14:42 +0600 Subject: [PATCH 4/7] rework to use compose --- WebApp/src/jsMain/kotlin/main.kt | 366 ++++++++++++++++--------------- 1 file changed, 187 insertions(+), 179 deletions(-) diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index 8bdeb7f..b0b396a 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -12,14 +12,16 @@ import io.ktor.client.request.* import io.ktor.client.statement.bodyAsText import io.ktor.http.* import io.ktor.http.content.TextContent -import kotlinx.browser.document import kotlinx.browser.window import kotlinx.coroutines.* import kotlinx.dom.appendElement import kotlinx.dom.appendText -import kotlinx.dom.clear import kotlinx.serialization.json.Json +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.css.Color as ComposeColor +import org.jetbrains.compose.web.css.backgroundColor import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.Input import org.jetbrains.compose.web.dom.P import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.renderComposable @@ -42,6 +44,10 @@ fun main() { val scope = rememberCoroutineScope() val isSafeState = remember { mutableStateOf(null) } val logsState = remember { mutableStateListOf() } + + Text(window.location.href) + P() + LaunchedEffect(baseUrl) { val response = client.post("$baseUrl/check") { setBody( @@ -53,15 +59,15 @@ fun main() { } val dataIsSafe = response.bodyAsText().toBoolean() - document.body ?.log( - if (dataIsSafe) { - "Data is safe" - } else { - "Data is unsafe" - } - ) + if (dataIsSafe) { + isSafeState.value = true + logsState.add("Data is safe") + } else { + isSafeState.value = false + logsState.add("Data is unsafe") + } - document.body ?.log( + logsState.add( webApp.initDataUnsafe.chat.toString() ) } @@ -169,174 +175,179 @@ fun main() { } P() - logsState.forEach { - P { Text(it) } - } - } - - window.onload = { - val scope = CoroutineScope(Dispatchers.Default) - runCatching { - - document.body ?.appendElement("button") { - addEventListener("click", { - webApp.showConfirm( - "This is confirm message" - ) { - document.body ?.log( - "You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm" - ) - } - }) - appendText("Confirm") - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - fun updateText() { - textContent = if (webApp.isClosingConfirmationEnabled) { - "Disable closing confirmation" - } else { - "Enable closing confirmation" - } - } - addEventListener("click", { - webApp.toggleClosingConfirmation() - updateText() - }) - updateText() - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - fun updateHeaderColor() { - val (r, g, b) = Random.nextUBytes(3) - val hex = Color.Hex(r, g, b) - webApp.setHeaderColor(hex) - (this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value - textContent = "Header color: ${webApp.headerColor ?.uppercase()} (click to change)" - } - addEventListener("click", { - updateHeaderColor() - }) - updateHeaderColor() - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - fun updateBackgroundColor() { - val (r, g, b) = Random.nextUBytes(3) - val hex = Color.Hex(r, g, b) - webApp.setBackgroundColor(hex) - (this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value - textContent = "Background color: ${webApp.backgroundColor ?.uppercase()} (click to change)" - } - addEventListener("click", { - updateBackgroundColor() - }) - updateBackgroundColor() - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - document.body ?.appendElement("button") { - fun updateBottomBarColor() { - val (r, g, b) = Random.nextUBytes(3) - val hex = Color.Hex(r, g, b) - webApp.setBottomBarColor(hex) - (this as? HTMLButtonElement) ?.style ?.backgroundColor = hex.value - textContent = "Bottom bar color: ${webApp.bottomBarColor ?.uppercase()} (click to change)" - } - addEventListener("click", { - updateBottomBarColor() - }) - updateBottomBarColor() - } ?: window.alert("Unable to load body") - - document.body ?.appendElement("p", {}) - - fun Element.updateCloudStorageContent() { - clear() - webApp.cloudStorage.getAll { - it.onSuccess { - document.body ?.log(it.toString()) - appendElement("label") { textContent = "Cloud storage" } - - appendElement("p", {}) - - it.forEach { (k, v) -> - appendElement("div") { - val kInput = appendElement("input", {}) as HTMLInputElement - val vInput = appendElement("input", {}) as HTMLInputElement - - kInput.value = k.key - vInput.value = v.value - - appendElement("button") { - addEventListener("click", { - if (k.key == kInput.value) { - webApp.cloudStorage.set(k.key, vInput.value) { - document.body ?.log(it.toString()) - this@updateCloudStorageContent.updateCloudStorageContent() - } - } else { - webApp.cloudStorage.remove(k.key) { - it.onSuccess { - webApp.cloudStorage.set(kInput.value, vInput.value) { - document.body ?.log(it.toString()) - this@updateCloudStorageContent.updateCloudStorageContent() - } - } - } - } - }) - this.textContent = "Save" - } - } - - appendElement("p", {}) - } - appendElement("label") { textContent = "Cloud storage: add new" } - - appendElement("p", {}) - - appendElement("div") { - val kInput = appendElement("input", {}) as HTMLInputElement - - appendElement("button") { - textContent = "Add key" - addEventListener("click", { - webApp.cloudStorage.set(kInput.value, kInput.value) { - document.body ?.log(it.toString()) - this@updateCloudStorageContent.updateCloudStorageContent() - } - }) - } - } - - appendElement("p", {}) - }.onFailure { - document.body ?.log(it.stackTraceToString()) - } + Button({ + onClick { + webApp.showConfirm( + "This is confirm message" + ) { + logsState.add( + "You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm" + ) } } - val cloudStorageContentDiv = document.body ?.appendElement("div") {} as HTMLDivElement + }) { + Text("Confirm") + } - document.body ?.appendElement("p", {}) + P() + val isClosingConfirmationEnabledState = remember { mutableStateOf(webApp.isClosingConfirmationEnabled) } + Button({ + onClick { + webApp.toggleClosingConfirmation() + isClosingConfirmationEnabledState.value = webApp.isClosingConfirmationEnabled + } + }) { + Text( + if (isClosingConfirmationEnabledState.value) { + "Disable closing confirmation" + } else { + "Enable closing confirmation" + } + ) + } + + P() + + val headerColor = remember { mutableStateOf(Color.Hex("#000000")) } + fun updateHeaderColor() { + val (r, g, b) = Random.nextUBytes(3) + headerColor.value = Color.Hex(r, g, b) + webApp.setHeaderColor(headerColor.value) + } + DisposableEffect(0) { + updateHeaderColor() + onDispose { } + } + Button({ + style { + backgroundColor(ComposeColor(headerColor.value.value)) + } + onClick { + updateHeaderColor() + } + }) { + key(headerColor.value) { + Text("Header color: ${webApp.headerColor ?.uppercase()} (click to change)") + } + } + + P() + + val backgroundColor = remember { mutableStateOf(Color.Hex("#000000")) } + fun updateBackgroundColor() { + val (r, g, b) = Random.nextUBytes(3) + backgroundColor.value = Color.Hex(r, g, b) + webApp.setBackgroundColor(backgroundColor.value) + } + DisposableEffect(0) { + updateBackgroundColor() + onDispose { } + } + Button({ + style { + backgroundColor(ComposeColor(backgroundColor.value.value)) + } + onClick { + updateBackgroundColor() + } + }) { + key(backgroundColor.value) { + Text("Background color: ${webApp.backgroundColor ?.uppercase()} (click to change)") + } + } + + P() + + val bottomBarColor = remember { mutableStateOf(Color.Hex("#000000")) } + fun updateBottomBarColor() { + val (r, g, b) = Random.nextUBytes(3) + bottomBarColor.value = Color.Hex(r, g, b) + webApp.setBottomBarColor(bottomBarColor.value) + } + DisposableEffect(0) { + updateBottomBarColor() + onDispose { } + } + Button({ + style { + backgroundColor(ComposeColor(bottomBarColor.value.value)) + } + onClick { + updateBottomBarColor() + } + }) { + key(bottomBarColor.value) { + Text("Bottom bar color: ${webApp.bottomBarColor ?.uppercase()} (click to change)") + } + } + + P() + + val storageTrigger = remember { mutableStateOf>>(emptyList()) } + fun updateCloudStorage() { + webApp.cloudStorage.getAll { + it.onSuccess { + storageTrigger.value = it.toList().sortedBy { it.first.key } + } + } + } + key(storageTrigger.value) { + storageTrigger.value.forEach { (key, value) -> + val keyState = remember { mutableStateOf(key.key) } + val valueState = remember { mutableStateOf(value.value) } + Input(InputType.Text) { + value(key.key) + onInput { keyState.value = it.value } + } + Input(InputType.Text) { + value(value.value) + onInput { valueState.value = it.value } + } + Button({ + onClick { + if (key.key != keyState.value) { + webApp.cloudStorage.remove(key) + } + webApp.cloudStorage.set(keyState.value, valueState.value) + updateCloudStorage() + } + }) { + Text("Save") + } + } + let { // new element adding + val keyState = remember { mutableStateOf("") } + val valueState = remember { mutableStateOf("") } + Input(InputType.Text) { + onInput { keyState.value = it.value } + } + Input(InputType.Text) { + onInput { valueState.value = it.value } + } + Button({ + onClick { + webApp.cloudStorage.set(keyState.value, valueState.value) + updateCloudStorage() + } + }) { + Text("Save") + } + } + } + + remember { webApp.apply { + onThemeChanged { - document.body ?.log("Theme changed: ${webApp.themeParams}") + logsState.add("Theme changed: ${webApp.themeParams}") } onViewportChanged { - document.body ?.log("Viewport changed: ${it}") + logsState.add("Viewport changed: ${it}") } backButton.apply { onClick { - document.body ?.log("Back button clicked") + logsState.add("Back button clicked") hapticFeedback.impactOccurred( HapticFeedbackStyle.Heavy ) @@ -346,7 +357,7 @@ fun main() { mainButton.apply { setText("Main button") onClick { - document.body ?.log("Main button clicked") + logsState.add("Main button clicked") hapticFeedback.notificationOccurred( HapticFeedbackType.Success ) @@ -356,7 +367,7 @@ fun main() { secondaryButton.apply { setText("Secondary button") onClick { - document.body ?.log("Secondary button clicked") + logsState.add("Secondary button clicked") hapticFeedback.notificationOccurred( HapticFeedbackType.Warning ) @@ -364,22 +375,19 @@ fun main() { show() } onSettingsButtonClicked { - document.body ?.log("Settings button clicked") + logsState.add("Settings button clicked") } onWriteAccessRequested { - document.body ?.log("Write access request result: $it") + logsState.add("Write access request result: $it") } onContactRequested { - document.body ?.log("Contact request result: $it") + logsState.add("Contact request result: $it") } } - webApp.ready() - document.body ?.appendElement("input", { - (this as HTMLInputElement).value = window.location.href - }) - cloudStorageContentDiv.updateCloudStorageContent() - }.onFailure { - window.alert(it.stackTraceToString()) + } + + logsState.forEach { + P { Text(it) } } } } From 2ab8ccbfdf3eb3188400f85a3c7fdd13ab1b526d Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 8 Dec 2024 10:20:37 +0600 Subject: [PATCH 5/7] small refactor in webapp --- WebApp/src/jsMain/kotlin/main.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index b0b396a..20a9956 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -79,7 +79,8 @@ fun main() { false -> "Data is unsafe" } ) - Text(webApp.initDataUnsafe.chat.toString()) + P() + Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}") Button({ onClick { From d294d0ef5906db741ebe006e086e8defb0314d74 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 8 Dec 2024 11:58:47 +0600 Subject: [PATCH 6/7] update events listeners --- WebApp/src/jsMain/kotlin/main.kt | 229 ++++++++++++++++++++++++++++++- 1 file changed, 222 insertions(+), 7 deletions(-) diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index 20a9956..e7bb2a9 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -2,10 +2,13 @@ import androidx.compose.runtime.* import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.webapps.* +import dev.inmo.tgbotapi.webapps.accelerometer.AccelerometerStartParams import dev.inmo.tgbotapi.webapps.cloud.* import dev.inmo.tgbotapi.webapps.events.* +import dev.inmo.tgbotapi.webapps.gyroscope.GyroscopeStartParams import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackStyle import dev.inmo.tgbotapi.webapps.haptic.HapticFeedbackType +import dev.inmo.tgbotapi.webapps.orientation.DeviceOrientationStartParams import dev.inmo.tgbotapi.webapps.popup.* import io.ktor.client.HttpClient import io.ktor.client.request.* @@ -18,11 +21,11 @@ import kotlinx.dom.appendElement import kotlinx.dom.appendText import kotlinx.serialization.json.Json import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.css.DisplayStyle import org.jetbrains.compose.web.css.Color as ComposeColor import org.jetbrains.compose.web.css.backgroundColor -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Input -import org.jetbrains.compose.web.dom.P +import org.jetbrains.compose.web.css.display +import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.renderComposable import org.w3c.dom.* @@ -43,10 +46,10 @@ fun main() { renderComposable("root") { val scope = rememberCoroutineScope() val isSafeState = remember { mutableStateOf(null) } - val logsState = remember { mutableStateListOf() } + val logsState = remember { mutableStateListOf() } - Text(window.location.href) - P() +// Text(window.location.href) +// P() LaunchedEffect(baseUrl) { val response = client.post("$baseUrl/check") { @@ -386,9 +389,221 @@ fun main() { } } } + P() + + let { // Accelerometer + val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) } + webApp.onAccelerometerStarted { enabledState.value = true } + webApp.onAccelerometerStopped { enabledState.value = false } + Button({ + onClick { + if (enabledState.value) { + webApp.accelerometer.stop { } + } else { + webApp.accelerometer.start(AccelerometerStartParams(200)) + } + } + }) { + Text("${if (enabledState.value) "Stop" else "Start"} accelerometer") + } + val xState = remember { mutableStateOf(webApp.accelerometer.x) } + val yState = remember { mutableStateOf(webApp.accelerometer.y) } + val zState = remember { mutableStateOf(webApp.accelerometer.z) } + fun updateValues() { + xState.value = webApp.accelerometer.x + yState.value = webApp.accelerometer.y + zState.value = webApp.accelerometer.z + } + remember { + updateValues() + } + + webApp.onAccelerometerChanged { + updateValues() + } + if (enabledState.value) { + P() + Text("x: ${xState.value}") + P() + Text("y: ${yState.value}") + P() + Text("z: ${zState.value}") + } + } + P() + + let { // Gyroscope + val enabledState = remember { mutableStateOf(webApp.gyroscope.isStarted) } + webApp.onGyroscopeStarted { enabledState.value = true } + webApp.onGyroscopeStopped { enabledState.value = false } + Button({ + onClick { + if (enabledState.value) { + webApp.gyroscope.stop { } + } else { + webApp.gyroscope.start(GyroscopeStartParams(200)) + } + } + }) { + Text("${if (enabledState.value) "Stop" else "Start"} gyroscope") + } + val xState = remember { mutableStateOf(webApp.gyroscope.x) } + val yState = remember { mutableStateOf(webApp.gyroscope.y) } + val zState = remember { mutableStateOf(webApp.gyroscope.z) } + fun updateValues() { + xState.value = webApp.gyroscope.x + yState.value = webApp.gyroscope.y + zState.value = webApp.gyroscope.z + } + remember { + updateValues() + } + + webApp.onGyroscopeChanged { + updateValues() + } + if (enabledState.value) { + P() + Text("x: ${xState.value}") + P() + Text("y: ${yState.value}") + P() + Text("z: ${zState.value}") + } + } + P() + + let { // DeviceOrientation + val enabledState = remember { mutableStateOf(webApp.deviceOrientation.isStarted) } + webApp.onDeviceOrientationStarted { enabledState.value = true } + webApp.onDeviceOrientationStopped { enabledState.value = false } + Button({ + onClick { + if (enabledState.value) { + webApp.deviceOrientation.stop { } + } else { + webApp.deviceOrientation.start(DeviceOrientationStartParams(200)) + } + } + }) { + Text("${if (enabledState.value) "Stop" else "Start"} deviceOrientation") + } + val alphaState = remember { mutableStateOf(webApp.deviceOrientation.alpha) } + val betaState = remember { mutableStateOf(webApp.deviceOrientation.beta) } + val gammaState = remember { mutableStateOf(webApp.deviceOrientation.gamma) } + fun updateValues() { + alphaState.value = webApp.deviceOrientation.alpha + betaState.value = webApp.deviceOrientation.beta + gammaState.value = webApp.deviceOrientation.gamma + } + remember { + updateValues() + } + + webApp.onDeviceOrientationChanged { + updateValues() + } + if (enabledState.value) { + P() + Text("alpha: ${alphaState.value}") + P() + Text("beta: ${betaState.value}") + P() + Text("gamma: ${gammaState.value}") + } + } + P() + + EventType.values().forEach { eventType -> + when (eventType) { + EventType.AccelerometerChanged -> webApp.onAccelerometerChanged { /*logsState.add("AccelerometerChanged") /* see accelerometer block */ */ } + EventType.AccelerometerFailed -> webApp.onAccelerometerFailed { + logsState.add(it.error) + } + EventType.AccelerometerStarted -> webApp.onAccelerometerStarted { logsState.add("AccelerometerStarted") } + EventType.AccelerometerStopped -> webApp.onAccelerometerStopped { logsState.add("AccelerometerStopped") } + EventType.Activated -> webApp.onActivated { logsState.add("Activated") } + EventType.BackButtonClicked -> webApp.onBackButtonClicked { logsState.add("BackButtonClicked") } + EventType.BiometricAuthRequested -> webApp.onBiometricAuthRequested { + logsState.add(it.isAuthenticated) + } + EventType.BiometricManagerUpdated -> webApp.onBiometricManagerUpdated { logsState.add("BiometricManagerUpdated") } + EventType.BiometricTokenUpdated -> webApp.onBiometricTokenUpdated { + logsState.add(it.isUpdated) + } + EventType.ClipboardTextReceived -> webApp.onClipboardTextReceived { + logsState.add(it.data) + } + EventType.ContactRequested -> webApp.onContactRequested { + logsState.add(it.status) + } + EventType.ContentSafeAreaChanged -> webApp.onContentSafeAreaChanged { logsState.add("ContentSafeAreaChanged") } + EventType.Deactivated -> webApp.onDeactivated { logsState.add("Deactivated") } + EventType.DeviceOrientationChanged -> webApp.onDeviceOrientationChanged { /*logsState.add("DeviceOrientationChanged")*//* see accelerometer block */ } + EventType.DeviceOrientationFailed -> webApp.onDeviceOrientationFailed { + logsState.add(it.error) + } + EventType.DeviceOrientationStarted -> webApp.onDeviceOrientationStarted { logsState.add("DeviceOrientationStarted") } + EventType.DeviceOrientationStopped -> webApp.onDeviceOrientationStopped { logsState.add("DeviceOrientationStopped") } + EventType.EmojiStatusAccessRequested -> webApp.onEmojiStatusAccessRequested { + logsState.add(it.status) + } + EventType.EmojiStatusFailed -> webApp.onEmojiStatusFailed { + logsState.add(it.error) + } + EventType.EmojiStatusSet -> webApp.onEmojiStatusSet { logsState.add("EmojiStatusSet") } + EventType.FileDownloadRequested -> webApp.onFileDownloadRequested { + logsState.add(it.status) + } + EventType.FullscreenChanged -> webApp.onFullscreenChanged { logsState.add("FullscreenChanged") } + EventType.FullscreenFailed -> webApp.onFullscreenFailed { + logsState.add(it.error) + } + EventType.GyroscopeChanged -> webApp.onGyroscopeChanged { /*logsState.add("GyroscopeChanged")*//* see gyroscope block */ } + EventType.GyroscopeFailed -> webApp.onGyroscopeFailed { + logsState.add(it.error) + } + EventType.GyroscopeStarted -> webApp.onGyroscopeStarted { logsState.add("GyroscopeStarted")/* see accelerometer block */ } + EventType.GyroscopeStopped -> webApp.onGyroscopeStopped { logsState.add("GyroscopeStopped") } + EventType.HomeScreenAdded -> webApp.onHomeScreenAdded { logsState.add("HomeScreenAdded") } + EventType.HomeScreenChecked -> webApp.onHomeScreenChecked { + logsState.add(it.status) + } + EventType.InvoiceClosed -> webApp.onInvoiceClosed { url, status -> + logsState.add(url) + logsState.add(status) + } + EventType.LocationManagerUpdated -> webApp.onLocationManagerUpdated { logsState.add("LocationManagerUpdated") } + EventType.LocationRequested -> webApp.onLocationRequested { + logsState.add(it.locationData) + } + EventType.MainButtonClicked -> webApp.onMainButtonClicked { logsState.add("MainButtonClicked") } + EventType.PopupClosed -> webApp.onPopupClosed { + logsState.add(it.buttonId) + } + EventType.QrTextReceived -> webApp.onQrTextReceived { + logsState.add(it.data) + } + EventType.SafeAreaChanged -> webApp.onSafeAreaChanged { logsState.add("SafeAreaChanged") } + EventType.ScanQrPopupClosed -> webApp.onScanQrPopupClosed { logsState.add("ScanQrPopupClosed") } + EventType.SecondaryButtonClicked -> webApp.onSecondaryButtonClicked { logsState.add("SecondaryButtonClicked") } + EventType.SettingsButtonClicked -> webApp.onSettingsButtonClicked { logsState.add("SettingsButtonClicked") } + EventType.ShareMessageFailed -> webApp.onShareMessageFailed { + logsState.add(it.error) + } + EventType.ShareMessageSent -> webApp.onShareMessageSent { logsState.add("ShareMessageSent") } + EventType.ThemeChanged -> webApp.onThemeChanged { logsState.add("ThemeChanged") } + EventType.ViewportChanged -> webApp.onViewportChanged { + logsState.add(it) + } + EventType.WriteAccessRequested -> webApp.onWriteAccessRequested { + logsState.add(it.status) + } + } + } logsState.forEach { - P { Text(it) } + P { Text(it.toString()) } } } } From 8cd75673f5e12a57361006e88a9ec1076b56682a Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 8 Dec 2024 13:17:39 +0600 Subject: [PATCH 7/7] add opportunity to set custom emoji status from webapp --- WebApp/build.gradle | 1 + .../commonMain/kotlin/CustomEmojiIdToSet.kt | 3 ++ WebApp/src/jsMain/kotlin/main.kt | 43 +++++++++++++++++++ WebApp/src/jvmMain/kotlin/WebAppServer.kt | 27 +++++++++--- 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt diff --git a/WebApp/build.gradle b/WebApp/build.gradle index 8700d47..8e04927 100644 --- a/WebApp/build.gradle +++ b/WebApp/build.gradle @@ -30,6 +30,7 @@ kotlin { dependencies { implementation kotlin('stdlib') implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" + implementation "dev.inmo:tgbotapi.core:$telegram_bot_api_version" implementation compose.runtime } } diff --git a/WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt b/WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt new file mode 100644 index 0000000..6c384a6 --- /dev/null +++ b/WebApp/src/commonMain/kotlin/CustomEmojiIdToSet.kt @@ -0,0 +1,3 @@ +import dev.inmo.tgbotapi.types.CustomEmojiId + +val CustomEmojiIdToSet = CustomEmojiId("5424939566278649034") diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index e7bb2a9..fa1c3d0 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -1,5 +1,7 @@ import androidx.compose.runtime.* import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.tgbotapi.types.CustomEmojiId +import dev.inmo.tgbotapi.types.userIdField import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.webapps.* import dev.inmo.tgbotapi.webapps.accelerometer.AccelerometerStartParams @@ -85,6 +87,47 @@ fun main() { P() Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}") + val emojiStatusAccessState = remember { mutableStateOf(false) } + webApp.onEmojiStatusAccessRequested { + emojiStatusAccessState.value = it.isAllowed + } + Button({ + onClick { + webApp.requestEmojiStatusAccess() + } + }) { + Text("Request custom emoji status access") + } + if (emojiStatusAccessState.value) { + Button({ + onClick { + webApp.setEmojiStatus(CustomEmojiIdToSet/* android custom emoji id */) + } + }) { + Text("Set custom emoji status") + } + val userId = webApp.initDataUnsafe.user ?.id + userId ?.let { userId -> + Button({ + onClick { + scope.launchSafelyWithoutExceptions { + client.post("$baseUrl/setCustomEmoji") { + parameter(userIdField, userId.long) + setBody( + Json.encodeToString( + WebAppDataWrapper.serializer(), + WebAppDataWrapper(webApp.initData, webApp.initDataUnsafe.hash) + ) + ) + } + } + } + }) { + Text("Set custom emoji status via bot") + } + } + } + Button({ onClick { scope.launchSafelyWithoutExceptions { diff --git a/WebApp/src/jvmMain/kotlin/WebAppServer.kt b/WebApp/src/jvmMain/kotlin/WebAppServer.kt index f067cd2..d6ed35f 100644 --- a/WebApp/src/jvmMain/kotlin/WebAppServer.kt +++ b/WebApp/src/jvmMain/kotlin/WebAppServer.kt @@ -6,6 +6,7 @@ import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.send +import dev.inmo.tgbotapi.extensions.api.set.setUserEmojiStatus import dev.inmo.tgbotapi.extensions.api.telegramBot import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery @@ -16,12 +17,9 @@ import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.webAppButton import dev.inmo.tgbotapi.requests.answers.InlineQueryResultsButton -import dev.inmo.tgbotapi.types.BotCommand +import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent -import dev.inmo.tgbotapi.types.InlineQueryId -import dev.inmo.tgbotapi.types.LinkPreviewOptions -import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.types.webapps.WebAppInfo import dev.inmo.tgbotapi.utils.* import io.ktor.http.* @@ -30,7 +28,6 @@ import io.ktor.server.http.content.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.Json import java.io.File @@ -105,6 +102,26 @@ suspend fun main(vararg args: String) { call.respond(HttpStatusCode.OK, isSafe.toString()) } + post("setCustomEmoji") { + val requestBody = call.receiveText() + val webAppCheckData = Json.decodeFromString(WebAppDataWrapper.serializer(), requestBody) + + val isSafe = telegramBotAPIUrlsKeeper.checkWebAppData(webAppCheckData.data, webAppCheckData.hash) + val rawUserId = call.parameters[userIdField] ?.toLongOrNull() ?.let(::RawChatId) ?: error("$userIdField should be presented as long value") + + val set = if (isSafe) { + runCatching { + bot.setUserEmojiStatus( + UserId(rawUserId), + CustomEmojiIdToSet + ) + }.getOrElse { false } + } else { + false + } + + call.respond(HttpStatusCode.OK, set.toString()) + } } }.start(false)