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

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