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
on:
pull_request:
branches: [ "master" ]
workflow_dispatch:
inputs:
build_type:
description: 'Select build type'
required: true
default: 'release'
type: choice
options:
- release
- alpha
- beta
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
env:
BUILD_TYPE: ${{ github.event.inputs.build_type || 'release' }}
steps:
- uses: actions/checkout@v4
@@ -34,13 +23,23 @@ jobs:
- name: Grant execute permission for 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
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
uses: akkuman/gitea-release-action@v1
with:
files: "composeApp/build/outputs/apk/${{ env.BUILD_TYPE }}/*.apk"
files: "composeApp/build/outputs/apk/release/*.apk"
server_url: "https://git.reya.su/"
repository: "reya/coop-mobile"
token: ${{ secrets.GITEA_TOKEN }}

View File

@@ -1,5 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins {
alias(libs.plugins.kotlinMultiplatform)
@@ -20,15 +19,12 @@ kotlin {
androidMain.dependencies {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
implementation("androidx.navigation:navigation-compose:2.8.8")
implementation("androidx.datastore:datastore-preferences:1.2.1")
implementation("androidx.datastore:datastore-preferences-core:1.2.1")
implementation("org.jetbrains.compose.material3:material3:1.11.0-alpha07")
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.process)
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
implementation("su.reya:nostr-sdk-kmp:0.2.3")
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")
}
commonMain.dependencies {
@@ -40,6 +36,8 @@ kotlin {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.datastore)
implementation(projects.shared)
}
commonTest.dependencies {
@@ -48,23 +46,19 @@ kotlin {
}
}
val localProperties = Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) {
load(file.inputStream())
}
}
android {
namespace = "su.reya.coop"
compileSdk = libs.versions.android.compileSdk.get().toInt()
base.archivesName.set("coop")
signingConfigs {
create("release") {
storeFile = localProperties.getProperty("keystore.path")?.let { file(it) }
storePassword = localProperties.getProperty("keystore.password")
keyAlias = localProperties.getProperty("key.alias")
keyPassword = localProperties.getProperty("key.password")
val path = System.getenv("KEYSTORE_PATH")
storeFile = path?.let { file(it) }
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
}
}
defaultConfig {
@@ -72,7 +66,7 @@ android {
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "0.1.0"
versionName = "0.1.1"
}
packaging {
resources {
@@ -84,24 +78,11 @@ android {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt")
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
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 {
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.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@@ -100,6 +102,8 @@ fun App() {
MaterialExpressiveTheme(
colorScheme = colorScheme,
typography = Typography(),
motionScheme = MotionScheme.expressive(),
) {
CompositionLocalProvider(
LocalNostrViewModel provides viewModel,

View File

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

View File

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

View File

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

View File

@@ -25,15 +25,15 @@ kotlin {
sourceSets {
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.websockets)
implementation(libs.ktor.client.content.negotiation)
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 {
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.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
@@ -406,20 +407,26 @@ class NostrViewModel(
}
fun createChatRoom(to: List<PublicKey>): Long {
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
try {
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
// Construct the rumor event
val rumor = EventBuilder
.privateMsgRumor(to.first(), "")
.tags(to.map { Tag.publicKey(it) })
.build(nostr.signer.currentUser!!)
// Construct the rumor event
val rumor = EventBuilder
.privateMsgRumor(to.first(), "")
.tags(to.map { Tag.publicKey(it) })
.build(nostr.signer.currentUser!!)
// Create a room from the rumor event
val room = Room.new(rumor, nostr.signer.currentUser!!)
_chatRooms.value += room
// Create a room from the rumor event
val room = Room.new(rumor, nostr.signer.currentUser!!)
_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 {
@@ -429,10 +436,12 @@ class NostrViewModel(
fun getChatRooms() {
viewModelScope.launch {
try {
_chatRooms.value = nostr.getChatRooms() ?: emptySet()
} catch (e: Exception) {
showError("Error: ${e.message}")
val rooms = nostr.getChatRooms() ?: emptySet()
_chatRooms.update { currentRooms ->
val virtualRooms = currentRooms.filter { local ->
rooms.none { db -> db.id == local.id }
}
rooms + virtualRooms
}
}
}