feat: implement group members caching and enhance emoji picker functionality in GroupSetupScreen
This commit is contained in:
@@ -149,6 +149,40 @@ private data class GroupSharedStats(
|
|||||||
val linksCount: Int = 0
|
val linksCount: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class GroupMembersCacheEntry(
|
||||||
|
val members: List<String>,
|
||||||
|
val memberInfoByKey: Map<String, SearchUser>,
|
||||||
|
val updatedAtMs: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
private object GroupMembersMemoryCache {
|
||||||
|
private const val TTL_MS = 90_000L
|
||||||
|
private val cache = mutableMapOf<String, GroupMembersCacheEntry>()
|
||||||
|
|
||||||
|
fun getAny(key: String): GroupMembersCacheEntry? = synchronized(cache) { cache[key] }
|
||||||
|
|
||||||
|
fun getFresh(key: String): GroupMembersCacheEntry? = synchronized(cache) {
|
||||||
|
val entry = cache[key] ?: return@synchronized null
|
||||||
|
if (System.currentTimeMillis() - entry.updatedAtMs <= TTL_MS) entry else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun put(key: String, members: List<String>, memberInfoByKey: Map<String, SearchUser>) {
|
||||||
|
if (key.isBlank()) return
|
||||||
|
synchronized(cache) {
|
||||||
|
cache[key] =
|
||||||
|
GroupMembersCacheEntry(
|
||||||
|
members = members,
|
||||||
|
memberInfoByKey = memberInfoByKey,
|
||||||
|
updatedAtMs = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(key: String) {
|
||||||
|
synchronized(cache) { cache.remove(key) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class GroupMediaItem(
|
private data class GroupMediaItem(
|
||||||
val key: String,
|
val key: String,
|
||||||
val attachment: MessageAttachment,
|
val attachment: MessageAttachment,
|
||||||
@@ -249,9 +283,13 @@ fun GroupInfoScreen(
|
|||||||
var isMuted by remember { mutableStateOf(false) }
|
var isMuted by remember { mutableStateOf(false) }
|
||||||
var showAddMembersPicker by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
|
var showAddMembersPicker by rememberSaveable(dialogPublicKey) { mutableStateOf(false) }
|
||||||
var pendingInviteText by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
|
var pendingInviteText by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
|
||||||
|
var isRefreshingMembers by remember(dialogPublicKey) { mutableStateOf(false) }
|
||||||
|
|
||||||
var members by remember(dialogPublicKey) { mutableStateOf<List<String>>(emptyList()) }
|
var members by remember(dialogPublicKey) { mutableStateOf<List<String>>(emptyList()) }
|
||||||
val memberInfoByKey = remember(dialogPublicKey) { mutableStateMapOf<String, SearchUser>() }
|
val memberInfoByKey = remember(dialogPublicKey) { mutableStateMapOf<String, SearchUser>() }
|
||||||
|
val membersCacheKey = remember(currentUserPublicKey, normalizedGroupId) {
|
||||||
|
"${currentUserPublicKey.trim().lowercase()}|${normalizedGroupId.trim().lowercase()}"
|
||||||
|
}
|
||||||
|
|
||||||
val groupEntity by produceState<com.rosetta.messenger.database.GroupEntity?>(
|
val groupEntity by produceState<com.rosetta.messenger.database.GroupEntity?>(
|
||||||
initialValue = null,
|
initialValue = null,
|
||||||
@@ -341,16 +379,32 @@ fun GroupInfoScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshMembers() {
|
fun refreshMembers(force: Boolean = false, showLoader: Boolean = true) {
|
||||||
if (normalizedGroupId.isBlank()) return
|
if (normalizedGroupId.isBlank()) return
|
||||||
|
if (isRefreshingMembers && !force) return
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
membersLoading = true
|
if (!force) {
|
||||||
|
GroupMembersMemoryCache.getFresh(membersCacheKey)?.let { cached ->
|
||||||
|
members = cached.members
|
||||||
|
memberInfoByKey.clear()
|
||||||
|
memberInfoByKey.putAll(cached.memberInfoByKey)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasAnyCache = GroupMembersMemoryCache.getAny(membersCacheKey) != null
|
||||||
|
val shouldShowLoader = showLoader && members.isEmpty() && !hasAnyCache
|
||||||
|
if (shouldShowLoader) membersLoading = true
|
||||||
|
isRefreshingMembers = true
|
||||||
|
try {
|
||||||
val fetchedMembers = withContext(Dispatchers.IO) {
|
val fetchedMembers = withContext(Dispatchers.IO) {
|
||||||
groupRepository.requestGroupMembers(normalizedGroupId).orEmpty()
|
groupRepository.requestGroupMembers(normalizedGroupId).orEmpty()
|
||||||
}
|
}
|
||||||
members = fetchedMembers.distinct()
|
val distinctMembers = fetchedMembers.distinct()
|
||||||
membersLoading = false
|
if (distinctMembers.isNotEmpty() || members.isEmpty()) {
|
||||||
|
members = distinctMembers
|
||||||
|
}
|
||||||
|
|
||||||
if (members.isEmpty()) return@launch
|
if (members.isEmpty()) return@launch
|
||||||
|
|
||||||
@@ -371,11 +425,30 @@ fun GroupInfoScreen(
|
|||||||
if (resolvedUsers.isNotEmpty()) {
|
if (resolvedUsers.isNotEmpty()) {
|
||||||
memberInfoByKey.putAll(resolvedUsers)
|
memberInfoByKey.putAll(resolvedUsers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupMembersMemoryCache.put(
|
||||||
|
key = membersCacheKey,
|
||||||
|
members = members,
|
||||||
|
memberInfoByKey = memberInfoByKey.toMap()
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
if (shouldShowLoader) membersLoading = false
|
||||||
|
isRefreshingMembers = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(normalizedGroupId) {
|
LaunchedEffect(membersCacheKey) {
|
||||||
refreshMembers()
|
val cachedEntry = GroupMembersMemoryCache.getAny(membersCacheKey)
|
||||||
|
cachedEntry?.let { cached ->
|
||||||
|
members = cached.members
|
||||||
|
memberInfoByKey.clear()
|
||||||
|
memberInfoByKey.putAll(cached.memberInfoByKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GroupMembersMemoryCache.getFresh(membersCacheKey) == null) {
|
||||||
|
refreshMembers(force = true, showLoader = cachedEntry == null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val onlineCount by remember(members, memberInfoByKey) {
|
val onlineCount by remember(members, memberInfoByKey) {
|
||||||
@@ -541,6 +614,7 @@ fun GroupInfoScreen(
|
|||||||
}
|
}
|
||||||
isLeaving = false
|
isLeaving = false
|
||||||
if (left) {
|
if (left) {
|
||||||
|
GroupMembersMemoryCache.remove(membersCacheKey)
|
||||||
onGroupLeft()
|
onGroupLeft()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to leave group", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Failed to leave group", Toast.LENGTH_SHORT).show()
|
||||||
@@ -580,7 +654,12 @@ fun GroupInfoScreen(
|
|||||||
if (removed) {
|
if (removed) {
|
||||||
members = members.filterNot { it.trim().equals(memberKey, ignoreCase = true) }
|
members = members.filterNot { it.trim().equals(memberKey, ignoreCase = true) }
|
||||||
memberInfoByKey.remove(member.publicKey)
|
memberInfoByKey.remove(member.publicKey)
|
||||||
refreshMembers()
|
GroupMembersMemoryCache.put(
|
||||||
|
key = membersCacheKey,
|
||||||
|
members = members,
|
||||||
|
memberInfoByKey = memberInfoByKey.toMap()
|
||||||
|
)
|
||||||
|
refreshMembers(force = true, showLoader = false)
|
||||||
Toast.makeText(context, "Member removed", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Member removed", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to remove member", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Failed to remove member", Toast.LENGTH_SHORT).show()
|
||||||
@@ -1117,43 +1196,86 @@ fun GroupInfoScreen(
|
|||||||
|
|
||||||
if (showEncryptionDialog) {
|
if (showEncryptionDialog) {
|
||||||
val displayLines = remember(encryptionKey) { encodeGroupKeyForDisplay(encryptionKey) }
|
val displayLines = remember(encryptionKey) { encodeGroupKeyForDisplay(encryptionKey) }
|
||||||
|
val keyImagePalette = if (isDarkTheme) {
|
||||||
|
listOf(
|
||||||
|
Color(0xFF2B4F78),
|
||||||
|
Color(0xFF2F5F90),
|
||||||
|
Color(0xFF3D74A8),
|
||||||
|
Color(0xFF4E89BE),
|
||||||
|
Color(0xFF64A0D6)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
Color(0xFFD5E8FF),
|
||||||
|
Color(0xFFBBD9FF),
|
||||||
|
Color(0xFFA1CAFF),
|
||||||
|
Color(0xFF87BAFF),
|
||||||
|
Color(0xFF6EA9F4)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val keyCardColor = if (isDarkTheme) Color(0xFF1F1F22) else Color(0xFFF7F9FC)
|
||||||
|
val keyCodeColor = if (isDarkTheme) Color(0xFFC7D6EA) else Color(0xFF34495E)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showEncryptionDialog = false },
|
onDismissRequest = { showEncryptionDialog = false },
|
||||||
title = { Text("Encryption key") },
|
containerColor = cardColor,
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Encryption key",
|
||||||
|
color = primaryText,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
Box(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = keyCardColor,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 14.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
DesktopStyleKeyImage(
|
DesktopStyleKeyImage(
|
||||||
keyRender = encryptionKey,
|
keyRender = encryptionKey,
|
||||||
size = 180.dp,
|
size = 180.dp,
|
||||||
radius = 14.dp
|
radius = 14.dp,
|
||||||
|
palette = keyImagePalette
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
}
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Column {
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = sectionColor,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) {
|
||||||
if (displayLines.isNotEmpty()) {
|
if (displayLines.isNotEmpty()) {
|
||||||
displayLines.forEach { line ->
|
displayLines.forEach { line ->
|
||||||
Text(
|
Text(
|
||||||
text = line,
|
text = line,
|
||||||
color = secondaryText,
|
color = keyCodeColor,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = FontFamily.Monospace
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "This key encrypts and decrypts group messages.",
|
text = "This key encrypts and decrypts group messages.",
|
||||||
color = secondaryText,
|
color = secondaryText,
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -1167,7 +1289,7 @@ fun GroupInfoScreen(
|
|||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showEncryptionDialog = false }) {
|
TextButton(onClick = { showEncryptionDialog = false }) {
|
||||||
Text("Close", color = secondaryText)
|
Text("Close", color = primaryText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1244,15 +1366,17 @@ fun GroupInfoScreen(
|
|||||||
private fun DesktopStyleKeyImage(
|
private fun DesktopStyleKeyImage(
|
||||||
keyRender: String,
|
keyRender: String,
|
||||||
size: androidx.compose.ui.unit.Dp,
|
size: androidx.compose.ui.unit.Dp,
|
||||||
radius: androidx.compose.ui.unit.Dp = 0.dp
|
radius: androidx.compose.ui.unit.Dp = 0.dp,
|
||||||
|
palette: List<Color> = KEY_IMAGE_COLORS
|
||||||
) {
|
) {
|
||||||
val composition = remember(keyRender) {
|
val colors = if (palette.isNotEmpty()) palette else KEY_IMAGE_COLORS
|
||||||
|
val composition = remember(keyRender, colors) {
|
||||||
buildList(64) {
|
buildList(64) {
|
||||||
val source = if (keyRender.isBlank()) "rosetta" else keyRender
|
val source = if (keyRender.isBlank()) "rosetta" else keyRender
|
||||||
for (i in 0 until 64) {
|
for (i in 0 until 64) {
|
||||||
val code = source[i % source.length].code
|
val code = source[i % source.length].code
|
||||||
val colorIndex = code % KEY_IMAGE_COLORS.size
|
val colorIndex = code % colors.size
|
||||||
add(KEY_IMAGE_COLORS[colorIndex])
|
add(colors[colorIndex])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1385,7 @@ private fun DesktopStyleKeyImage(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(RoundedCornerShape(radius))
|
.clip(RoundedCornerShape(radius))
|
||||||
.background(KEY_IMAGE_COLORS.first())
|
.background(colors.first())
|
||||||
) {
|
) {
|
||||||
val cells = 8
|
val cells = 8
|
||||||
val cellSize = this.size.minDimension / cells.toFloat()
|
val cellSize = this.size.minDimension / cells.toFloat()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.rosetta.messenger.ui.chats
|
package com.rosetta.messenger.ui.chats
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
@@ -17,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -39,6 +40,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -75,12 +77,15 @@ import com.rosetta.messenger.data.GroupRepository
|
|||||||
import com.rosetta.messenger.network.GroupStatus
|
import com.rosetta.messenger.network.GroupStatus
|
||||||
import com.rosetta.messenger.network.SearchUser
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
|
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
||||||
import com.rosetta.messenger.ui.components.AvatarImage
|
import com.rosetta.messenger.ui.components.AvatarImage
|
||||||
import com.rosetta.messenger.ui.icons.TelegramIcons
|
import com.rosetta.messenger.ui.icons.TelegramIcons
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
import com.rosetta.messenger.utils.ImageCropHelper
|
import com.rosetta.messenger.utils.ImageCropHelper
|
||||||
import com.rosetta.messenger.ui.settings.ProfilePhotoPicker
|
import com.rosetta.messenger.ui.settings.ProfilePhotoPicker
|
||||||
|
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
|
||||||
|
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -118,6 +123,9 @@ fun GroupSetupScreen(
|
|||||||
var errorText by rememberSaveable { mutableStateOf<String?>(null) }
|
var errorText by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
var showEmojiKeyboard by rememberSaveable { mutableStateOf(false) }
|
var showEmojiKeyboard by rememberSaveable { mutableStateOf(false) }
|
||||||
var showPhotoPicker by rememberSaveable { mutableStateOf(false) }
|
var showPhotoPicker by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val coordinator = rememberKeyboardTransitionCoordinator()
|
||||||
|
var lastToggleTime by remember { mutableLongStateOf(0L) }
|
||||||
|
val toggleCooldownMs = 500L
|
||||||
|
|
||||||
val cropLauncher =
|
val cropLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(
|
||||||
@@ -217,20 +225,90 @@ fun GroupSetupScreen(
|
|||||||
val actionEnabled = if (step == GroupSetupStep.DETAILS) canGoNext else canCreate
|
val actionEnabled = if (step == GroupSetupStep.DETAILS) canGoNext else canCreate
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val imeBottomPx = WindowInsets.ime.getBottom(density)
|
val imeBottomPx = WindowInsets.ime.getBottom(density)
|
||||||
val navBottomPx = WindowInsets.navigationBars.getBottom(density)
|
val imeBottomDp = with(density) { imeBottomPx.toDp() }
|
||||||
val keyboardHeightPx = (imeBottomPx - navBottomPx).coerceAtLeast(0)
|
val keyboardOrEmojiHeight =
|
||||||
|
if (coordinator.isEmojiBoxVisible) coordinator.emojiHeight else imeBottomDp
|
||||||
val fabBottomPadding =
|
val fabBottomPadding =
|
||||||
if (keyboardHeightPx > 0) {
|
if (keyboardOrEmojiHeight > 0.dp) {
|
||||||
with(density) { keyboardHeightPx.toDp() } + 14.dp
|
keyboardOrEmojiHeight + 14.dp
|
||||||
} else {
|
} else {
|
||||||
18.dp
|
18.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(step) {
|
LaunchedEffect(step) {
|
||||||
if (step != GroupSetupStep.DETAILS) {
|
if (step != GroupSetupStep.DETAILS) {
|
||||||
showEmojiKeyboard = false
|
if (showEmojiKeyboard || coordinator.isEmojiVisible || coordinator.isEmojiBoxVisible) {
|
||||||
|
coordinator.closeEmoji(hideEmoji = { showEmojiKeyboard = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val savedPx = KeyboardHeightProvider.getSavedKeyboardHeight(context)
|
||||||
|
if (savedPx > 0) {
|
||||||
|
coordinator.initializeEmojiHeight(with(density) { savedPx.toDp() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
|
||||||
|
LaunchedEffect(imeBottomPx) {
|
||||||
|
val currentImeHeight = with(density) { imeBottomPx.toDp() }
|
||||||
|
coordinator.updateKeyboardHeight(currentImeHeight)
|
||||||
|
if (currentImeHeight > 100.dp) {
|
||||||
|
coordinator.syncHeights()
|
||||||
|
lastStableKeyboardHeight = currentImeHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleEmojiPicker() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastToggleTime < toggleCooldownMs || step != GroupSetupStep.DETAILS || isLoading) return
|
||||||
|
lastToggleTime = now
|
||||||
|
|
||||||
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
if (coordinator.isEmojiVisible) {
|
||||||
|
coordinator.requestShowKeyboard(
|
||||||
|
showKeyboard = {
|
||||||
|
nameFocusRequester.requestFocus()
|
||||||
|
keyboardController?.show()
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
|
||||||
|
},
|
||||||
|
hideEmoji = { showEmojiKeyboard = false }
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imeBottomPx > 0) {
|
||||||
|
coordinator.requestShowEmoji(
|
||||||
|
hideKeyboard = {
|
||||||
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
showEmoji = {
|
||||||
|
focusManager.clearFocus(force = true)
|
||||||
|
showEmojiKeyboard = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coordinator.emojiHeight == 0.dp) {
|
||||||
|
val fallbackHeight = if (lastStableKeyboardHeight > 0.dp) {
|
||||||
|
lastStableKeyboardHeight
|
||||||
|
} else {
|
||||||
|
with(density) { KeyboardHeightProvider.getSavedKeyboardHeight(context).toDp() }
|
||||||
|
}
|
||||||
|
coordinator.initializeEmojiHeight(fallbackHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
coordinator.openEmojiOnly(
|
||||||
|
showEmoji = {
|
||||||
|
focusManager.clearFocus(force = true)
|
||||||
|
showEmojiKeyboard = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (step == GroupSetupStep.DETAILS) {
|
if (step == GroupSetupStep.DETAILS) {
|
||||||
@@ -270,7 +348,11 @@ fun GroupSetupScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (step == GroupSetupStep.DETAILS && showEmojiKeyboard) {
|
if (step == GroupSetupStep.DETAILS) {
|
||||||
|
AnimatedKeyboardTransition(
|
||||||
|
coordinator = coordinator,
|
||||||
|
showEmojiPicker = showEmojiKeyboard
|
||||||
|
) {
|
||||||
OptimizedEmojiPicker(
|
OptimizedEmojiPicker(
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
@@ -278,10 +360,12 @@ fun GroupSetupScreen(
|
|||||||
val emoji = decodeEmojiCodeToUnicode(emojiCode)
|
val emoji = decodeEmojiCodeToUnicode(emojiCode)
|
||||||
title = (title + emoji).take(80)
|
title = (title + emoji).take(80)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().navigationBarsPadding()
|
onClose = { toggleEmojiPicker() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -366,8 +450,11 @@ fun GroupSetupScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(nameFocusRequester)
|
.focusRequester(nameFocusRequester)
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
if (focusState.isFocused) {
|
if (focusState.isFocused &&
|
||||||
showEmojiKeyboard = false
|
showEmojiKeyboard &&
|
||||||
|
!coordinator.isTransitioning
|
||||||
|
) {
|
||||||
|
coordinator.closeEmoji(hideEmoji = { showEmojiKeyboard = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(vertical = 2.dp),
|
.padding(vertical = 2.dp),
|
||||||
@@ -391,21 +478,15 @@ fun GroupSetupScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = { toggleEmojiPicker() },
|
||||||
if (showEmojiKeyboard) {
|
|
||||||
showEmojiKeyboard = false
|
|
||||||
nameFocusRequester.requestFocus()
|
|
||||||
keyboardController?.show()
|
|
||||||
} else {
|
|
||||||
showEmojiKeyboard = true
|
|
||||||
focusManager.clearFocus(force = true)
|
|
||||||
keyboardController?.hide()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = !isLoading
|
enabled = !isLoading
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = TelegramIcons.Smile,
|
painter = if (showEmojiKeyboard || coordinator.isEmojiBoxVisible) {
|
||||||
|
TelegramIcons.Keyboard
|
||||||
|
} else {
|
||||||
|
TelegramIcons.Smile
|
||||||
|
},
|
||||||
contentDescription = "Emoji",
|
contentDescription = "Emoji",
|
||||||
tint = secondaryTextColor,
|
tint = secondaryTextColor,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
|
|||||||
Reference in New Issue
Block a user