Промежуточный результат для 1.0.4 версии
This commit is contained in:
@@ -4,7 +4,11 @@ private const val PLACEHOLDER_ACCOUNT_NAME = "Account"
|
||||
|
||||
fun resolveAccountDisplayName(publicKey: String, name: String?, username: String?): String {
|
||||
val normalizedName = name?.trim().orEmpty()
|
||||
if (normalizedName.isNotEmpty() && !normalizedName.equals(PLACEHOLDER_ACCOUNT_NAME, ignoreCase = true)) {
|
||||
if (
|
||||
normalizedName.isNotEmpty() &&
|
||||
!normalizedName.equals(PLACEHOLDER_ACCOUNT_NAME, ignoreCase = true) &&
|
||||
!looksLikePublicKeyAlias(normalizedName, publicKey)
|
||||
) {
|
||||
return normalizedName
|
||||
}
|
||||
|
||||
@@ -24,5 +28,19 @@ fun resolveAccountDisplayName(publicKey: String, name: String?, username: String
|
||||
}
|
||||
}
|
||||
|
||||
private fun looksLikePublicKeyAlias(name: String, publicKey: String): Boolean {
|
||||
if (publicKey.isBlank()) return false
|
||||
if (name.equals(publicKey, ignoreCase = true)) return true
|
||||
|
||||
val compactKeyAlias =
|
||||
if (publicKey.length > 12) "${publicKey.take(6)}...${publicKey.takeLast(4)}" else publicKey
|
||||
if (name.equals(compactKeyAlias, ignoreCase = true)) return true
|
||||
|
||||
val normalized = name.lowercase()
|
||||
val prefix = publicKey.take(6).lowercase()
|
||||
val suffix = publicKey.takeLast(4).lowercase()
|
||||
return normalized.startsWith(prefix) && normalized.endsWith(suffix) && normalized.contains("...")
|
||||
}
|
||||
|
||||
fun isPlaceholderAccountName(name: String?): Boolean =
|
||||
name?.trim().orEmpty().equals(PLACEHOLDER_ACCOUNT_NAME, ignoreCase = true)
|
||||
|
||||
104
app/src/main/java/com/rosetta/messenger/data/DraftManager.kt
Normal file
104
app/src/main/java/com/rosetta/messenger/data/DraftManager.kt
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.rosetta.messenger.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* 📝 Менеджер черновиков сообщений (как в Telegram)
|
||||
* Хранит текст из инпута для каждого диалога.
|
||||
* При возврате в список чатов показывает "Draft: текст" красным цветом.
|
||||
*
|
||||
* Использует SharedPreferences для персистентности между перезапусками.
|
||||
* Ключ: "draft_{account}_{opponentKey}" -> текст черновика
|
||||
*/
|
||||
object DraftManager {
|
||||
|
||||
private const val PREFS_NAME = "rosetta_drafts"
|
||||
private const val KEY_PREFIX = "draft_"
|
||||
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
// 🔥 Реактивный Map: opponentKey -> draftText
|
||||
// Обновляется при каждом изменении черновика для мгновенного обновления UI списка чатов
|
||||
private val _drafts = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||
val drafts: StateFlow<Map<String, String>> = _drafts.asStateFlow()
|
||||
|
||||
private var currentAccount: String = ""
|
||||
|
||||
/** Инициализация с контекстом приложения */
|
||||
fun init(context: Context) {
|
||||
if (prefs == null) {
|
||||
prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
|
||||
/** Установить текущий аккаунт и загрузить его черновики */
|
||||
fun setAccount(account: String) {
|
||||
if (currentAccount == account) return
|
||||
currentAccount = account
|
||||
loadDraftsFromPrefs()
|
||||
}
|
||||
|
||||
/** Сохранить черновик для диалога */
|
||||
fun saveDraft(opponentKey: String, text: String) {
|
||||
if (currentAccount.isEmpty()) return
|
||||
|
||||
val trimmed = text.trim()
|
||||
val currentDrafts = _drafts.value.toMutableMap()
|
||||
|
||||
if (trimmed.isEmpty()) {
|
||||
// Удаляем черновик если текст пустой
|
||||
currentDrafts.remove(opponentKey)
|
||||
prefs?.edit()?.remove(prefKey(opponentKey))?.apply()
|
||||
} else {
|
||||
currentDrafts[opponentKey] = trimmed
|
||||
prefs?.edit()?.putString(prefKey(opponentKey), trimmed)?.apply()
|
||||
}
|
||||
|
||||
_drafts.value = currentDrafts
|
||||
}
|
||||
|
||||
/** Получить черновик для диалога */
|
||||
fun getDraft(opponentKey: String): String? {
|
||||
return _drafts.value[opponentKey]
|
||||
}
|
||||
|
||||
/** Очистить черновик для диалога (после отправки сообщения) */
|
||||
fun clearDraft(opponentKey: String) {
|
||||
if (currentAccount.isEmpty()) return
|
||||
|
||||
val currentDrafts = _drafts.value.toMutableMap()
|
||||
currentDrafts.remove(opponentKey)
|
||||
_drafts.value = currentDrafts
|
||||
|
||||
prefs?.edit()?.remove(prefKey(opponentKey))?.apply()
|
||||
}
|
||||
|
||||
/** Очистить все черновики (при смене аккаунта / логауте) */
|
||||
fun clearAll() {
|
||||
_drafts.value = emptyMap()
|
||||
}
|
||||
|
||||
/** Загрузить черновики текущего аккаунта из SharedPreferences */
|
||||
private fun loadDraftsFromPrefs() {
|
||||
val allPrefs = prefs?.all ?: return
|
||||
val prefix = "${KEY_PREFIX}${currentAccount}_"
|
||||
val loaded = mutableMapOf<String, String>()
|
||||
|
||||
allPrefs.forEach { (key, value) ->
|
||||
if (key.startsWith(prefix) && value is String && value.isNotEmpty()) {
|
||||
val opponentKey = key.removePrefix(prefix)
|
||||
loaded[opponentKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
_drafts.value = loaded
|
||||
}
|
||||
|
||||
private fun prefKey(opponentKey: String): String {
|
||||
return "${KEY_PREFIX}${currentAccount}_${opponentKey}"
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,15 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
const val SYSTEM_SAFE_TITLE = "Safe"
|
||||
const val SYSTEM_SAFE_USERNAME = "safe"
|
||||
|
||||
const val SYSTEM_UPDATES_PUBLIC_KEY = "0x000000000000000000000000000000000000000001"
|
||||
const val SYSTEM_UPDATES_TITLE = "Rosetta Updates"
|
||||
const val SYSTEM_UPDATES_USERNAME = "updates"
|
||||
|
||||
/** All system account public keys */
|
||||
val SYSTEM_ACCOUNT_KEYS = setOf(SYSTEM_SAFE_PUBLIC_KEY, SYSTEM_UPDATES_PUBLIC_KEY)
|
||||
|
||||
fun isSystemAccount(publicKey: String): Boolean = publicKey in SYSTEM_ACCOUNT_KEYS
|
||||
|
||||
// 🔥 In-memory кэш обработанных messageId для предотвращения дубликатов
|
||||
// LRU кэш с ограничением 1000 элементов - защита от race conditions
|
||||
private val processedMessageIds =
|
||||
@@ -221,6 +230,80 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
_newMessageEvents.tryEmit(dialogKey)
|
||||
}
|
||||
|
||||
/** Send a system message from "Rosetta Updates" account */
|
||||
suspend fun addUpdateSystemMessage(messageText: String) {
|
||||
val account = currentAccount ?: return
|
||||
val privateKey = currentPrivateKey ?: return
|
||||
|
||||
val encryptedPlainMessage =
|
||||
try {
|
||||
CryptoManager.encryptWithPassword(messageText, privateKey)
|
||||
} catch (_: Exception) {
|
||||
return
|
||||
}
|
||||
|
||||
val messageId = UUID.randomUUID().toString().replace("-", "").take(32)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val dialogKey = getDialogKey(SYSTEM_UPDATES_PUBLIC_KEY)
|
||||
|
||||
val inserted =
|
||||
messageDao.insertMessage(
|
||||
MessageEntity(
|
||||
account = account,
|
||||
fromPublicKey = SYSTEM_UPDATES_PUBLIC_KEY,
|
||||
toPublicKey = account,
|
||||
content = "",
|
||||
timestamp = timestamp,
|
||||
chachaKey = "",
|
||||
read = 0,
|
||||
fromMe = 0,
|
||||
delivered = DeliveryStatus.DELIVERED.value,
|
||||
messageId = messageId,
|
||||
plainMessage = encryptedPlainMessage,
|
||||
attachments = "[]",
|
||||
dialogKey = dialogKey
|
||||
)
|
||||
)
|
||||
|
||||
if (inserted == -1L) return
|
||||
|
||||
val existing = dialogDao.getDialog(account, SYSTEM_UPDATES_PUBLIC_KEY)
|
||||
dialogDao.insertDialog(
|
||||
DialogEntity(
|
||||
id = existing?.id ?: 0,
|
||||
account = account,
|
||||
opponentKey = SYSTEM_UPDATES_PUBLIC_KEY,
|
||||
opponentTitle = existing?.opponentTitle?.ifBlank { SYSTEM_UPDATES_TITLE } ?: SYSTEM_UPDATES_TITLE,
|
||||
opponentUsername =
|
||||
existing?.opponentUsername?.ifBlank { SYSTEM_UPDATES_USERNAME }
|
||||
?: SYSTEM_UPDATES_USERNAME,
|
||||
isOnline = existing?.isOnline ?: 0,
|
||||
lastSeen = existing?.lastSeen ?: 0,
|
||||
verified = maxOf(existing?.verified ?: 0, 1),
|
||||
iHaveSent = 1
|
||||
)
|
||||
)
|
||||
|
||||
dialogDao.updateDialogFromMessages(account, SYSTEM_UPDATES_PUBLIC_KEY)
|
||||
_newMessageEvents.tryEmit(dialogKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app version changed and send update message from "Rosetta Updates"
|
||||
* Called after account initialization, like desktop useUpdateMessage hook
|
||||
*/
|
||||
suspend fun checkAndSendVersionUpdateMessage() {
|
||||
val account = currentAccount ?: return
|
||||
val prefs = context.getSharedPreferences("rosetta_system_${account}", Context.MODE_PRIVATE)
|
||||
val lastNoticeVersion = prefs.getString("lastNoticeVersion", "") ?: ""
|
||||
val currentVersion = com.rosetta.messenger.BuildConfig.VERSION_NAME
|
||||
|
||||
if (lastNoticeVersion != currentVersion) {
|
||||
addUpdateSystemMessage(ReleaseNotes.getNotice(currentVersion))
|
||||
prefs.edit().putString("lastNoticeVersion", currentVersion).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** Инициализация с текущим аккаунтом */
|
||||
fun initialize(publicKey: String, privateKey: String) {
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
@@ -40,6 +40,7 @@ class PreferencesManager(private val context: Context) {
|
||||
val AUTO_DOWNLOAD_PHOTOS = booleanPreferencesKey("auto_download_photos")
|
||||
val AUTO_DOWNLOAD_VIDEOS = booleanPreferencesKey("auto_download_videos")
|
||||
val AUTO_DOWNLOAD_FILES = booleanPreferencesKey("auto_download_files")
|
||||
val CAMERA_FLASH_MODE = intPreferencesKey("camera_flash_mode") // 0=off, 1=auto, 2=on
|
||||
|
||||
// Privacy
|
||||
val SHOW_ONLINE_STATUS = booleanPreferencesKey("show_online_status")
|
||||
@@ -158,6 +159,11 @@ class PreferencesManager(private val context: Context) {
|
||||
val autoDownloadFiles: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_FILES] ?: false }
|
||||
|
||||
val cameraFlashMode: Flow<Int> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
normalizeCameraFlashMode(preferences[CAMERA_FLASH_MODE])
|
||||
}
|
||||
|
||||
suspend fun setMessageTextSize(value: Int) {
|
||||
context.dataStore.edit { preferences -> preferences[MESSAGE_TEXT_SIZE] = value }
|
||||
}
|
||||
@@ -178,6 +184,23 @@ class PreferencesManager(private val context: Context) {
|
||||
context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_FILES] = value }
|
||||
}
|
||||
|
||||
suspend fun getCameraFlashMode(): Int {
|
||||
return normalizeCameraFlashMode(context.dataStore.data.first()[CAMERA_FLASH_MODE])
|
||||
}
|
||||
|
||||
suspend fun setCameraFlashMode(value: Int) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[CAMERA_FLASH_MODE] = normalizeCameraFlashMode(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeCameraFlashMode(value: Int?): Int {
|
||||
return when (value) {
|
||||
0, 1, 2 -> value
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// 🔐 PRIVACY
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
31
app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt
Normal file
31
app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.rosetta.messenger.data
|
||||
|
||||
/**
|
||||
* Release notes for "Rosetta Updates" system messages.
|
||||
*
|
||||
* When releasing a new version, update [RELEASE_NOTICE] below.
|
||||
* The text will be sent once to each user after they update the app.
|
||||
*/
|
||||
object ReleaseNotes {
|
||||
|
||||
/**
|
||||
* Current release notice shown to users after update.
|
||||
* [VERSION_PLACEHOLDER] will be replaced with the actual version from BuildConfig.
|
||||
*/
|
||||
const val VERSION_PLACEHOLDER = "{version}"
|
||||
|
||||
val RELEASE_NOTICE = """
|
||||
Update v$VERSION_PLACEHOLDER
|
||||
|
||||
- New attachment panel with tabs (photos, files, avatar)
|
||||
- Message drafts — text is saved when leaving a chat
|
||||
- Circular reveal animation for theme switching
|
||||
- Photo albums in media picker
|
||||
- Camera flash mode is now saved between sessions
|
||||
- Swipe-back gesture fixes
|
||||
- UI and performance improvements
|
||||
""".trimIndent()
|
||||
|
||||
fun getNotice(version: String): String =
|
||||
RELEASE_NOTICE.replace(VERSION_PLACEHOLDER, version)
|
||||
}
|
||||
Reference in New Issue
Block a user