From dd3b31d0b243407da324a9df9695307788892e6c Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Sat, 21 Feb 2026 21:21:57 +0300 Subject: [PATCH] wip: Migrate to Trixnity --- .../java/ru/risdeveau/pixeldragon/Common.kt | 26 +--- .../pixeldragon/ui/activity/MainActivity.kt | 19 ++- .../ru/risdeveau/pixeldragon/ui/item/Image.kt | 127 +++++++++++++++++- .../risdeveau/pixeldragon/ui/layout/Rooms.kt | 126 ++++++++++++++++- gradle/libs.versions.toml | 7 +- 5 files changed, 269 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt index 73ab520..c190b00 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/Common.kt @@ -12,9 +12,6 @@ import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import net.folivo.trixnity.client.MatrixClient -//import ru.risdeveau.pixeldragon.api.MatrixSyncService -//import ru.risdeveau.pixeldragon.api.getMe -import splitties.preferences.Preferences val webClient = HttpClient { install(Logging) { @@ -29,25 +26,4 @@ val webClient = HttpClient { install(HttpCache) } -var client: MatrixClient? = null - -object AccountData : Preferences("system_parameters") { - var token by stringOrNullPref("token", null) - var userId by stringOrNullPref("user_id", null) - var homeserver by stringOrNullPref("homeserver", null) - var syncLastBatch by stringOrNullPref("next_batch", null) - var filter by stringOrNullPref("filter", null) -} - -//val syncService = MatrixSyncService() - -//suspend fun initCheck(): Boolean { -// Log.d("initCheck", "checking...") -// -// token = AccountData.token ?: return false -// homeserver = AccountData.homeserver ?: return false -// -// baseUrl = "$homeserver/_matrix/client/v3" -// -// return getMe() != null -//} +var client: MatrixClient? = null \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt index ed37a2d..559a8b8 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt @@ -30,9 +30,13 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import net.folivo.trixnity.client.MatrixClient import net.folivo.trixnity.client.fromStore import ru.risdeveau.pixeldragon.client +import ru.risdeveau.pixeldragon.ui.layout.RoomList import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme import ru.risdeveau.pixeldragon.util.getMediaStore import ru.risdeveau.pixeldragon.util.getRoomStore @@ -70,9 +74,10 @@ class MainActivity : ComponentActivity() { ) { innerPadding -> val navController = rememberNavController() - NavHost(navController = navController, startDestination = "none") { - composable("none") { } - composable("rooms") { /*RoomList(Modifier.padding(innerPadding), navController)*/ } + NavHost(navController = navController, startDestination = "rooms") { + composable("rooms") { + RoomList(Modifier.padding(innerPadding), navController) + } composable( "room/{rid}", arguments = listOf(navArgument("rid") { type = NavType.StringType }) @@ -109,9 +114,17 @@ class MainActivity : ComponentActivity() { } Log.i("MainActivity", "Log in as ${client!!.userId}") + client!!.startSync() isClientReady = true } } } } + + override fun onDestroy() { + super.onDestroy() + CoroutineScope(Dispatchers.Main).launch { + client?.stopSync() + } + } } diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/item/Image.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/item/Image.kt index 8828b5a..a95b8b5 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/item/Image.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/item/Image.kt @@ -1,4 +1,129 @@ /* * Created by sweetbread * Copyright (c) 2026. All rights reserved. - */ \ No newline at end of file + */ + +package ru.risdeveau.pixeldragon.ui.item + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import coil3.compose.rememberAsyncImagePainter +import coil3.request.ImageRequest +import kotlinx.coroutines.flow.MutableStateFlow +import net.folivo.trixnity.client.media +import net.folivo.trixnity.clientserverapi.model.media.FileTransferProgress +import ru.risdeveau.pixeldragon.client + +enum class ImageLoadState { + Loading, Success, Error +} + +@Composable +fun MXCImage( + mxcUrl: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, + contentDescription: String = "", + showProgress: Boolean = true +) { + val context = LocalContext.current + var imageLoadState by remember { mutableStateOf(ImageLoadState.Loading) } + var imageBytes by remember { mutableStateOf(null) } + + val progressFlow = remember { MutableStateFlow(null) } + + LaunchedEffect(mxcUrl) { + if (mxcUrl.isBlank()) { + imageLoadState = ImageLoadState.Error + return@LaunchedEffect + } + imageLoadState = ImageLoadState.Loading + progressFlow.value = null + + val result = client!!.media.getMedia( + uri = mxcUrl, + progress = progressFlow.takeIf { showProgress }, + saveToCache = true + ) + + imageLoadState = result.fold( + onSuccess = { media -> + val bytes = media.toByteArray() + if (bytes != null) { + imageBytes = bytes + ImageLoadState.Success + } else { + ImageLoadState.Error + } + }, + onFailure = { + ImageLoadState.Error + } + ) + } + + val progress by progressFlow.collectAsState() + + val showProgressIndicator = showProgress && + imageLoadState == ImageLoadState.Loading && + progress != null + + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(context) + .data(imageBytes) + .build() + ) + + Box(modifier = modifier, contentAlignment = Alignment.Center) { + if (imageBytes != null) { + Image( + painter = painter, + contentDescription = contentDescription, + contentScale = contentScale, + modifier = Modifier.fillMaxSize() + ) + } + + when { + showProgressIndicator -> { + progress?.let { p -> + val percent = p.total?.let { p.transferred.toFloat() / it } + if (percent == null) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } else { + CircularProgressIndicator( + progress = percent, + modifier = Modifier.align(Alignment.Center) + ) + } + } ?: CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + imageLoadState == ImageLoadState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + imageLoadState == ImageLoadState.Error -> { + Icon( + Icons.Outlined.Warning, + contentDescription = "Error", + modifier = Modifier.align(Alignment.Center) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt index 8828b5a..f61eaea 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/layout/Rooms.kt @@ -1,4 +1,128 @@ /* * Created by sweetbread * Copyright (c) 2026. All rights reserved. - */ \ No newline at end of file + */ + +package ru.risdeveau.pixeldragon.ui.layout + +import android.annotation.SuppressLint +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import kotlinx.coroutines.flow.map +import net.folivo.trixnity.client.flattenValues +import net.folivo.trixnity.client.room +import net.folivo.trixnity.client.store.Room +import net.folivo.trixnity.client.store.type +import net.folivo.trixnity.core.model.events.m.room.CreateEventContent +import ru.risdeveau.pixeldragon.client +import ru.risdeveau.pixeldragon.ui.item.MXCImage + +@SuppressLint("FlowOperatorInvokedInComposition") +@Composable +fun RoomList(modifier: Modifier = Modifier, navController: NavController) { + val rooms by client!!.room.getAll().flattenValues().map { it.toList() }.collectAsState(initial = emptyList()) + + val listState = rememberLazyListState() + +// if (itemState.scrollToTop) { +// LaunchedEffect(coroutineScope) { +// Log.e("TAG", "TopCoinsScreen: scrollToTop" ) +// listState.scrollToItem(0) +// } +// } + +// LaunchedEffect(Unit) { +// list = withContext(Dispatchers.IO) { Room.getJoined() } +// } + + LazyColumn(modifier = modifier, state = listState) { + items(rooms) { room -> + RoomItem(room = room, navController = navController ) + } + + item { + if (rooms.isEmpty()) { + Text("You have no rooms") + } + } + } +} + +@Composable +fun RoomItem(modifier: Modifier = Modifier, room: Room, navController: NavController) { + val name = room.name + val isSpace = room.type == CreateEventContent.RoomType.Space + + + Row( + modifier + .padding(8.dp) + .height((52 + 8 * 2).dp) + .fillMaxWidth() + .background( + color = + if (isSpace) + MaterialTheme.colorScheme.tertiary + else + MaterialTheme.colorScheme.background + ) + .clip(RoundedCornerShape(12.dp)) + .clickable { + if (isSpace) + navController.navigate("space/${room.roomId}") + else + navController.navigate("room/${room.roomId}") + } + .padding(8.dp) + ) { + Log.v("RoomItem", room.avatarUrl.toString()) + room.avatarUrl?.let { mxc -> + MXCImage( + mxc, + modifier = Modifier + .padding(end = 4.dp) + .height(52.dp) + .width(52.dp) + .let { + if (isSpace) + it.clip(RoundedCornerShape(12.dp)) + else + it.clip(CircleShape) + }, + ContentScale.Crop + ) + } + Column { + (name?.explicitName ?: name?.heroes?.firstNotNullOf {it.localpart})?.let { + Text( + it, + maxLines = 1, + color = MaterialTheme.colorScheme.primary, + fontSize = MaterialTheme.typography.titleLarge.fontSize + ) + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b23bd0a..bab4b31 100755 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,9 +15,7 @@ composeBom = "2025.02.00" navigationCompose = "2.8.8" room = "2.6.1" splittiesFunPackAndroidBase = "3.0.0" -trixnityMessenger = "3.8.11" -trixnityClient = "4.15.0" -ksp = "2.0.21-1.0.27" +trixnityClient = "4.22.7" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -27,7 +25,6 @@ androidx-navigation-dynamic-features-fragment = { module = "androidx.navigation: androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "navigationCompose" } androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigationCompose" } androidx-navigation-ui = { module = "androidx.navigation:navigation-ui", version.ref = "navigationCompose" } -androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } @@ -46,7 +43,6 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } @@ -54,7 +50,6 @@ splitties-base = { module = "com.louiscad.splitties:splitties-fun-pack-android-b trixnity-client = { module = "net.folivo:trixnity-client", version.ref = "trixnityClient" } trixnity-client-media-okio = { module = "net.folivo:trixnity-client-media-okio", version.ref = "trixnityClient" } trixnity-client-repository-room = { module = "net.folivo:trixnity-client-repository-room", version.ref = "trixnityClient" } -trixnity-messenger = { module = "de.connect2x:trixnity-messenger", version.ref = "trixnityMessenger" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }