mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-23 11:35:06 +03:00
Compare commits
6 Commits
@@ -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")
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") {
|
||||||
@@ -134,4 +195,4 @@ tasks.register("generateResourceAccessors") {
|
|||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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">Couldn’t load this profile</string>
|
<string name="profile_load_failed">Couldn’t 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">Couldn’t 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>
|
||||||
|
|||||||
+1
-1
@@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -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 {
|
||||||
|
|||||||
+4
-4
@@ -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,
|
||||||
|
|||||||
+3
@@ -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
|
||||||
|
|||||||
+6
-2
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ kotlin {
|
|||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
iosSimulatorArm64(),
|
iosSimulatorArm64()
|
||||||
iosX64(),
|
|
||||||
).forEach {
|
).forEach {
|
||||||
it.binaries.framework {
|
it.binaries.framework {
|
||||||
baseName = "shared"
|
baseName = "shared"
|
||||||
|
|||||||
+1
-1
@@ -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) {
|
||||||
|
|||||||
+8
-1
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user