feat: Refactor SearchResultsList component for improved loading and empty state handling
- Updated UI to use AnimatedVisibility for loading indicator and empty state message. - Enhanced user feedback with a centered loading spinner and message during search. - Improved layout and spacing for search results and user items. - Adjusted colors and sizes for better visual consistency. chore: Update gradle.properties to use new Java 17 path - Changed Java home path to reflect new installation location. docs: Add comprehensive README for project setup and development - Included quick start instructions, available emulators, technologies used, and useful commands. - Documented project structure and debugging tips. - Added license information and contact details for the development team. build: Add development scripts for quick rebuild and installation - Created dev.sh for fast rebuild and install without cleaning. - Added run.sh for a complete build and install process with emulator checks. - Introduced watch-dev.sh for automatic rebuild on file changes.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -79,12 +80,17 @@ dependencies {
|
||||
// Room for database
|
||||
implementation("androidx.room:room-runtime:2.6.1")
|
||||
implementation("androidx.room:room-ktx:2.6.1")
|
||||
annotationProcessor("androidx.room:room-compiler:2.6.1")
|
||||
kapt("androidx.room:room-compiler:2.6.1")
|
||||
|
||||
// Biometric authentication
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
|
||||
// Testing dependencies
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.13.8")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.10.01"))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material3.*
|
||||
@@ -22,205 +21,197 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.network.SearchUser
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
|
||||
/**
|
||||
* Компонент отображения результатов поиска пользователей
|
||||
* Аналогичен результатам поиска в React Native приложении
|
||||
* Компонент отображения результатов поиска пользователей Аналогичен результатам поиска в React
|
||||
* Native приложении
|
||||
*/
|
||||
@Composable
|
||||
fun SearchResultsList(
|
||||
searchResults: List<SearchUser>,
|
||||
isSearching: Boolean,
|
||||
currentUserPublicKey: String,
|
||||
isDarkTheme: Boolean,
|
||||
onUserClick: (SearchUser) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
searchResults: List<SearchUser>,
|
||||
isSearching: Boolean,
|
||||
currentUserPublicKey: String,
|
||||
isDarkTheme: Boolean,
|
||||
onUserClick: (SearchUser) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val backgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color.White
|
||||
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp))
|
||||
.background(backgroundColor)
|
||||
) {
|
||||
// Разделительная линия сверху
|
||||
Divider(
|
||||
color = borderColor,
|
||||
thickness = 1.dp
|
||||
)
|
||||
|
||||
when {
|
||||
isSearching -> {
|
||||
// Индикатор загрузки
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
AnimatedVisibility(
|
||||
visible = isSearching,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 200)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 200))
|
||||
) {
|
||||
// Индикатор загрузки в центре
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(32.dp),
|
||||
color = if (isDarkTheme) Color(0xFF9E9E9E) else PrimaryBlue,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(text = "Searching...", fontSize = 14.sp, color = secondaryTextColor)
|
||||
}
|
||||
searchResults.isEmpty() -> {
|
||||
// Пустые результаты - подсказка
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "You can search by username or public key.",
|
||||
fontSize = 12.sp,
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !isSearching && searchResults.isEmpty(),
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 300)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 200))
|
||||
) {
|
||||
// Подсказка в центре экрана
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = "Search by username or public key",
|
||||
fontSize = 15.sp,
|
||||
color = secondaryTextColor
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Список результатов
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp)
|
||||
) {
|
||||
itemsIndexed(searchResults) { index, user ->
|
||||
SearchResultItem(
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !isSearching && searchResults.isNotEmpty(),
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 300)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 200))
|
||||
) {
|
||||
// Список результатов без серой плашки
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(searchResults) { index, user ->
|
||||
SearchResultItem(
|
||||
user = user,
|
||||
isOwnAccount = user.publicKey == currentUserPublicKey,
|
||||
isDarkTheme = isDarkTheme,
|
||||
isLastItem = index == searchResults.size - 1,
|
||||
onClick = { onUserClick(user) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Элемент результата поиска - пользователь
|
||||
*/
|
||||
/** Элемент результата поиска - пользователь */
|
||||
@Composable
|
||||
private fun SearchResultItem(
|
||||
user: SearchUser,
|
||||
isOwnAccount: Boolean,
|
||||
isDarkTheme: Boolean,
|
||||
isLastItem: Boolean,
|
||||
onClick: () -> Unit
|
||||
user: SearchUser,
|
||||
isOwnAccount: Boolean,
|
||||
isDarkTheme: Boolean,
|
||||
isLastItem: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
|
||||
|
||||
|
||||
// Получаем цвета аватара
|
||||
val avatarColors = getAvatarColor(
|
||||
if (isOwnAccount) "SavedMessages" else (user.title.ifEmpty { user.publicKey }),
|
||||
isDarkTheme
|
||||
)
|
||||
|
||||
val avatarColors =
|
||||
getAvatarColor(
|
||||
if (isOwnAccount) "SavedMessages" else (user.title.ifEmpty { user.publicKey }),
|
||||
isDarkTheme
|
||||
)
|
||||
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Аватар
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (isOwnAccount) PrimaryBlue else avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
modifier =
|
||||
Modifier.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (isOwnAccount) PrimaryBlue
|
||||
else avatarColors.backgroundColor
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isOwnAccount) {
|
||||
Icon(
|
||||
Icons.Default.Bookmark,
|
||||
contentDescription = "Saved Messages",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
Icons.Default.Bookmark,
|
||||
contentDescription = "Saved Messages",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = if (user.title.isNotEmpty()) {
|
||||
getInitials(user.title)
|
||||
} else {
|
||||
user.publicKey.take(2).uppercase()
|
||||
},
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
text =
|
||||
if (user.title.isNotEmpty()) {
|
||||
getInitials(user.title)
|
||||
} else {
|
||||
user.publicKey.take(2).uppercase()
|
||||
},
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
|
||||
// Информация о пользователе
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
// Имя и значок верификации
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (isOwnAccount) {
|
||||
"Saved Messages"
|
||||
} else {
|
||||
user.title.ifEmpty { user.publicKey.take(10) }
|
||||
},
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
text =
|
||||
if (isOwnAccount) {
|
||||
"Saved Messages"
|
||||
} else {
|
||||
user.title.ifEmpty { user.publicKey.take(10) }
|
||||
},
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
|
||||
// Значок верификации
|
||||
if (!isOwnAccount && user.verified > 0) {
|
||||
VerifiedBadge(
|
||||
verified = user.verified,
|
||||
size = 16
|
||||
)
|
||||
VerifiedBadge(verified = user.verified, size = 16)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
|
||||
// Юзернейм или публичный ключ
|
||||
Text(
|
||||
text = if (isOwnAccount) {
|
||||
"Notes"
|
||||
} else {
|
||||
"@${user.username.ifEmpty { user.publicKey.take(10) + "..." }}"
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
text =
|
||||
if (isOwnAccount) {
|
||||
"Notes"
|
||||
} else {
|
||||
"@${user.username.ifEmpty { user.publicKey.take(10) + "..." }}"
|
||||
},
|
||||
fontSize = 14.sp,
|
||||
color = secondaryTextColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Разделитель между элементами
|
||||
if (!isLastItem) {
|
||||
Divider(
|
||||
modifier = Modifier.padding(start = 64.dp),
|
||||
color = dividerColor,
|
||||
thickness = 0.5.dp
|
||||
modifier = Modifier.padding(start = 80.dp),
|
||||
color = dividerColor,
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user