feat: add animated badge for top-level requests count in ChatsListScreen

This commit is contained in:
2026-02-14 00:31:05 +05:00
parent e399fd04aa
commit a46968cfff
7 changed files with 242 additions and 83 deletions

View File

@@ -27,6 +27,13 @@ object BackgroundBlurPresets {
label = "Avatar"
)
/** Вариант "none" — стандартные цвета шапки без blur и без overlay */
val noneDefault = BackgroundBlurOption(
id = "none",
colors = emptyList(),
label = "None"
)
/** Сплошные цвета */
private val solidColors = listOf(
BackgroundBlurOption("solid_blue", listOf(Color(0xFF0D8CF4)), "Blue"),
@@ -54,19 +61,19 @@ object BackgroundBlurPresets {
/** Все варианты в порядке отображения: сначала сплошные, потом градиенты */
val all: List<BackgroundBlurOption> = solidColors + gradients
/** Все варианты включая "Avatar" (default) */
val allWithDefault: List<BackgroundBlurOption> = listOf(avatarDefault) + all
/** Все варианты включая "Avatar" и "None" */
val allWithDefault: List<BackgroundBlurOption> = listOf(noneDefault, avatarDefault) + all
/**
* Найти вариант по id. Возвращает [avatarDefault] если не найден.
*/
fun findById(id: String): BackgroundBlurOption {
return allWithDefault.find { it.id == id } ?: avatarDefault
return allWithDefault.find { it.id == id } ?: noneDefault
}
/**
* Получить список цветов для overlay по id.
* Возвращает null если id == "avatar" (значит используется blur аватарки без overlay).
* Возвращает null если id == "avatar" или "none" (без overlay).
*/
fun getOverlayColors(id: String): List<Color>? {
val option = findById(id)

View File

@@ -252,7 +252,14 @@ private fun ProfileBlurPreview(
// ═══════════════════════════════════════════════════
// LAYER 2: Color/gradient overlay (или fallback)
// ═══════════════════════════════════════════════════
if (overlayColors != null && overlayColors.isNotEmpty()) {
if (selectedId == "none") {
// None — стандартный цвет шапки
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4))
)
} else if (overlayColors != null && overlayColors.isNotEmpty()) {
val overlayMod = if (overlayColors.size == 1) {
Modifier
.fillMaxSize()
@@ -475,6 +482,24 @@ private fun ColorCircleItem(
contentAlignment = Alignment.Center
) {
when {
option.id == "none" -> {
// None — стандартные цвета, перечеркнутый круг
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = TablerIcons.X,
contentDescription = "None",
tint = Color.White.copy(alpha = 0.9f),
modifier = Modifier.size(18.dp)
)
}
}
option.id == "avatar" -> {
Box(
modifier = Modifier
@@ -512,8 +537,8 @@ private fun ColorCircleItem(
}
}
// Галочка с затемнённым фоном (не для avatar — там уже иконка)
if (isSelected && option.id != "avatar") {
// Галочка с затемнённым фоном (не для avatar/none — там уже иконка)
if (isSelected && option.id != "avatar" && option.id != "none") {
Box(
modifier = Modifier
.fillMaxSize()

View File

@@ -577,19 +577,21 @@ fun OtherProfileScreen(
)
// ═══════════════════════════════════════════════════════════
// ✉️ WRITE MESSAGE BUTTON
// ✉️ WRITE MESSAGE + 📞 CALL BUTTONS
// ═══════════════════════════════════════════════════════════
Spacer(modifier = Modifier.height(12.dp))
Box(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// Write Message
Button(
onClick = { onWriteMessage(user) },
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
@@ -609,12 +611,43 @@ fun OtherProfileScreen(
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Write Message",
fontSize = 16.sp,
text = "Message",
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
}
// Call
Button(
onClick = { /* TODO: call action */ },
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE8E8ED),
contentColor = if (isDarkTheme) Color.White else Color.Black
),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
) {
Icon(
imageVector = TablerIcons.Phone,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = if (isDarkTheme) Color.White else PrimaryBlue
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Call",
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
color = if (isDarkTheme) Color.White else Color.Black
)
}
}
Spacer(modifier = Modifier.height(12.dp))
@@ -662,7 +695,8 @@ fun OtherProfileScreen(
state = pagerState,
modifier = Modifier.fillMaxWidth().heightIn(min = sharedPagerMinHeight),
beyondBoundsPageCount = 0,
verticalAlignment = Alignment.Top
verticalAlignment = Alignment.Top,
userScrollEnabled = false
) { page ->
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopStart) {
OtherProfileSharedTabContent(

View File

@@ -568,6 +568,14 @@ fun ProfileScreen(
hasTriggeredSnapBackHaptic = true
}
}
// Коллапс при удалении аватара — сразу сворачиваем
LaunchedEffect(hasAvatar) {
if (!hasAvatar) {
isPulledDown = false
overscrollOffset = 0f
}
}
// ═══════════════════════════════════════════════════════════════
// NESTED SCROLL — overscroll (pull-down аватарка) + header snap
@@ -605,8 +613,8 @@ fun ProfileScreen(
available: Offset,
source: NestedScrollSource
): Offset {
// Overscroll при свайпе вниз от верха (когда LazyColumn в начале)
if (available.y > 0 && !listState.canScrollBackward) {
// Overscroll при свайпе вниз от верха (только если есть аватар)
if (available.y > 0 && !listState.canScrollBackward && hasAvatar) {
isDragging = true
val resistance = if (isPulledDown) 1f else 0.5f
val delta = available.y * resistance
@@ -1126,15 +1134,24 @@ private fun CollapsingProfileHeader(
// и естественно перекрывает его. Без мерцания.
// ═══════════════════════════════════════════════════════════
Box(modifier = Modifier.matchParentSize()) {
BlurredAvatarBackground(
publicKey = publicKey,
avatarRepository = avatarRepository,
fallbackColor = avatarColors.backgroundColor,
blurRadius = 20f,
alpha = 0.9f,
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
isDarkTheme = isDarkTheme
)
if (backgroundBlurColorId == "none") {
// None — стандартный цвет шапки без blur
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4))
)
} else {
BlurredAvatarBackground(
publicKey = publicKey,
avatarRepository = avatarRepository,
fallbackColor = avatarColors.backgroundColor,
blurRadius = 20f,
alpha = 0.9f,
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
isDarkTheme = isDarkTheme
)
}
}
// ═══════════════════════════════════════════════════════════