8 Commits

Author SHA1 Message Date
83af44002c chore: restructure and update dependencies
Some checks failed
Build and Release / build (push) Has been cancelled
2026-05-24 09:32:25 +07:00
44acbfa6b7 chore: fix app crash when create new room via QR (#3)
Reviewed-on: #3
2026-05-24 01:30:48 +00:00
2d25cb36bd chore: bump version
Some checks failed
Build and Release / build (push) Has been cancelled
2026-05-23 20:40:56 +07:00
439cf60b66 chore: fix proguard issues (#2)
Reviewed-on: #2
2026-05-23 13:40:05 +00:00
f1f603525b chore: fix apk signing 2026-05-23 13:45:23 +07:00
5c7027e559 chore: fix build 2026-05-23 13:31:48 +07:00
4bcb2518b7 chore: fix ci 2026-05-23 13:04:27 +07:00
25852e08a9 chore: fix github ci 2026-05-23 12:43:54 +07:00
10 changed files with 104 additions and 82 deletions

View File

@@ -1,25 +1,14 @@
name: Build and Release name: Build and Release
on: on:
pull_request:
branches: [ "master" ]
workflow_dispatch: workflow_dispatch:
inputs: push:
build_type: tags:
description: 'Select build type' - "v*"
required: true
default: 'release'
type: choice
options:
- release
- alpha
- beta
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
BUILD_TYPE: ${{ github.event.inputs.build_type || 'release' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -34,13 +23,23 @@ jobs:
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Decode Keystore
run: |
# Decodes the Base64 string from secrets to a physical file
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > composeApp/release.jks
- name: Build APK - name: Build APK
run: ./gradlew :composeApp:assemble${{ env.BUILD_TYPE }} run: ./gradlew :composeApp:assembleRelease
env:
KEYSTORE_PATH: release.jks
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
- name: Gitea Release - name: Gitea Release
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
files: "composeApp/build/outputs/apk/${{ env.BUILD_TYPE }}/*.apk" files: "composeApp/build/outputs/apk/release/*.apk"
server_url: "https://git.reya.su/" server_url: "https://git.reya.su/"
repository: "reya/coop-mobile" repository: "reya/coop-mobile"
token: ${{ secrets.GITEA_TOKEN }} token: ${{ secrets.GITEA_TOKEN }}

View File

@@ -1,5 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
@@ -20,15 +19,12 @@ kotlin {
androidMain.dependencies { androidMain.dependencies {
implementation(libs.compose.uiToolingPreview) implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation("androidx.navigation:navigation-compose:2.8.8") implementation(libs.androidx.navigation.compose)
implementation("androidx.datastore:datastore-preferences:1.2.1") implementation(libs.androidx.lifecycle.process)
implementation("androidx.datastore:datastore-preferences-core:1.2.1")
implementation("org.jetbrains.compose.material3:material3:1.11.0-alpha07")
implementation("io.coil-kt.coil3:coil-compose:3.4.0") implementation("io.coil-kt.coil3:coil-compose:3.4.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
implementation("su.reya:nostr-sdk-kmp:0.2.3") implementation("su.reya:nostr-sdk-kmp:0.2.3")
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0") implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
implementation("androidx.lifecycle:lifecycle-process:2.8.0")
implementation("io.github.alexzhirkevich:qrose:1.1.2") implementation("io.github.alexzhirkevich:qrose:1.1.2")
} }
commonMain.dependencies { commonMain.dependencies {
@@ -40,6 +36,8 @@ kotlin {
implementation(libs.compose.uiToolingPreview) implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.datastore)
implementation(projects.shared) implementation(projects.shared)
} }
commonTest.dependencies { commonTest.dependencies {
@@ -48,23 +46,19 @@ kotlin {
} }
} }
val localProperties = Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) {
load(file.inputStream())
}
}
android { android {
namespace = "su.reya.coop" namespace = "su.reya.coop"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
base.archivesName.set("coop")
signingConfigs { signingConfigs {
create("release") { create("release") {
storeFile = localProperties.getProperty("keystore.path")?.let { file(it) } val path = System.getenv("KEYSTORE_PATH")
storePassword = localProperties.getProperty("keystore.password") storeFile = path?.let { file(it) }
keyAlias = localProperties.getProperty("key.alias") storePassword = System.getenv("KEYSTORE_PASSWORD")
keyPassword = localProperties.getProperty("key.password") keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
} }
} }
defaultConfig { defaultConfig {
@@ -72,7 +66,7 @@ android {
minSdk = libs.versions.android.minSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1 versionCode = 1
versionName = "0.1.0" versionName = "0.1.1"
} }
packaging { packaging {
resources { resources {
@@ -84,24 +78,11 @@ android {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt") getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
) )
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
} }
create("beta") {
initWith(getByName("release"))
applicationIdSuffix = ".beta"
versionNameSuffix = "-beta"
manifestPlaceholders["appName"] = "Coop Beta"
signingConfig = signingConfigs.getByName("release")
}
create("alpha") {
initWith(getByName("release"))
applicationIdSuffix = ".alpha"
versionNameSuffix = "-alpha"
manifestPlaceholders["appName"] = "Coop Alpha"
signingConfig = signingConfigs.getByName("release")
}
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

11
composeApp/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,11 @@
-dontwarn com.sun.jna.**
-keep class com.sun.jna.** { *; }
-keep class * extends com.sun.jna.Structure { *; }
-keep class * extends com.sun.jna.Library { *; }
-keep class * extends com.sun.jna.Callback { *; }
-keep class rust.nostr.sdk.** { *; }
-keep class su.reya.nostr.** { *; }
-keepattributes Signature, InnerClasses, EnclosingMethod, RuntimeVisibleAnnotations

View File

@@ -15,8 +15,10 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
@@ -100,6 +102,8 @@ fun App() {
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography(),
motionScheme = MotionScheme.expressive(),
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalNostrViewModel provides viewModel, LocalNostrViewModel provides viewModel,

View File

@@ -68,8 +68,19 @@ fun ChatScreen(
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val viewModel = LocalNostrViewModel.current val viewModel = LocalNostrViewModel.current
val room = viewModel.getChatRoom(id)
val listState = rememberLazyListState() val listState = rememberLazyListState()
val chatRooms by viewModel.chatRooms.collectAsState()
val room = remember(chatRooms, id) { chatRooms.firstOrNull { it.id == id } }
if (room == null) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LoadingIndicator()
}
return
}
val displayName by remember(room) { room.displayNameFlow(viewModel) }.collectAsState("Loading...") val displayName by remember(room) { room.displayNameFlow(viewModel) }.collectAsState("Loading...")
val picture by remember(room) { room.pictureFlow(viewModel) }.collectAsState(null) val picture by remember(room) { room.pictureFlow(viewModel) }.collectAsState(null)

View File

@@ -58,7 +58,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
@@ -85,7 +84,6 @@ fun HomeScreen(
onOpenChat: (Long) -> Unit, onOpenChat: (Long) -> Unit,
onNewChat: () -> Unit, onNewChat: () -> Unit,
) { ) {
val clipboard = LocalClipboard.current
val navController = LocalNavController.current val navController = LocalNavController.current
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val viewModel = LocalNostrViewModel.current val viewModel = LocalNostrViewModel.current
@@ -112,15 +110,21 @@ fun HomeScreen(
?: remember { mutableStateOf(null) } ?: remember { mutableStateOf(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (qrResult == null) {
viewModel.getChatRooms() viewModel.getChatRooms()
} }
}
LaunchedEffect(qrResult) { LaunchedEffect(qrResult) {
qrResult?.let { result -> qrResult?.let { result ->
runCatching { PublicKey.parse(result) } runCatching { PublicKey.parse(result) }
.onSuccess { pubkey -> .onSuccess { pubkey ->
try {
val roomId = viewModel.createChatRoom(listOf(pubkey)) val roomId = viewModel.createChatRoom(listOf(pubkey))
navController.navigate(Screen.Chat(roomId)) navController.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
}
} }
.onFailure { e -> println("Failed to parse QR: ${e.message}") } .onFailure { e -> println("Failed to parse QR: ${e.message}") }

View File

@@ -76,7 +76,6 @@ fun ScanScreen(
ScannerWithPermissions( ScannerWithPermissions(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onScanned = { onScanned = {
println("Scanned: $it");
onResult(it) onResult(it)
true true
}, },

View File

@@ -8,16 +8,19 @@ androidx-appcompat = "1.7.1"
androidx-core = "1.18.0" androidx-core = "1.18.0"
androidx-espresso = "3.7.0" androidx-espresso = "3.7.0"
androidx-lifecycle = "2.10.0" androidx-lifecycle = "2.10.0"
androidx-navigation = "2.8.8" androidx-navigation = "2.9.8"
androidx-testExt = "1.3.0" androidx-testExt = "1.3.0"
composeMultiplatform = "1.10.3" composeMultiplatform = "1.11.0"
datastorePreferences = "1.2.1"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.3.20" kotlin = "2.3.21"
kotlinx-serialization = "1.8.0" kotlinx-serialization = "1.11.0"
material3 = "1.10.0-alpha05" material3 = "1.11.0-alpha07"
ktor = "3.4.3" ktor = "3.5.0"
[libraries] [libraries]
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastorePreferences" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" } junit = { module = "junit:junit", version.ref = "junit" }
@@ -31,6 +34,7 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } 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-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" } androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx-lifecycle" }
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", 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-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }

View File

@@ -25,15 +25,15 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
implementation("su.reya:nostr-sdk-kmp:0.2.3")
implementation("com.squareup.okio:okio:3.16.2")
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
implementation(libs.ktor.client.websockets) implementation(libs.ktor.client.websockets)
implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
implementation("su.reya:nostr-sdk-kmp:0.2.3")
implementation("com.squareup.okio:okio:3.16.2")
} }
androidMain.dependencies { androidMain.dependencies {
implementation(libs.ktor.client.okhttp) implementation(libs.ktor.client.okhttp)

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
@@ -406,6 +407,7 @@ class NostrViewModel(
} }
fun createChatRoom(to: List<PublicKey>): Long { fun createChatRoom(to: List<PublicKey>): Long {
try {
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in") if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required") if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
@@ -417,9 +419,14 @@ class NostrViewModel(
// Create a room from the rumor event // Create a room from the rumor event
val room = Room.new(rumor, nostr.signer.currentUser!!) val room = Room.new(rumor, nostr.signer.currentUser!!)
_chatRooms.value += room _chatRooms.update { currentRooms ->
currentRooms + room
}
return room.id return room.id
} catch (e: Exception) {
throw IllegalArgumentException("Failed to create room: ${e.message}")
}
} }
fun getChatRoom(id: Long): Room { fun getChatRoom(id: Long): Room {
@@ -429,10 +436,12 @@ class NostrViewModel(
fun getChatRooms() { fun getChatRooms() {
viewModelScope.launch { viewModelScope.launch {
try { val rooms = nostr.getChatRooms() ?: emptySet()
_chatRooms.value = nostr.getChatRooms() ?: emptySet() _chatRooms.update { currentRooms ->
} catch (e: Exception) { val virtualRooms = currentRooms.filter { local ->
showError("Error: ${e.message}") rooms.none { db -> db.id == local.id }
}
rooms + virtualRooms
} }
} }
} }