6 Commits

29 changed files with 249 additions and 120 deletions
+2 -7
View File
@@ -62,8 +62,8 @@ extensions.configure<ApplicationExtension> {
applicationId = "ru.fromchat" applicationId = "ru.fromchat"
minSdk = 24 minSdk = 24
targetSdk = 37 targetSdk = 37
versionCode = 1 versionCode = rootProject.extra["versionCode"] as Int
versionName = "1.0" versionName = rootProject.extra["versionName"] as String
ndk { ndk {
abiFilters += listOf("arm64-v8a", "x86_64") abiFilters += listOf("arm64-v8a", "x86_64")
@@ -166,9 +166,4 @@ dependencies {
implementation(project(":app:shared")) implementation(project(":app:shared"))
implementation(project(":utils:shared")) implementation(project(":utils:shared"))
testImplementation("junit:junit:4.13.2")
testImplementation(libs.androidx.compose.material3)
testImplementation("androidx.graphics:graphics-shapes:1.0.1")
testImplementation("org.robolectric:robolectric:4.14.1")
} }
+3
View File
@@ -22,6 +22,9 @@
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_fromchat" /> android:resource="@drawable/ic_stat_fromchat" />
<meta-data
android:name="firebase_messaging_installation_id_enabled"
android:value="true" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -9,8 +9,8 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.fromchat.Logger import ru.fromchat.Logger
import ru.fromchat.api.ApiClient import ru.fromchat.api.ApiClient
import ru.fromchat.notifications.NotificationHelper
import ru.fromchat.api.uploadPendingFcmTokenIfAvailable import ru.fromchat.api.uploadPendingFcmTokenIfAvailable
import ru.fromchat.notifications.NotificationHelper
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
class FromChatFirebaseMessagingService : FirebaseMessagingService() { class FromChatFirebaseMessagingService : FirebaseMessagingService() {
@@ -65,18 +65,16 @@ class FromChatFirebaseMessagingService : FirebaseMessagingService() {
} }
} }
override fun onNewToken(token: String) { override fun onRegistered(installationId: String) {
Logger.i("FromChatFCM", "onNewToken received (...${token.takeLast(8)})") Logger.i("FromChatFCM", "onRegistered received (...${installationId.takeLast(8)})")
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
settings.putString("pending_fcm_token", token) settings.putString("pending_fcm_token", installationId)
uploadPendingFcmTokenIfAvailable() uploadPendingFcmTokenIfAvailable()
Logger.i("FromChatFCM", "FCM token queued or uploaded for this app instance") Logger.i("FromChatFCM", "FCM installation id queued or uploaded for this app instance")
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("FromChatFCM", "onNewToken upload error: ${e.message}", e) Logger.e("FromChatFCM", "onRegistered upload error: ${e.message}", e)
} }
super.onNewToken(token)
} }
} }
} }
+62 -1
View File
@@ -1,3 +1,12 @@
@file:Suppress("TaskMissingDescription")
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
plugins { plugins {
alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.compose.multiplatform) alias(libs.plugins.compose.multiplatform)
@@ -7,6 +16,54 @@ plugins {
alias(libs.plugins.sqldelight) alias(libs.plugins.sqldelight)
} }
abstract class GenerateAppBuildInfoTask : DefaultTask() {
@get:Input
abstract val versionName: Property<String>
@get:Input
abstract val versionCode: Property<Int>
@get:Input
abstract val debugBuild: Property<Boolean>
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun generate() {
val outRoot = outputDirectory.get().asFile
check(outRoot.invariantSeparatorsPath.contains("/generated/")) {
"AppBuildInfo must be written under a generated/ directory, got: $outRoot"
}
outRoot.deleteRecursively()
outRoot.resolve("ru/fromchat").apply { mkdirs() }.resolve("AppBuildInfo.kt").writeText(
"""
|package ru.fromchat
|
|/** Injected by Gradle into generated sources (not under src/). */
|object AppBuildInfo {
| const val version = "${versionName.get()}"
| const val versionCode = ${versionCode.get()}
| const val isDebug = ${debugBuild.get()}
|}
|
""".trimMargin()
)
}
}
val generateAppBuildInfo = tasks.register<GenerateAppBuildInfoTask>("generateAppBuildInfo") {
versionName.set(rootProject.extra["versionName"] as String)
versionCode.set(rootProject.extra["versionCode"] as Int)
debugBuild.set(
gradle.startParameter.taskNames.let { names ->
!names.any { it.contains("Release", ignoreCase = true) } ||
names.any { it.contains("Debug", ignoreCase = true) }
},
)
outputDirectory.set(layout.buildDirectory.dir("generated/sources/appBuildInfo/kotlin"))
}
kotlin { kotlin {
android { android {
namespace = "ru.fromchat.shared" namespace = "ru.fromchat.shared"
@@ -21,7 +78,6 @@ kotlin {
listOf( listOf(
iosArm64(), iosArm64(),
iosSimulatorArm64(), iosSimulatorArm64(),
iosX64(),
).forEach { iosTarget -> ).forEach { iosTarget ->
iosTarget.binaries.framework { iosTarget.binaries.framework {
baseName = "ComposeApp" baseName = "ComposeApp"
@@ -37,6 +93,10 @@ kotlin {
} }
} }
commonMain {
kotlin.srcDir(generateAppBuildInfo.map { it.outputDirectory })
}
commonMain.dependencies { commonMain.dependencies {
implementation(libs.compose.runtime) implementation(libs.compose.runtime)
implementation(libs.compose.foundation) implementation(libs.compose.foundation)
@@ -123,6 +183,7 @@ compose.resources {
tasks.matching { it.name == "compileAndroidMain" || it.name == "compileKotlinIosArm64" }.configureEach { tasks.matching { it.name == "compileAndroidMain" || it.name == "compileKotlinIosArm64" }.configureEach {
dependsOn("generateResourceAccessorsForCommonMain") dependsOn("generateResourceAccessorsForCommonMain")
dependsOn(generateAppBuildInfo)
} }
tasks.register("generateResourceAccessors") { tasks.register("generateResourceAccessors") {
@@ -1,6 +1,7 @@
package ru.fromchat.api package ru.fromchat.api
import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Task
import com.google.firebase.installations.FirebaseInstallations
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import com.pr0gramm3r101.utils.settings.settings import com.pr0gramm3r101.utils.settings.settings
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -13,17 +14,25 @@ import kotlin.coroutines.resumeWithException
private const val PENDING_FCM_TOKEN_KEY = "pending_fcm_token" private const val PENDING_FCM_TOKEN_KEY = "pending_fcm_token"
private const val CURRENT_FCM_TOKEN_KEY = "current_fcm_token" private const val CURRENT_FCM_TOKEN_KEY = "current_fcm_token"
private suspend fun fetchCurrentFcmToken(): String? = suspendCancellableCoroutine { cont -> private suspend fun <T> Task<T>.awaitResult(): T = suspendCancellableCoroutine { cont ->
FirebaseMessaging.getInstance().token addOnCompleteListener { task ->
.addOnCompleteListener { task: Task<String> -> if (task.isSuccessful) {
if (task.isSuccessful) { cont.resume(task.result)
cont.resume(task.result) } else {
} else { cont.resumeWithException(
cont.resumeWithException( task.exception ?: IllegalStateException("Firebase task failed"),
task.exception ?: IllegalStateException("Failed to fetch FCM token") )
)
}
} }
}
}
/** Registers with FCM and returns the Firebase Installation ID used for targeting. */
private suspend fun fetchCurrentFcmToken(): String? = runCatching {
FirebaseMessaging.getInstance().register().awaitResult()
FirebaseInstallations.getInstance().id.awaitResult()
}.getOrElse { e ->
Logger.e("FcmReg", "Failed to fetch FCM installation id: ${e.message}", e)
null
} }
private suspend fun postFcmToken(token: String): Boolean { private suspend fun postFcmToken(token: String): Boolean {
@@ -40,7 +40,7 @@ internal actual suspend fun platformAesGcmStreamDecryptMekFile(
outputFile.delete() outputFile.delete()
} }
val cipher = GCMBlockCipher.newInstance(AESEngine()) val cipher = GCMBlockCipher.newInstance(AESEngine.newInstance())
cipher.init(false, AEADParameters(KeyParameter(key), 128, iv)) cipher.init(false, AEADParameters(KeyParameter(key), 128, iv))
val inBuf = ByteArray(FILE_DECRYPT_BUFFER_BYTES) val inBuf = ByteArray(FILE_DECRYPT_BUFFER_BYTES)
@@ -5,7 +5,7 @@
<string name="settings">Настройки</string> <string name="settings">Настройки</string>
<string name="home">Главная</string> <string name="home">Главная</string>
<string name="about">О приложении</string> <string name="about">О приложении</string>
<string name="about_version">Версия 1.0</string> <string name="about_version">Версия %1$s</string>
<string name="about_link_telegram">Telegram</string> <string name="about_link_telegram">Telegram</string>
<string name="about_link_max">MAX</string> <string name="about_link_max">MAX</string>
<string name="about_link_website">Сайт</string> <string name="about_link_website">Сайт</string>
@@ -169,6 +169,7 @@
<string name="profile_load_failed">Не получилось загрузить профиль</string> <string name="profile_load_failed">Не получилось загрузить профиль</string>
<string name="profile_not_found">Профиль не найден</string> <string name="profile_not_found">Профиль не найден</string>
<string name="profile_open_failed">Не удалось открыть профиль. Попробуйте снова.</string> <string name="profile_open_failed">Не удалось открыть профиль. Попробуйте снова.</string>
<string name="profile_invalid_link">Не удалось открыть ссылку</string>
<string name="action_open_settings">Настройки</string> <string name="action_open_settings">Настройки</string>
<string name="action_chat">Написать</string> <string name="action_chat">Написать</string>
<string name="action_copy_link">Скопировать ссылку</string> <string name="action_copy_link">Скопировать ссылку</string>
@@ -8,7 +8,7 @@
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="home">Home</string> <string name="home">Home</string>
<string name="about">About</string> <string name="about">About</string>
<string name="about_version">Version 1.0</string> <string name="about_version">Version %1$s</string>
<string name="about_link_telegram">Telegram</string> <string name="about_link_telegram">Telegram</string>
<string name="about_link_max">MAX</string> <string name="about_link_max">MAX</string>
<string name="about_link_website">Website</string> <string name="about_link_website">Website</string>
@@ -187,6 +187,7 @@
<string name="profile_load_failed">Couldnt load this profile</string> <string name="profile_load_failed">Couldnt load this profile</string>
<string name="profile_not_found">This profile could not be found</string> <string name="profile_not_found">This profile could not be found</string>
<string name="profile_open_failed">Could not open this profile. Please try again.</string> <string name="profile_open_failed">Could not open this profile. Please try again.</string>
<string name="profile_invalid_link">Couldnt open the link</string>
<string name="action_open_settings">Settings</string> <string name="action_open_settings">Settings</string>
<string name="action_chat">Chat</string> <string name="action_chat">Chat</string>
<string name="action_copy_link">Copy link</string> <string name="action_copy_link">Copy link</string>
@@ -386,7 +386,7 @@ object MessageCacheStore {
if (product in 0.92f..1.08f && kotlin.math.abs(decodedAspect - serverAspect) > 0.15f) { if (product in 0.92f..1.08f && kotlin.math.abs(decodedAspect - serverAspect) > 0.15f) {
resolved = resolved.copy( resolved = resolved.copy(
fileAspectRatios = listOf(decodedAspect), fileAspectRatios = listOf(decodedAspect),
fileDimensions = thumbDims?.let { listOf(it.first to it.second) } ?: resolved.fileDimensions, fileDimensions = listOf(thumbDims.first to thumbDims.second),
) )
} }
} }
@@ -1,6 +1,7 @@
package ru.fromchat.api.local.db.store package ru.fromchat.api.local.db.store
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import ru.fromchat.api.ApiClient import ru.fromchat.api.ApiClient
import ru.fromchat.api.local.messages.ChatListPreviewState import ru.fromchat.api.local.messages.ChatListPreviewState
import ru.fromchat.api.local.messages.ChatListPreviewStrings import ru.fromchat.api.local.messages.ChatListPreviewStrings
@@ -21,7 +22,9 @@ object MessageRepository {
MessageCacheStore.observeMessages(activeInstance(), conversationId) MessageCacheStore.observeMessages(activeInstance(), conversationId)
fun observePublicMessages(): Flow<List<Message>> = fun observePublicMessages(): Flow<List<Message>> =
observeMessages(conversationIdForGroup(GENERAL_PUBLIC_GROUP_ID)) observeMessages(conversationIdForGroup(GENERAL_PUBLIC_GROUP_ID)).map { rows ->
ProfileCache.enrichPublicMessagesForDisplay(rows)
}
fun observeDmMessages(otherUserId: Int): Flow<List<Message>> = fun observeDmMessages(otherUserId: Int): Flow<List<Message>> =
observeMessages(conversationIdForDm(otherUserId)) observeMessages(conversationIdForDm(otherUserId))
@@ -454,30 +454,33 @@ object ProfileCache {
val uid = message.user_id val uid = message.user_id
if (uid <= 0) return if (uid <= 0) return
val existing = get(uid) val existing = get(uid)
val incomingDisplay = message.displayName?.trim()?.takeIf { it.isNotEmpty() }
val incomingPic = message.profile_picture?.takeIf { it.isNotBlank() }
if (existing != null && !existing.isClientPreviewOnly) { if (existing != null && !existing.isClientPreviewOnly) {
val patched = existing.copy( val patched = existing.copy(
verified = message.verified ?: existing.verified, verified = message.verified ?: existing.verified,
verificationStatus = message.verificationStatus ?: existing.verificationStatus, verificationStatus = message.verificationStatus ?: existing.verificationStatus,
displayName = existing.displayName?.takeIf { it.isNotBlank() } ?: incomingDisplay,
profilePicture = existing.profilePicture?.takeIf { it.isNotBlank() } ?: incomingPic,
username = existing.username.trim().ifBlank { message.username.trim() },
) )
if (patched != existing) put(patched) if (patched != existing) put(patched)
return return
} }
val uname = message.username.trim().ifBlank { existing?.username?.trim().orEmpty() } val uname = message.username.trim().ifBlank { existing?.username?.trim().orEmpty() }
val incomingDisplay = message.displayName?.trim()?.takeIf { it.isNotEmpty() } val displayName = incomingDisplay ?: existing?.displayName?.takeIf { it.isNotBlank() }
?: existing?.displayName?.takeIf { it.isNotBlank() } if (uname.isBlank() && displayName.isNullOrBlank()) return
if (uname.isBlank() && incomingDisplay.isNullOrBlank()) return if (uname.isBlank() || displayName.isNullOrBlank()) {
if (uname.isBlank() || incomingDisplay.isNullOrBlank()) {
Logger.d( Logger.d(
"ProfileCache", "ProfileCache",
"mergePreviewFromPublicMessage missingIdentity id=$uid " + "mergePreviewFromPublicMessage missingIdentity id=$uid " +
"hasUsername=${uname.isNotBlank()} hasDisplayName=${!incomingDisplay.isNullOrBlank()}", "hasUsername=${uname.isNotBlank()} hasDisplayName=${!displayName.isNullOrBlank()}",
) )
} }
val isDeleted = isDeletedPlaceholderUsername(uname) || existing?.deleted == true val isDeleted = isDeletedPlaceholderUsername(uname) || existing?.deleted == true
val display = if (isDeleted) null else incomingDisplay val display = if (isDeleted) null else displayName
val pic = if (isDeleted) null else message.profile_picture?.takeIf { it.isNotBlank() } val pic = if (isDeleted) null else incomingPic ?: existing?.profilePicture
?: existing?.profilePicture
put( put(
UserProfile( UserProfile(
@@ -1189,7 +1189,7 @@ fun ChatScreen(
.getOrNull(panelState.messages.lastIndex - 1) .getOrNull(panelState.messages.lastIndex - 1)
previous != null && previous != null &&
messageListKey(previous) == listKey && messageListKey(previous) == listKey &&
classifyEnterMode(previous, newest!!) == classifyEnterMode(previous, newest) ==
EnterMode.ExtendGroup EnterMode.ExtendGroup
} }
val showTimestamp = when { val showTimestamp = when {
@@ -136,7 +136,7 @@ fun ChatFileAttachmentTile(
} }
} }
} }
downloadPaused && canDownload -> file?.let { downloadFile -> downloadPaused && file != null && canDownload -> {
{ {
AttachmentDownloadNotifier.beginDownload( AttachmentDownloadNotifier.beginDownload(
messageId = messageId, messageId = messageId,
@@ -148,7 +148,7 @@ fun ChatFileAttachmentTile(
val ok = downloadAttachmentToCache( val ok = downloadAttachmentToCache(
messageId = messageId, messageId = messageId,
fileIndex = fileIndex, fileIndex = fileIndex,
file = downloadFile, file = file,
dmEnvelope = dmEnvelope, dmEnvelope = dmEnvelope,
currentUserId = currentUserId, currentUserId = currentUserId,
clientMessageId = clientMessageId, clientMessageId = clientMessageId,
@@ -166,7 +166,7 @@ fun ChatFileAttachmentTile(
} }
} }
} }
canDownload && !isDownloading && !downloadPaused -> file?.let { downloadFile -> file != null && canDownload && !isDownloading && !downloadPaused -> {
{ {
AttachmentDownloadNotifier.beginDownload( AttachmentDownloadNotifier.beginDownload(
messageId = messageId, messageId = messageId,
@@ -178,7 +178,7 @@ fun ChatFileAttachmentTile(
val ok = downloadAttachmentToCache( val ok = downloadAttachmentToCache(
messageId = messageId, messageId = messageId,
fileIndex = fileIndex, fileIndex = fileIndex,
file = downloadFile, file = file,
dmEnvelope = dmEnvelope, dmEnvelope = dmEnvelope,
currentUserId = currentUserId, currentUserId = currentUserId,
clientMessageId = clientMessageId, clientMessageId = clientMessageId,
@@ -84,6 +84,7 @@ class PublicChatPanel(
ProfileCache.enrichPublicMessageForDisplay( ProfileCache.enrichPublicMessageForDisplay(
mergeMessageUiFields(fresh, message).copy( mergeMessageUiFields(fresh, message).copy(
username = fresh.username, username = fresh.username,
displayName = fresh.displayName,
profile_picture = fresh.profile_picture, profile_picture = fresh.profile_picture,
verified = fresh.verified, verified = fresh.verified,
verificationStatus = fresh.verificationStatus, verificationStatus = fresh.verificationStatus,
@@ -179,6 +180,8 @@ class PublicChatPanel(
} }
suspend fun hydrateFromLocalCache() { suspend fun hydrateFromLocalCache() {
// Sender display names live in ProfileCache (message rows only store userId).
runCatching { ProfileCache.hydrateFromDisk() }
hydrateMessagesFromLocalCache() hydrateMessagesFromLocalCache()
runCatching { PublicChatProfileCache.hydrateFromDisk() } runCatching { PublicChatProfileCache.hydrateFromDisk() }
PublicChatProfileCache.profile?.let { applyPublicChatProfile(it) } PublicChatProfileCache.profile?.let { applyPublicChatProfile(it) }
@@ -129,7 +129,17 @@ internal fun mergeMessageUiFields(db: Message, panel: Message?): Message {
?: db.pendingFileAspectRatio?.takeIf { it > 0f } ?: db.pendingFileAspectRatio?.takeIf { it > 0f }
?: panel.fileDimensions?.firstOrNull()?.let { (w, h) -> aspectRatioFromDimensionPair(w, h) } ?: panel.fileDimensions?.firstOrNull()?.let { (w, h) -> aspectRatioFromDimensionPair(w, h) }
?: db.fileDimensions?.firstOrNull()?.let { (w, h) -> aspectRatioFromDimensionPair(w, h) } ?: db.fileDimensions?.firstOrNull()?.let { (w, h) -> aspectRatioFromDimensionPair(w, h) }
// DB rows only store userId; sender identity is reconstructed from ProfileCache and can
// briefly be blank. Keep non-blank panel fields (e.g. from a network payload) so text
// avatars / names are not wiped on every SQLDelight emission.
val merged = db.copy( val merged = db.copy(
username = db.username.trim().ifBlank { panel.username.trim() },
displayName = db.displayName?.trim()?.takeIf { it.isNotEmpty() }
?: panel.displayName?.trim()?.takeIf { it.isNotEmpty() },
profile_picture = db.profile_picture?.takeIf { it.isNotBlank() }
?: panel.profile_picture?.takeIf { it.isNotBlank() },
verified = db.verified ?: panel.verified,
verificationStatus = db.verificationStatus ?: panel.verificationStatus,
pendingFileUri = when { pendingFileUri = when {
confirmed -> localPreview confirmed -> localPreview
else -> panel.pendingFileUri ?: db.pendingFileUri else -> panel.pendingFileUri ?: db.pendingFileUri
@@ -20,7 +20,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetState
import ru.fromchat.ui.components.Text import ru.fromchat.ui.components.Text
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -138,7 +139,10 @@ fun SuspendedAccountSupportSheet(
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val onContact = { uriHandler.openUri("https://t.me/fromchat_ch?direct") } val onContact = { uriHandler.openUri("https://t.me/fromchat_ch?direct") }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberBottomSheetState(
initialValue = SheetValue.Hidden,
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
)
val closeSheet: () -> Unit = { val closeSheet: () -> Unit = {
scope.launch { scope.launch {
@@ -938,13 +938,13 @@ internal fun DmConversationRowContent(
val avatarUrl = if (isPeerDeleted) null else cached?.profilePicture val avatarUrl = if (isPeerDeleted) null else cached?.profilePicture
val peerTitle = when { val peerTitle = when {
isPeerDeleted -> deletedUserDisplayNameForUi() isPeerDeleted -> deletedUserDisplayNameForUi()
!cached?.displayName.isNullOrBlank() -> cached.displayName!!.trim() !cached?.displayName.isNullOrBlank() -> cached.displayName.trim()
conversation.displayName.isNotBlank() -> conversation.displayName conversation.displayName.isNotBlank() -> conversation.displayName
else -> cached?.visibleUsername(currentUserId).orEmpty() else -> cached?.visibleUsername(currentUserId).orEmpty()
} }
val avatarInitialsLabel = when { val avatarInitialsLabel = when {
isPeerDeleted -> deletedUserDisplayNameForUi() isPeerDeleted -> deletedUserDisplayNameForUi()
!cached?.displayName.isNullOrBlank() -> cached.displayName!!.trim() !cached?.displayName.isNullOrBlank() -> cached.displayName.trim()
conversation.displayName.isNotBlank() -> conversation.displayName conversation.displayName.isNotBlank() -> conversation.displayName
else -> "" else -> ""
} }
@@ -90,15 +90,14 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -529,7 +528,7 @@ fun ChatsTab(
animatedVisibilityScope: AnimatedVisibilityScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null,
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
val clipboardManager = LocalClipboardManager.current val clipboard = supportClipboardManagerImpl
val haptic = rememberHapticFeedback() val haptic = rememberHapticFeedback()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val connectionStatus by ConnectionStateStore.status.collectAsState() val connectionStatus by ConnectionStateStore.status.collectAsState()
@@ -1092,7 +1091,9 @@ fun ChatsTab(
chatContextMenuOverlay.onLink = { chatContextMenuOverlay.onLink = {
when (contextMenuState.target) { when (contextMenuState.target) {
ChatContextMenuTarget.Public -> { ChatContextMenuTarget.Public -> {
publicChatLink?.let { clipboardManager.setText(AnnotatedString(it)) } publicChatLink?.let { link ->
scope.launch { clipboard.setText(link) }
}
} }
ChatContextMenuTarget.Dm -> { ChatContextMenuTarget.Dm -> {
val link = contextMenuState.otherUserId?.let { userId -> val link = contextMenuState.otherUserId?.let { userId ->
@@ -1100,7 +1101,7 @@ fun ChatsTab(
val username = cached?.visibleUsername(ApiClient.user?.id) ?: cached?.username val username = cached?.visibleUsername(ApiClient.user?.id) ?: cached?.username
username?.let { "https://fromchat.ru/@$it" } ?: "https://fromchat.ru/?u=$userId" username?.let { "https://fromchat.ru/@$it" } ?: "https://fromchat.ru/?u=$userId"
} }
link?.let { clipboardManager.setText(AnnotatedString(it)) } link?.let { scope.launch { clipboard.setText(it) } }
} }
} }
} }
@@ -37,6 +37,7 @@ import com.pr0gramm3r101.components.ListItem
import com.pr0gramm3r101.ui.Website import com.pr0gramm3r101.ui.Website
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource import org.jetbrains.compose.resources.vectorResource
import ru.fromchat.AppBuildInfo
import ru.fromchat.Res import ru.fromchat.Res
import ru.fromchat.about import ru.fromchat.about
import ru.fromchat.about_link_max import ru.fromchat.about_link_max
@@ -111,7 +112,10 @@ fun AboutScreen() {
BrandTitle(Modifier.padding(bottom = 4.dp)) BrandTitle(Modifier.padding(bottom = 4.dp))
Text( Text(
text = stringResource(Res.string.about_version), text = stringResource(
Res.string.about_version,
AppBuildInfo.version + if (AppBuildInfo.isDebug) "-beta" else "",
),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
@@ -46,7 +46,8 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -595,11 +596,14 @@ fun DevicesScreen(onBack: () -> Unit) {
} }
sheetDevice?.let { d -> sheetDevice?.let { d ->
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberBottomSheetState(
initialValue = SheetValue.Hidden,
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
)
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { if (!sheetSigningOut) sheetDevice = null }, onDismissRequest = { if (!sheetSigningOut) sheetDevice = null },
sheetState = sheetState sheetState = sheetState,
) { ) {
DeviceSessionDetailBottomSheet( DeviceSessionDetailBottomSheet(
d = d, d = d,
@@ -50,7 +50,8 @@ import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -389,13 +390,15 @@ fun LogFilesScreen(
} }
if (showShareSheet) { if (showShareSheet) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showShareSheet = false showShareSheet = false
pendingSharePaths = emptyList() pendingSharePaths = emptyList()
}, },
sheetState = sheetState, sheetState = rememberBottomSheetState(
initialValue = SheetValue.Hidden,
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
),
) { ) {
LogsShareBottomSheet( LogsShareBottomSheet(
onUncompressed = { performShare(LogShareCompression.Uncompressed) }, onUncompressed = { performShare(LogShareCompression.Uncompressed) },
@@ -88,7 +88,8 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -632,13 +633,15 @@ fun LogsScreen() {
} }
if (showShareSheet) { if (showShareSheet) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showShareSheet = false showShareSheet = false
pendingShareRequest = null pendingShareRequest = null
}, },
sheetState = sheetState, sheetState = rememberBottomSheetState(
initialValue = SheetValue.Hidden,
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
),
) { ) {
LogsShareBottomSheet( LogsShareBottomSheet(
onUncompressed = { performShare(LogShareCompression.Uncompressed) }, onUncompressed = { performShare(LogShareCompression.Uncompressed) },
@@ -648,7 +651,10 @@ fun LogsScreen() {
} }
if (showCleanSheet) { if (showCleanSheet) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberBottomSheetState(
initialValue = SheetValue.Hidden,
enabledValues = setOf(SheetValue.Hidden, SheetValue.Expanded),
)
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { showCleanSheet = false }, onDismissRequest = { showCleanSheet = false },
sheetState = sheetState, sheetState = sheetState,
@@ -105,10 +105,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pr0gramm3r101.components.Category import com.pr0gramm3r101.components.Category
import com.pr0gramm3r101.components.ContextMenuPressable import com.pr0gramm3r101.components.ContextMenuPressable
@@ -177,6 +174,7 @@ import ru.fromchat.profile_headline_bio
import ru.fromchat.profile_headline_member_since import ru.fromchat.profile_headline_member_since
import ru.fromchat.profile_headline_username import ru.fromchat.profile_headline_username
import ru.fromchat.profile_headline_verification import ru.fromchat.profile_headline_verification
import ru.fromchat.profile_invalid_link
import ru.fromchat.profile_load_failed import ru.fromchat.profile_load_failed
import ru.fromchat.profile_not_found import ru.fromchat.profile_not_found
import ru.fromchat.profile_verified_support import ru.fromchat.profile_verified_support
@@ -267,7 +265,6 @@ fun ProfileScreen(
onOpenSettings: () -> Unit = {}, onOpenSettings: () -> Unit = {},
showBackButton: Boolean = false, showBackButton: Boolean = false,
) { ) {
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val clipboard = supportClipboardManagerImpl val clipboard = supportClipboardManagerImpl
val navController = LocalNavController.current val navController = LocalNavController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -377,7 +374,7 @@ fun ProfileScreen(
ApiClient.applyOwnProfile(refreshed) ApiClient.applyOwnProfile(refreshed)
state = latestUi.copy(profile = refreshed, error = null) state = latestUi.copy(profile = refreshed, error = null)
} catch (_: Exception) { } catch (_: Exception) {
ownUserId?.let { ProfileCache.get(it) }?.let { cached -> ownUserId.let { ProfileCache.get(it) }?.let { cached ->
state = latestUi.copy(profile = cached) state = latestUi.copy(profile = cached)
} }
} }
@@ -609,7 +606,7 @@ fun ProfileScreen(
ProfileAction( ProfileAction(
label = labelLink, label = labelLink,
icon = Icons.Filled.Link, icon = Icons.Filled.Link,
onClick = { clipboardManager.setText(AnnotatedString(profileLink.orEmpty())) }, onClick = { scope.launch { clipboard.setText(profileLink.orEmpty()) } },
), ),
ProfileAction( ProfileAction(
label = labelSettings, label = labelSettings,
@@ -654,7 +651,7 @@ fun ProfileScreen(
ProfileAction( ProfileAction(
label = labelLink, label = labelLink,
icon = Icons.Filled.Link, icon = Icons.Filled.Link,
onClick = { clipboardManager.setText(AnnotatedString(profileLink.orEmpty())) }, onClick = { scope.launch { clipboard.setText(profileLink.orEmpty()) } },
) )
) )
if (ServerConfig.callsEnabled) { if (ServerConfig.callsEnabled) {
@@ -831,10 +828,10 @@ fun ProfileScreen(
labelCopy = labelCopy, labelCopy = labelCopy,
labelEdit = labelEdit, labelEdit = labelEdit,
detailsBringIntoView = detailsBringIntoView, detailsBringIntoView = detailsBringIntoView,
clipboardManager = clipboardManager,
clipboard = clipboard, clipboard = clipboard,
navController = navController, navController = navController,
scope = scope, scope = scope,
snackbarHostState = snackbarHostState,
openContextMenuHaptic = openContextMenuHaptic, openContextMenuHaptic = openContextMenuHaptic,
onBack = onBack, onBack = onBack,
onProfileUpdated = { updated -> onProfileUpdated = { updated ->
@@ -880,7 +877,6 @@ fun PublicChatProfileScreen(
initialDisplayName: String? = null, initialDisplayName: String? = null,
showBackButton: Boolean = false, showBackButton: Boolean = false,
) { ) {
val clipboardManager = LocalClipboardManager.current
val clipboard = supportClipboardManagerImpl val clipboard = supportClipboardManagerImpl
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@@ -962,7 +958,7 @@ fun PublicChatProfileScreen(
ProfileAction( ProfileAction(
label = labelLink, label = labelLink,
icon = Icons.Filled.Link, icon = Icons.Filled.Link,
onClick = { clipboardManager.setText(AnnotatedString(profileLink.orEmpty())) }, onClick = { scope.launch { clipboard.setText(profileLink.orEmpty()) } },
), ),
ProfileAction( ProfileAction(
label = labelSearch, label = labelSearch,
@@ -1003,15 +999,15 @@ fun PublicChatProfileScreen(
when { when {
useSharedAvatar && displayName.isNotBlank() -> { useSharedAvatar && displayName.isNotBlank() -> {
item { item {
with(sharedTransitionScope!!) { with(sharedTransitionScope) {
Avatar( Avatar(
profilePictureUrl = null, profilePictureUrl = null,
displayName = displayName, displayName = displayName,
modifier = Modifier modifier = Modifier
.padding(top = profileAvatarTop) .padding(top = profileAvatarTop)
.sharedElement( .sharedElement(
rememberSharedContentState(key = sharedAvatarKey!!), rememberSharedContentState(key = sharedAvatarKey),
animatedVisibilityScope = animatedVisibilityScope!!, animatedVisibilityScope = animatedVisibilityScope,
) )
.size(104.dp), .size(104.dp),
) )
@@ -1051,6 +1047,7 @@ fun PublicChatProfileScreen(
labelCopy = labelCopy, labelCopy = labelCopy,
clipboard = clipboard, clipboard = clipboard,
scope = scope, scope = scope,
snackbarHostState = snackbarHostState,
) )
} }
} }
@@ -1090,6 +1087,7 @@ private fun PublicChatProfileLoadedBody(
labelCopy: String, labelCopy: String,
clipboard: SupportClipboardManager, clipboard: SupportClipboardManager,
scope: CoroutineScope, scope: CoroutineScope,
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@@ -1136,6 +1134,7 @@ private fun PublicChatProfileLoadedBody(
supportingSlot = { supportingSlot = {
ProfileBioMarkdown( ProfileBioMarkdown(
content = resolvedProfile.bio.orEmpty(), content = resolvedProfile.bio.orEmpty(),
snackbarHostState = snackbarHostState,
) )
}, },
position = ListItemPosition.START, position = ListItemPosition.START,
@@ -1320,10 +1319,10 @@ private fun ProfileLoadedBody(
labelCopy: String, labelCopy: String,
labelEdit: String, labelEdit: String,
detailsBringIntoView: BringIntoViewRequester, detailsBringIntoView: BringIntoViewRequester,
clipboardManager: ClipboardManager,
clipboard: SupportClipboardManager, clipboard: SupportClipboardManager,
navController: NavController, navController: NavController,
scope: CoroutineScope, scope: CoroutineScope,
snackbarHostState: SnackbarHostState,
openContextMenuHaptic: () -> Unit, openContextMenuHaptic: () -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
onProfileUpdated: (UserProfile) -> Unit, onProfileUpdated: (UserProfile) -> Unit,
@@ -1349,7 +1348,7 @@ private fun ProfileLoadedBody(
onContextMenuOpen = openContextMenuHaptic, onContextMenuOpen = openContextMenuHaptic,
contextMenu = { contextMenu = {
item(Icons.Rounded.ContentCopy, labelCopy) { item(Icons.Rounded.ContentCopy, labelCopy) {
clipboardManager.setText(AnnotatedString(displayName)) scope.launch { clipboard.setText(displayName) }
} }
if (isOwnProfile) { if (isOwnProfile) {
item(Icons.Rounded.Edit, labelEdit) { item(Icons.Rounded.Edit, labelEdit) {
@@ -1453,9 +1452,9 @@ private fun ProfileLoadedBody(
}, },
contextMenu = { contextMenu = {
item(Icons.Rounded.ContentCopy, labelCopy) { item(Icons.Rounded.ContentCopy, labelCopy) {
clipboardManager.setText( scope.launch {
AnnotatedString(usernameForLinks.orEmpty()), clipboard.setText(usernameForLinks.orEmpty())
) }
} }
if (isOwnProfile) { if (isOwnProfile) {
item(Icons.Rounded.Edit, labelEdit) { item(Icons.Rounded.Edit, labelEdit) {
@@ -1491,7 +1490,7 @@ private fun ProfileLoadedBody(
}, },
contextMenu = { contextMenu = {
item(Icons.Rounded.ContentCopy, labelCopy) { item(Icons.Rounded.ContentCopy, labelCopy) {
clipboardManager.setText(AnnotatedString(memberSinceText)) scope.launch { clipboard.setText(memberSinceText) }
} }
}, },
) )
@@ -1505,7 +1504,10 @@ private fun ProfileLoadedBody(
headline = headlineBio, headline = headlineBio,
supportingSlot = { supportingSlot = {
key(resolvedProfile.id, bioContent) { key(resolvedProfile.id, bioContent) {
ProfileBioMarkdown(content = bioContent) ProfileBioMarkdown(
content = bioContent,
snackbarHostState = snackbarHostState,
)
} }
}, },
divider = true, divider = true,
@@ -1864,14 +1866,27 @@ private fun ProfileLoadedBody(
@Composable @Composable
private fun ProfileBioMarkdown( private fun ProfileBioMarkdown(
content: String, content: String,
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
val invalidLinkMessage = stringResource(Res.string.profile_invalid_link)
MarkdownPlain( MarkdownPlain(
content = content, content = content,
modifier = modifier, modifier = modifier,
onLinkClick = { uriHandler.openUri(it) }, onLinkClick = { uri ->
runCatching { uriHandler.openUri(uri) }.onFailure {
scope.launch {
snackbarHostState.showReplacingSnackbar(
message = invalidLinkMessage,
withDismissAction = false,
duration = SnackbarDuration.Short,
)
}
}
},
) )
} }
+4
View File
@@ -8,6 +8,10 @@ plugins {
alias(libs.plugins.google.services) apply false alias(libs.plugins.google.services) apply false
} }
/** Single source of truth for app version (APK + generated [AppBuildInfo]). */
extra["versionName"] = "1.1.3"
extra["versionCode"] = 113
buildscript { buildscript {
repositories { repositories {
google() google()
+23 -25
View File
@@ -1,52 +1,52 @@
[versions] [versions]
agp = "9.1.1" agp = "9.3.0"
androidx-activityCompose = "1.13.0" androidx-activityCompose = "1.13.0"
androidx-appcompat = "1.7.1" androidx-appcompat = "1.7.1"
androidx-core-ktx = "1.18.0" androidx-core-ktx = "1.19.0"
androidx-exifinterface = "1.3.7" androidx-exifinterface = "1.4.2"
coilCompose = "3.4.0" coilCompose = "3.5.0"
compose-multiplatform = "1.10.3" compose-multiplatform = "1.11.1"
#noinspection NewerVersionAvailable #noinspection NewerVersionAvailable
constraintlayout = "0.6.1-shaded" constraintlayout = "0.8.0-shaded"
coreSplashscreen = "1.2.0" coreSplashscreen = "1.2.0"
firebaseMessaging = "25.0.2" firebaseMessaging = "25.1.1"
googleServices = "4.4.4" googleServices = "4.5.0"
haze = "1.7.2" haze = "1.7.2"
kotlin = "2.3.21" kotlin = "2.4.10"
adaptiveAndroid = "1.2.0" adaptiveAndroid = "1.2.0"
biometric = "1.4.0-alpha07" biometric = "1.4.0-alpha07"
gson = "2.14.0" gson = "2.14.0"
kotlinxIoBytestring = "0.9.0" kotlinxIoBytestring = "0.9.1"
kotlinxCoroutinesCore = "1.11.0" kotlinxCoroutinesCore = "1.11.0"
kotlinxIoCore = "0.9.0" kotlinxIoCore = "0.9.1"
multiplatformCryptoLibsodiumBindings = "0.9.5" multiplatformCryptoLibsodiumBindings = "0.9.5"
multiplatformSettings = "1.3.0" multiplatformSettings = "1.3.0"
serialization = "2.3.21" serialization = "2.4.10"
serialization-json = "1.11.0" serialization-json = "1.11.0"
material = "1.13.0" material = "1.14.0"
activityKtx = "1.13.0" activityKtx = "1.13.0"
navigationCompose = "2.9.2" navigationCompose = "2.9.2"
datastore = "1.2.1" datastore = "1.2.1"
security-crypto = "1.1.0" security-crypto = "1.1.0"
ktor = "3.4.3" ktor = "3.5.1"
slf4j = "1.7.36" slf4j = "1.7.36"
kotlinxDatetime = "0.8.0" kotlinxDatetime = "0.8.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.11.0"
composeBom = "2026.05.00" composeBom = "2026.06.01"
composeMaterialIconsExtended = "1.7.3" composeMaterialIconsExtended = "1.7.3"
composeMaterial3 = "1.10.0-alpha05" composeMaterial3 = "1.12.0-alpha03"
composeComponents = "1.10.3" composeComponents = "1.11.1"
playServicesBase = "18.10.0" playServicesBase = "18.10.0"
tweetnaclJava = "1.1.3" tweetnaclJava = "1.1.3"
androidxWork = "2.11.2" androidxWork = "2.11.2"
cryptography-kotlin = "0.6.0" cryptography-kotlin = "0.6.0"
krypto = "4.0.10" krypto = "4.0.10"
sqldelight = "2.3.2" sqldelight = "2.3.2"
livekitAndroid = "2.25.2" livekitAndroid = "2.27.0"
livekitAndroidComposeComponents = "2.3.0" livekitAndroidComposeComponents = "2.4.0"
markdownRendererM3 = "0.41.0" markdownRendererM3 = "0.43.0"
bouncycastle = "1.79" bouncycastle = "1.85"
webkit = "1.14.0" webkit = "1.16.0"
[libraries] [libraries]
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
@@ -87,7 +87,6 @@ ktor-client-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" } slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
@@ -104,7 +103,6 @@ compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", v
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMaterial3" } compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMaterial3" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" } compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-multiplatform" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeComponents" } compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeComponents" }
compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "compose-multiplatform" }
compose-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMaterialIconsExtended" } compose-materialIconsExtended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMaterialIconsExtended" }
tweetnacl-java = { module = "org.purejava:tweetnacl-java", version.ref = "tweetnaclJava" } tweetnacl-java = { module = "org.purejava:tweetnacl-java", version.ref = "tweetnaclJava" }
krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "krypto" } krypto = { module = "com.soywiz.korlibs.krypto:krypto", version.ref = "krypto" }
+2 -5
View File
@@ -1,9 +1,6 @@
#Tue Jul 21 20:27:03 MSK 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+1 -2
View File
@@ -18,8 +18,7 @@ kotlin {
listOf( listOf(
iosArm64(), iosArm64(),
iosSimulatorArm64(), iosSimulatorArm64()
iosX64(),
).forEach { ).forEach {
it.binaries.framework { it.binaries.framework {
baseName = "shared" baseName = "shared"
@@ -17,7 +17,7 @@ actual fun Clipboard.toSupport(): SupportClipboardManager {
override suspend fun getText(): String? { override suspend fun getText(): String? {
val entry = clipboard.getClipEntry() ?: return null val entry = clipboard.getClipEntry() ?: return null
return entry.clipData?.getItemAt(0)?.text?.toString() return entry.clipData.getItemAt(0)?.text?.toString()
} }
override fun setTextListener(listener: (String) -> Unit) { override fun setTextListener(listener: (String) -> Unit) {
@@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package com.pr0gramm3r101.utils.settings package com.pr0gramm3r101.utils.settings
import androidx.core.content.edit import androidx.core.content.edit
@@ -7,6 +9,12 @@ import com.pr0gramm3r101.utils.UtilsLibrary.context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/**
* Secure prefs via EncryptedSharedPreferences.
*
* These APIs are deprecated in favor of DataStore + Tink (`datastore-tink`, DataStore 1.3+).
* Kept until that stack is stable enough to migrate auth/identity keys without risk.
*/
class AndroidSecureSettings : Settings { class AndroidSecureSettings : Settings {
private val masterKey by lazy { private val masterKey by lazy {
MasterKey.Builder(context) MasterKey.Builder(context)
@@ -80,4 +88,3 @@ class AndroidSecureSettings : Settings {
encryptedPrefs.contains(key) encryptedPrefs.contains(key)
} }
} }