Enhance OnboardingScreen with smoother pager swipes and add pulse animation to Rosetta logo

- Implemented custom fling behavior for HorizontalPager to improve swipe experience.
- Added pulse animation effect to the Rosetta logo on the onboarding screen for better visual appeal.
- Updated the layout and animation specifications to enhance user interaction and aesthetics.
This commit is contained in:
k1ngsterr1
2026-01-08 20:04:51 +05:00
parent fc54cc89df
commit 307670e691
10 changed files with 1740 additions and 403 deletions

View File

@@ -0,0 +1,458 @@
package com.rosetta.messenger.ui.chats
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import java.text.SimpleDateFormat
import java.util.*
data class Chat(
val id: String,
val name: String,
val lastMessage: String,
val lastMessageTime: Date,
val unreadCount: Int = 0,
val isOnline: Boolean = false,
val publicKey: String,
val isSavedMessages: Boolean = false
)
// Beautiful avatar colors
private val avatarColors = listOf(
Color(0xFF5E9FFF) to Color(0xFFE8F1FF), // Blue
Color(0xFFFF7EB3) to Color(0xFFFFEEF4), // Pink
Color(0xFF7B68EE) to Color(0xFFF0EDFF), // Purple
Color(0xFF50C878) to Color(0xFFE8F8EE), // Green
Color(0xFFFF6B6B) to Color(0xFFFFEEEE), // Red
Color(0xFF4ECDC4) to Color(0xFFE8F8F7), // Teal
Color(0xFFFFB347) to Color(0xFFFFF5E8), // Orange
Color(0xFFBA55D3) to Color(0xFFF8EEFF) // Orchid
)
fun getAvatarColor(name: String, isDark: Boolean): Pair<Color, Color> {
val index = name.hashCode().mod(avatarColors.size).let { if (it < 0) it + avatarColors.size else it }
val (primary, light) = avatarColors[index]
return if (isDark) primary to primary.copy(alpha = 0.2f) else primary to light
}
fun getInitials(name: String): String {
val words = name.trim().split(Regex("\\s+")).filter { it.isNotEmpty() }
return when {
words.isEmpty() -> "??"
words.size == 1 -> words[0].take(2).uppercase()
else -> "${words[0].first()}${words[1].first()}".uppercase()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChatsListScreen(
isDarkTheme: Boolean,
chats: List<Chat>,
onChatClick: (Chat) -> Unit,
onNewChat: () -> Unit,
onProfileClick: () -> Unit,
onSavedMessagesClick: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
visible = true
}
Scaffold(
topBar = {
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400)) + slideInVertically(
initialOffsetY = { -it },
animationSpec = tween(400)
)
) {
TopAppBar(
title = {
Text(
"Chats",
fontWeight = FontWeight.Bold,
fontSize = 28.sp
)
},
actions = {
IconButton(onClick = onNewChat) {
Icon(
Icons.Default.Edit,
contentDescription = "New Chat",
tint = PrimaryBlue
)
}
IconButton(onClick = onProfileClick) {
Icon(
Icons.Default.Person,
contentDescription = "Profile",
tint = textColor
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = backgroundColor,
titleContentColor = textColor
)
)
}
},
containerColor = backgroundColor
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(vertical = 8.dp)
) {
// Saved Messages section
item {
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 100)) + slideInHorizontally(
initialOffsetX = { -50 },
animationSpec = tween(400, delayMillis = 100)
)
) {
SavedMessagesItem(
isDarkTheme = isDarkTheme,
onClick = onSavedMessagesClick
)
}
}
// Chat items
items(chats, key = { it.id }) { chat ->
val index = chats.indexOf(chat)
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 150 + (index * 50))) + slideInHorizontally(
initialOffsetX = { -50 },
animationSpec = tween(400, delayMillis = 150 + (index * 50))
)
) {
ChatItem(
chat = chat,
isDarkTheme = isDarkTheme,
onClick = { onChatClick(chat) }
)
}
}
// Empty state
if (chats.isEmpty()) {
item {
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 200)) + scaleIn(
initialScale = 0.9f,
animationSpec = tween(400, delayMillis = 200)
)
) {
EmptyChatsState(
isDarkTheme = isDarkTheme,
onNewChat = onNewChat
)
}
}
}
}
}
}
@Composable
private fun SavedMessagesItem(
isDarkTheme: Boolean,
onClick: () -> Unit
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Saved Messages icon
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Bookmark,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Saved Messages",
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Text(
text = "Your personal cloud storage",
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
HorizontalDivider(
modifier = Modifier.padding(start = 84.dp),
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
)
}
@Composable
private fun ChatItem(
chat: Chat,
isDarkTheme: Boolean,
onClick: () -> Unit
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val (avatarTextColor, avatarBgColor) = getAvatarColor(chat.name, isDarkTheme)
val initials = getInitials(chat.name)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(avatarBgColor),
contentAlignment = Alignment.Center
) {
Text(
text = initials,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = avatarTextColor
)
// Online indicator
if (chat.isOnline) {
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(14.dp)
.clip(CircleShape)
.background(if (isDarkTheme) Color(0xFF1E1E1E) else Color.White)
.padding(2.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50))
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = chat.name,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Text(
text = formatTime(chat.lastMessageTime),
fontSize = 13.sp,
color = if (chat.unreadCount > 0) PrimaryBlue else secondaryTextColor
)
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = chat.lastMessage,
fontSize = 15.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
if (chat.unreadCount > 0) {
Box(
modifier = Modifier
.padding(start = 8.dp)
.clip(CircleShape)
.background(PrimaryBlue)
.padding(horizontal = 8.dp, vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = if (chat.unreadCount > 99) "99+" else chat.unreadCount.toString(),
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
}
}
}
}
}
HorizontalDivider(
modifier = Modifier.padding(start = 84.dp),
color = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
)
}
@Composable
private fun EmptyChatsState(
isDarkTheme: Boolean,
onNewChat: () -> Unit
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(60.dp))
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.ChatBubbleOutline,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(40.dp)
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "No chats yet",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Start a conversation with\nsomeone new",
fontSize = 15.sp,
color = secondaryTextColor,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = onNewChat,
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Start Chat", fontSize = 16.sp)
}
}
}
private fun formatTime(date: Date): String {
val now = Calendar.getInstance()
val messageTime = Calendar.getInstance().apply { time = date }
return when {
// Today
now.get(Calendar.DATE) == messageTime.get(Calendar.DATE) -> {
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
}
// Yesterday
now.get(Calendar.DATE) - messageTime.get(Calendar.DATE) == 1 -> {
"Yesterday"
}
// This week
now.get(Calendar.WEEK_OF_YEAR) == messageTime.get(Calendar.WEEK_OF_YEAR) -> {
SimpleDateFormat("EEE", Locale.getDefault()).format(date)
}
// This year
now.get(Calendar.YEAR) == messageTime.get(Calendar.YEAR) -> {
SimpleDateFormat("MMM d", Locale.getDefault()).format(date)
}
// Other
else -> {
SimpleDateFormat("dd.MM.yy", Locale.getDefault()).format(date)
}
}
}