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:
senseiGai
2026-01-11 17:14:02 +05:00
parent 5f21f120f1
commit 5e3b9d0882
9 changed files with 1428 additions and 1086 deletions

View File

@@ -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"))

View File

@@ -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
)
}
}