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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user