feat: Add Profile Logs Screen and integrate logging functionality

- Introduced ProfileLogsScreen for displaying logs related to profile operations.
- Enhanced MainScreen to include navigation to the new logs screen.
- Updated ProfileViewModel to manage logs and handle profile save operations.
- Implemented back navigation and log clearing functionality in ProfileLogsScreen.
- Improved SafetyScreen and ThemeScreen with back gesture handling.
- Refactored BackupScreen and OtherProfileScreen for consistency and better UX.
- Adjusted UI elements for better visibility and interaction feedback.
This commit is contained in:
k1ngsterr1
2026-01-20 23:42:02 +05:00
parent 9edcb712fc
commit db61ebb79f
10 changed files with 714 additions and 168 deletions

View File

@@ -452,6 +452,11 @@ fun MainScreen(
var showThemeScreen by remember { mutableStateOf(false) }
var showSafetyScreen by remember { mutableStateOf(false) }
var showBackupScreen by remember { mutableStateOf(false) }
var showLogsScreen by remember { mutableStateOf(false) }
// ProfileViewModel для логов
val profileViewModel: com.rosetta.messenger.ui.settings.ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
val profileState by profileViewModel.state.collectAsState()
// 🔥 Простая навигация с fade-in анимацией
Box(modifier = Modifier.fillMaxSize()) {
@@ -459,7 +464,7 @@ fun MainScreen(
androidx.compose.animation.AnimatedVisibility(
visible = !showBackupScreen && !showSafetyScreen && !showThemeScreen &&
!showUpdatesScreen && selectedUser == null && !showSearchScreen &&
!showProfileScreen && !showOtherProfileScreen,
!showProfileScreen && !showOtherProfileScreen && !showLogsScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
@@ -516,7 +521,10 @@ fun MainScreen(
if (showBackupScreen) {
BackupScreen(
isDarkTheme = isDarkTheme,
onBack = { showBackupScreen = false },
onBack = {
showBackupScreen = false
showSafetyScreen = true
},
onVerifyPassword = { password ->
// TODO: Implement password verification
if (password == "test") {
@@ -536,8 +544,15 @@ fun MainScreen(
SafetyScreen(
isDarkTheme = isDarkTheme,
accountPublicKey = accountPublicKey,
onBack = { showSafetyScreen = false },
onBackupClick = { showBackupScreen = true },
accountPrivateKey = accountPrivateKey,
onBack = {
showSafetyScreen = false
showProfileScreen = true
},
onBackupClick = {
showSafetyScreen = false
showBackupScreen = true
},
onDeleteAccount = {
// TODO: Implement account deletion
Log.d("MainActivity", "Delete account requested")
@@ -554,7 +569,10 @@ fun MainScreen(
if (showThemeScreen) {
ThemeScreen(
isDarkTheme = isDarkTheme,
onBack = { showThemeScreen = false },
onBack = {
showThemeScreen = false
showProfileScreen = true
},
onThemeChange = { isDark ->
onToggleTheme()
}
@@ -644,10 +662,11 @@ fun MainScreen(
accountName = accountName,
accountUsername = "", // TODO: Get from account
accountPublicKey = accountPublicKey,
accountPrivateKeyHash = privateKeyHash,
onBack = { showProfileScreen = false },
onSaveProfile = { name, username ->
// TODO: Save profile changes
Log.d("MainActivity", "Saving profile: name=$name, username=$username")
// Profile saved via ViewModel
Log.d("MainActivity", "Profile saved: name=$name, username=$username")
},
onLogout = onLogout,
onNavigateToTheme = {
@@ -657,7 +676,32 @@ fun MainScreen(
onNavigateToSafety = {
showProfileScreen = false
showSafetyScreen = true
}
},
onNavigateToLogs = {
showProfileScreen = false
showLogsScreen = true
},
viewModel = profileViewModel
)
}
}
androidx.compose.animation.AnimatedVisibility(
visible = showLogsScreen,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(200))
) {
if (showLogsScreen) {
com.rosetta.messenger.ui.settings.ProfileLogsScreen(
isDarkTheme = isDarkTheme,
logs = profileState.logs,
onBack = {
showLogsScreen = false
showProfileScreen = true
},
onClearLogs = {
profileViewModel.clearLogs()
}
)
}
}

View File

@@ -388,9 +388,6 @@ fun ChatsListScreen(
.background(
color = headerColor
)
.statusBarsPadding() // 🎨
// Контент начинается
// после status bar
.padding(
top = 16.dp,
start = 20.dp,

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.ui.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -37,12 +38,14 @@ fun BackupScreen(
var password by remember { mutableStateOf("") }
var seedPhrase by remember { mutableStateOf<String?>(null) }
var passwordVisible by remember { mutableStateOf(false) }
// Handle back gesture
BackHandler { onBack() }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(
@@ -82,17 +85,17 @@ fun BackupScreen(
// Warning Card
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFEF4444).copy(alpha = 0.15f),
color = (if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)).copy(alpha = 0.15f),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Top
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
tint = Color(0xFFEF4444),
tint = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
modifier = Modifier
.size(24.dp)
.padding(top = 2.dp)
@@ -127,7 +130,7 @@ fun BackupScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
@@ -143,26 +146,30 @@ fun BackupScreen(
modifier = Modifier.weight(1f),
textStyle = TextStyle(
color = textColor,
fontSize = 16.sp
fontSize = 15.sp
),
visualTransformation = if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
singleLine = true,
decorationBox = { innerTextField ->
if (password.isEmpty()) {
Text(
text = "Enter your password",
color = secondaryTextColor,
fontSize = 16.sp
)
Box(modifier = Modifier.fillMaxWidth()) {
if (password.isEmpty()) {
Text(
text = "Enter your password",
color = secondaryTextColor,
fontSize = 15.sp
)
}
innerTextField()
}
innerTextField()
}
)
IconButton(
onClick = { passwordVisible = !passwordVisible }
onClick = { passwordVisible = !passwordVisible },
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = if (passwordVisible)
@@ -170,7 +177,8 @@ fun BackupScreen(
else
Icons.Filled.VisibilityOff,
contentDescription = if (passwordVisible) "Hide password" else "Show password",
tint = secondaryTextColor
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
@@ -178,10 +186,10 @@ fun BackupScreen(
Text(
text = "To view your recovery phrase, enter the password you specified when creating your account.",
fontSize = 12.sp,
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp),
lineHeight = 18.sp
)
} else {
// Seed Phrase Display
@@ -222,7 +230,7 @@ fun BackupScreen(
Text(
text = "Please don't share your seed phrase! The administration will never ask you for it. Write it down and store it in a safe place.",
fontSize = 12.sp,
color = Color(0xFFEF4444),
color = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 12.dp),
lineHeight = 16.sp

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.ui.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -30,6 +31,9 @@ fun OtherProfileScreen(
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
// Handle back gesture
BackHandler { onBack() }
Column(
modifier = Modifier
@@ -175,7 +179,7 @@ fun OtherProfileScreen(
} else {
ProfileNavigationItem(
icon = Icons.Outlined.Block,
iconBackground = Color(0xFFEF4444), // red
iconBackground = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
title = "Block this user",
subtitle = "Prevent this user from messaging you",
onClick = {
@@ -184,7 +188,7 @@ fun OtherProfileScreen(
},
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFFEF4444)
textColor = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
)
}
}

View File

@@ -0,0 +1,166 @@
package com.rosetta.messenger.ui.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ProfileLogsScreen(
isDarkTheme: Boolean,
logs: List<String>,
onBack: () -> Unit,
onClearLogs: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val successColor = if (isDarkTheme) Color(0xFF4CAF50) else Color(0xFF2E7D32)
val errorColor = if (isDarkTheme) Color(0xFFEF5350) else Color(0xFFD32F2F)
val listState = rememberLazyListState()
// Auto-scroll to bottom when new logs added
LaunchedEffect(logs.size) {
if (logs.isNotEmpty()) {
listState.animateScrollToItem(logs.size - 1)
}
}
// Handle back gesture
BackHandler { onBack() }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
// Top Bar
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = textColor
)
}
Text(
text = "Profile Logs",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
)
IconButton(onClick = onClearLogs) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Clear logs",
tint = secondaryTextColor
)
}
}
}
Divider(color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8))
// Logs content
if (logs.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "No logs yet.\nSave profile to see logs.",
fontSize = 14.sp,
color = secondaryTextColor,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
} else {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp, vertical = 8.dp)
) {
items(logs) { log ->
LogItem(
log = log,
textColor = textColor,
successColor = successColor,
errorColor = errorColor,
isDarkTheme = isDarkTheme
)
}
}
}
}
}
@Composable
private fun LogItem(
log: String,
textColor: Color,
successColor: Color,
errorColor: Color,
isDarkTheme: Boolean
) {
val logColor = when {
log.contains("✅ SUCCESS") -> successColor
log.contains("❌ ERROR") -> errorColor
log.contains("===") -> if (isDarkTheme) Color(0xFF9C27B0) else Color(0xFF6A1B9A)
else -> textColor
}
val backgroundColor = when {
log.contains("✅ SUCCESS") -> if (isDarkTheme) Color(0xFF1B5E20).copy(alpha = 0.2f) else Color(0xFFC8E6C9)
log.contains("❌ ERROR") -> if (isDarkTheme) Color(0xFFB71C1C).copy(alpha = 0.2f) else Color(0xFFFFCDD2)
else -> Color.Transparent
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
color = backgroundColor,
shape = androidx.compose.foundation.shape.RoundedCornerShape(4.dp)
) {
Text(
text = log,
fontSize = 12.sp,
color = logColor,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
lineHeight = 16.sp
)
}
}

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -97,11 +98,14 @@ fun ProfileScreen(
accountName: String,
accountUsername: String,
accountPublicKey: String,
accountPrivateKeyHash: String,
onBack: () -> Unit,
onSaveProfile: (name: String, username: String) -> Unit,
onLogout: () -> Unit,
onNavigateToTheme: () -> Unit = {},
onNavigateToSafety: () -> Unit = {}
onNavigateToSafety: () -> Unit = {},
onNavigateToLogs: () -> Unit = {},
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
// Цвета в зависимости от темы - такие же как в ChatsListScreen
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
@@ -115,6 +119,36 @@ fun ProfileScreen(
var editedName by remember { mutableStateOf(accountName) }
var editedUsername by remember { mutableStateOf(accountUsername) }
var hasChanges by remember { mutableStateOf(false) }
// ViewModel state
val profileState by viewModel.state.collectAsState()
// Show success toast
val context = LocalContext.current
LaunchedEffect(profileState.saveSuccess) {
if (profileState.saveSuccess) {
android.widget.Toast.makeText(
context,
"Profile updated successfully",
android.widget.Toast.LENGTH_SHORT
).show()
hasChanges = false
viewModel.resetSaveState()
onSaveProfile(editedName, editedUsername)
}
}
// Show error toast
LaunchedEffect(profileState.error) {
profileState.error?.let { error ->
android.widget.Toast.makeText(
context,
"Error: $error",
android.widget.Toast.LENGTH_SHORT
).show()
viewModel.resetSaveState()
}
}
// Update hasChanges when fields change
LaunchedEffect(editedName, editedUsername) {
@@ -142,8 +176,15 @@ fun ProfileScreen(
onBack = null, // Кнопка назад будет снаружи
hasChanges = hasChanges,
onSave = {
onSaveProfile(editedName, editedUsername)
hasChanges = false
// Save via ViewModel
viewModel.saveProfile(
publicKey = accountPublicKey,
privateKeyHash = accountPrivateKeyHash,
name = editedName,
username = editedUsername
)
// Also update local account name
viewModel.updateLocalAccountName(accountPublicKey, editedName)
}
)
@@ -175,8 +216,7 @@ fun ProfileScreen(
value = editedUsername,
onValueChange = { editedUsername = it },
placeholder = "ex. freddie871",
isDarkTheme = isDarkTheme,
leadingText = "@"
isDarkTheme = isDarkTheme
)
}
}
@@ -244,7 +284,37 @@ fun ProfileScreen(
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// 🚪 LOGOUT SECTION
// <EFBFBD> DEBUG / LOGS SECTION
// ═════════════════════════════════════════════════════════════
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
ProfileNavigationItem(
icon = Icons.Outlined.BugReport,
iconBackground = Color(0xFFFB8C00), // orange
title = "View Logs",
subtitle = "Debug profile save operations",
onClick = onNavigateToLogs,
isDarkTheme = isDarkTheme
)
}
Text(
text = "View detailed logs of profile save operations for debugging.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// ═════════════════════════════════════════════════════════════
// <20>🚪 LOGOUT SECTION
// ═════════════════════════════════════════════════════════════
Surface(
modifier = Modifier
@@ -255,13 +325,13 @@ fun ProfileScreen(
) {
ProfileNavigationItem(
icon = Icons.Outlined.Logout,
iconBackground = Color(0xFFEF4444), // red
iconBackground = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444),
title = "Logout",
subtitle = "Sign out of your account",
onClick = onLogout,
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFFEF4444)
textColor = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
)
}
@@ -341,6 +411,7 @@ fun ProfileCard(
onClick = onBack,
modifier = Modifier
.align(Alignment.TopStart)
.statusBarsPadding()
.padding(4.dp)
) {
Icon(
@@ -358,6 +429,7 @@ fun ProfileCard(
exit = fadeOut() + shrinkHorizontally(),
modifier = Modifier
.align(Alignment.TopEnd)
.statusBarsPadding()
.padding(4.dp)
) {
TextButton(onClick = onSave) {
@@ -372,13 +444,13 @@ fun ProfileCard(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp, bottom = 24.dp),
.padding(top = 80.dp, bottom = 48.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 👤 Large Avatar - Telegram style
Box(
modifier = Modifier
.size(120.dp)
.size(140.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f))
.padding(3.dp)
@@ -497,6 +569,8 @@ private fun ProfileEditableField(
color = textColor,
fontSize = 16.sp
),
singleLine = true,
cursorBrush = androidx.compose.ui.graphics.SolidColor(textColor),
decorationBox = { innerTextField ->
if (value.isEmpty()) {
Text(

View File

@@ -0,0 +1,174 @@
package com.rosetta.messenger.ui.settings
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.network.Packet
import com.rosetta.messenger.network.PacketResult
import com.rosetta.messenger.network.PacketUserInfo
import com.rosetta.messenger.network.ProtocolManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
data class ProfileState(
val isLoading: Boolean = false,
val isSaving: Boolean = false,
val error: String? = null,
val saveSuccess: Boolean = false,
val logs: List<String> = emptyList()
)
class ProfileViewModel(application: Application) : AndroidViewModel(application) {
private val accountManager = AccountManager(application)
private val _state = MutableStateFlow(ProfileState())
val state: StateFlow<ProfileState> = _state
private var resultCallback: ((Packet) -> Unit)? = null
init {
// Register listener for PacketResult (0x02)
resultCallback = { packet ->
if (packet is PacketResult) {
handlePacketResult(packet)
}
}
ProtocolManager.waitPacket(0x02, resultCallback!!)
}
override fun onCleared() {
super.onCleared()
// Unregister listener
resultCallback?.let { ProtocolManager.unwaitPacket(0x02, it) }
}
private fun addLog(message: String) {
val timestamp = java.text.SimpleDateFormat("HH:mm:ss.SSS", java.util.Locale.getDefault()).format(java.util.Date())
val logMessage = "[$timestamp] $message"
_state.value = _state.value.copy(logs = _state.value.logs + logMessage)
Log.d("ProfileViewModel", logMessage)
}
/**
* Save profile (name and username) to server
*/
fun saveProfile(
publicKey: String,
privateKeyHash: String,
name: String,
username: String
) {
viewModelScope.launch {
try {
addLog("=== Starting profile save ===")
addLog("Public Key: ${publicKey.take(20)}...")
addLog("Name: '$name'")
addLog("Username: '$username'")
_state.value = _state.value.copy(isSaving = true, error = null, saveSuccess = false)
// Create and send PacketUserInfo
val packet = PacketUserInfo()
packet.publicKey = publicKey
packet.title = name
packet.username = username
addLog("Packet created: PacketUserInfo")
addLog("Packet ID: 0x${packet.getPacketId().toString(16).uppercase()}")
addLog("Sending packet to server...")
ProtocolManager.send(packet)
addLog("Packet sent successfully")
// Wait for response (handled in handlePacketResult)
} catch (e: Exception) {
Log.e("ProfileViewModel", "Error saving profile", e)
_state.value = _state.value.copy(
isSaving = false,
error = e.message ?: "Unknown error"
)
}
}
}
/**
* Handle PacketResult from server
*/
private fun handlePacketResult(packet: PacketResult) {
viewModelScope.launch {
addLog("Received PacketResult from server")
addLog("Result code: ${packet.resultCode}")
addLog("Result message: '${packet.message}'")
when (packet.resultCode) {
0 -> { // SUCCESS
addLog("✅ SUCCESS: Profile saved successfully")
_state.value = _state.value.copy(
isSaving = false,
saveSuccess = true,
error = null
)
addLog("State updated: saveSuccess=true")
// Update local account name if needed
// (username is stored on server only)
}
else -> { // ERROR
addLog("❌ ERROR: Profile save failed")
addLog("Error details: ${packet.message}")
_state.value = _state.value.copy(
isSaving = false,
error = packet.message.ifEmpty { "Failed to save profile" }
)
addLog("State updated: error='${packet.message}'")
}
}
addLog("=== Profile save completed ===")
}
}
/**
* Update local account name
*/
fun updateLocalAccountName(publicKey: String, name: String) {
viewModelScope.launch {
try {
val account = accountManager.getAccount(publicKey)
if (account != null) {
val updatedAccount = EncryptedAccount(
publicKey = account.publicKey,
encryptedPrivateKey = account.encryptedPrivateKey,
encryptedSeedPhrase = account.encryptedSeedPhrase,
name = name
)
accountManager.saveAccount(updatedAccount)
Log.d("ProfileViewModel", "Local account name updated")
}
} catch (e: Exception) {
Log.e("ProfileViewModel", "Error updating local account name", e)
}
}
}
/**
* Reset save success state
*/
fun resetSaveState() {
_state.value = _state.value.copy(saveSuccess = false, error = null)
}
/**
* Clear all logs
*/
fun clearLogs() {
_state.value = _state.value.copy(logs = emptyList())
addLog("Logs cleared")
}
}

View File

@@ -1,27 +1,37 @@
package com.rosetta.messenger.ui.settings
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun SafetyScreen(
isDarkTheme: Boolean,
accountPublicKey: String,
accountPrivateKey: String = "",
onBack: () -> Unit,
onBackupClick: () -> Unit,
onDeleteAccount: () -> Unit
@@ -30,12 +40,30 @@ fun SafetyScreen(
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val redColor = if (isDarkTheme) Color(0xFFFF8787) else Color(0xFFEF4444)
val greenColor = if (isDarkTheme) Color(0xFF3FC158) else Color(0xFF34C759)
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
// Copy states
var copiedPublicKey by remember { mutableStateOf(false) }
var copiedPrivateKey by remember { mutableStateOf(false) }
// Handle back gesture
BackHandler { onBack() }
// Short display keys
val shortPublicKey = if (accountPublicKey.length > 20)
accountPublicKey.take(20) else accountPublicKey
val shortPrivateKey = if (accountPrivateKey.length > 20)
accountPrivateKey.take(20) else "B8zPXVMQrYkxaWE39v"
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(
@@ -52,11 +80,11 @@ fun SafetyScreen(
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back",
tint = if (isDarkTheme) Color.White else Color.Black
tint = textColor
)
}
Text(
text = "Safety & Security",
text = "Safety",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
@@ -72,47 +100,175 @@ fun SafetyScreen(
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
// Warning Card
// ═══════════════════════════════════════════════════════════════
// Public Key - clickable row with copy feedback
// ═══════════════════════════════════════════════════════════════
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color(0xFFFF9800).copy(alpha = 0.15f),
modifier = Modifier
.fillMaxWidth()
.clickable {
clipboardManager.setText(AnnotatedString(accountPublicKey))
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Public Key", accountPublicKey)
cm.setPrimaryClip(clip)
copiedPublicKey = true
scope.launch {
delay(2000)
copiedPublicKey = false
}
},
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Top
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = null,
tint = Color(0xFFFF9800),
modifier = Modifier
.size(24.dp)
.padding(top = 2.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Never share your private key or seed phrase with anyone. Keep your backup secure and private.",
fontSize = 13.sp,
text = "Public Key",
fontSize = 15.sp,
color = textColor,
lineHeight = 18.sp
fontWeight = FontWeight.Normal
)
if (copiedPublicKey) {
Text(
text = "copied",
fontSize = 15.sp,
color = greenColor
)
} else {
Text(
text = shortPublicKey,
fontSize = 15.sp,
color = secondaryTextColor
)
}
}
}
Text(
text = "This is your public key. If you haven't set a @username yet, you can ask a friend to message you using your public key.",
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(16.dp))
// ═══════════════════════════════════════════════════════════════
// Private Key - clickable row with copy feedback
// ═══════════════════════════════════════════════════════════════
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable {
if (accountPrivateKey.isNotEmpty()) {
clipboardManager.setText(AnnotatedString(accountPrivateKey))
val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Private Key", accountPrivateKey)
cm.setPrimaryClip(clip)
copiedPrivateKey = true
scope.launch {
delay(2000)
copiedPrivateKey = false
}
}
},
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Private key",
fontSize = 15.sp,
color = textColor,
fontWeight = FontWeight.Normal
)
if (copiedPrivateKey) {
Text(
text = "copied",
fontSize = 15.sp,
color = greenColor
)
} else {
Text(
text = shortPrivateKey,
fontSize = 15.sp,
color = secondaryTextColor
)
}
}
}
Text(
text = "This is your private key. For security reasons, we provide it only in an encrypted form so you can simply admire it. If anyone asks you for this key, please do not share it.",
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(16.dp))
// ═══════════════════════════════════════════════════════════════
// Backup - with chevron like desktop
// ═══════════════════════════════════════════════════════════════
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onBackupClick),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Backup",
fontSize = 15.sp,
color = redColor,
fontWeight = FontWeight.Normal
)
Icon(
imageVector = Icons.Filled.ChevronRight,
contentDescription = null,
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Public Key Section
Text(
text = "Public Key",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
text = "Please save your seed phrase, it is necessary for future access to your conversations. Do not share this seed phrase with anyone, otherwise, the person you share it with will gain access to your conversations.",
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(16.dp))
// ═══════════════════════════════════════════════════════════════
// Delete Account
// ═══════════════════════════════════════════════════════════════
Surface(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onDeleteAccount),
color = surfaceColor,
shape = RoundedCornerShape(12.dp)
) {
@@ -123,106 +279,23 @@ fun SafetyScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = accountPublicKey.take(20) + "..." + accountPublicKey.takeLast(20),
fontSize = 12.sp,
color = textColor,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = { /* TODO: Copy to clipboard */ }
) {
Icon(
imageVector = Icons.Outlined.ContentCopy,
contentDescription = "Copy",
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
}
}
Text(
text = "This is your public key. You can safely share this with others so they can message you.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Backup Section
Text(
text = "Backup & Recovery",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
ProfileNavigationItem(
icon = Icons.Outlined.ContentCopy,
iconBackground = Color(0xFFFF9800),
title = "Backup Seed Phrase",
subtitle = "View and save your recovery phrase",
onClick = onBackupClick,
isDarkTheme = isDarkTheme,
textColor = Color(0xFFFF9800)
text = "Delete Account",
fontSize = 15.sp,
color = redColor,
fontWeight = FontWeight.Normal
)
}
}
Text(
text = "Please save your seed phrase, it is necessary for future access to your conversations. Do not share this seed phrase with anyone.",
fontSize = 12.sp,
text = "This action cannot be undone, it will result in the complete deletion of account data from your device. Please note, this will also delete your data on the server, such as your avatar, encrypted messages, and username.",
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(24.dp))
// Danger Zone
Text(
text = "Danger Zone",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color(0xFFEF4444),
modifier = Modifier.padding(bottom = 8.dp)
)
Surface(
modifier = Modifier.fillMaxWidth(),
color = surfaceColor,
shape = RoundedCornerShape(16.dp)
) {
Column {
ProfileNavigationItem(
icon = Icons.Outlined.ContentCopy,
iconBackground = Color(0xFFEF4444),
title = "Delete Account",
subtitle = "Permanently delete your account",
onClick = onDeleteAccount,
isDarkTheme = isDarkTheme,
hideChevron = true,
textColor = Color(0xFFEF4444)
)
}
}
Text(
text = "This action cannot be undone. It will result in the complete deletion of account data from your device and server.",
fontSize = 12.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp),
lineHeight = 16.sp
)
Spacer(modifier = Modifier.height(32.dp))
}
}
}

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.ui.settings
import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
@@ -40,12 +41,14 @@ fun ThemeScreen(
// Theme mode: "light", "dark", "auto"
var themeMode by remember { mutableStateOf(if (isDarkTheme) "dark" else "light") }
// Handle back gesture
BackHandler { onBack() }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.ui.settings
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -25,12 +26,14 @@ fun UpdatesScreen(
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
// Handle back gesture
BackHandler { onBack() }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
// Top Bar
Surface(