commit 4fd52c0c3c566b88260766eb7e44c012d055b359
parent a373c8fceb559147ca0fd5c9941bef7b7d00017b
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sat, 30 Mar 2024 10:57:54 +0100

feat(bulk_messaging_action): search bar

Diffstat:
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt | 8++++++--
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt | 147++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
2 files changed, 106 insertions(+), 49 deletions(-)

diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt @@ -3,6 +3,8 @@ package me.rhunk.snapenhance.common.ui import android.app.AlertDialog import android.content.Context import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.WindowManager import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner @@ -79,8 +81,10 @@ fun createComposeAlertDialog(context: Context, builder: AlertDialog.Builder.() - }) .create().apply { alertDialog = this - window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + Handler(Looper.getMainLooper()).post { + window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + } } } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/action/impl/BulkMessagingAction.kt @@ -4,7 +4,9 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.widget.ProgressBar +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* @@ -14,9 +16,11 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -85,7 +89,7 @@ class BulkMessagingAction : AbstractAction() { translation.format("progress_status", "index" to index.toString(), "total" to ids.size.toString()) ) } - delay(500) + delay(100) } withContext(Dispatchers.Main) { dialog.dismiss() @@ -115,10 +119,34 @@ class BulkMessagingAction : AbstractAction() { ) } + private fun filterFriends(friends: List<FriendInfo>, filter: Filter, nameFilter: String): List<FriendInfo> { + val userIdBlacklist = arrayOf( + context.database.myUserId, + "b42f1f70-5a8b-4c53-8c25-34e7ec9e6781", // myai + "84ee8839-3911-492d-8b94-72dd80f3713a", // teamsnapchat + ) + return friends.filter { friend -> + friend.userId !in userIdBlacklist && when (filter) { + Filter.ALL -> true + Filter.MY_FRIENDS -> friend.friendLinkType == FriendLinkType.MUTUAL.value && friend.addedTimestamp > 0 + Filter.BLOCKED -> friend.friendLinkType == FriendLinkType.BLOCKED.value + Filter.REMOVED_ME -> friend.friendLinkType == FriendLinkType.OUTGOING.value && friend.addedTimestamp > 0 && friend.businessCategory == 0 // ignore followed accounts + Filter.SUGGESTED -> friend.friendLinkType == FriendLinkType.SUGGESTED.value + Filter.DELETED -> friend.friendLinkType == FriendLinkType.DELETED.value + Filter.BUSINESS_ACCOUNTS -> friend.businessCategory > 0 + } && nameFilter.takeIf { it.isNotBlank() }?.let { name -> + friend.mutableUsername?.contains( + name, + ignoreCase = true + ) == true || friend.displayName?.contains(name, ignoreCase = true) == true + } ?: true + } + } - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable private fun BulkMessagingDialog() { + val coroutineScope = rememberCoroutineScope { Dispatchers.IO } var sortBy by remember { mutableStateOf(SortBy.USERNAME) } var filter by remember { mutableStateOf(Filter.REMOVED_ME) } var sortReverseOrder by remember { mutableStateOf(false) } @@ -127,27 +155,13 @@ class BulkMessagingAction : AbstractAction() { val bitmojiCache = remember { EvictingMap<String, Bitmap>(50) } val noBitmojiBitmap = remember { BitmapFactory.decodeResource(context.resources, android.R.drawable.ic_menu_report_image).asImageBitmap() } - suspend fun refreshList() { - withContext(Dispatchers.Main) { - selectedFriends.clear() - friends.clear() - } + val focusManager = LocalFocusManager.current + var nameFilter by remember { mutableStateOf("") } + + suspend fun refreshList(clearSelected: Boolean = true) { withContext(Dispatchers.IO) { - val userIdBlacklist = arrayOf( - context.database.myUserId, - "b42f1f70-5a8b-4c53-8c25-34e7ec9e6781", // myai - "84ee8839-3911-492d-8b94-72dd80f3713a", // teamsnapchat - ) - val newFriends = context.database.getAllFriends().filter { - it.userId !in userIdBlacklist && when (filter) { - Filter.ALL -> true - Filter.MY_FRIENDS -> it.friendLinkType == FriendLinkType.MUTUAL.value && it.addedTimestamp > 0 - Filter.BLOCKED -> it.friendLinkType == FriendLinkType.BLOCKED.value - Filter.REMOVED_ME -> it.friendLinkType == FriendLinkType.OUTGOING.value && it.addedTimestamp > 0 && it.businessCategory == 0 // ignore followed accounts - Filter.SUGGESTED -> it.friendLinkType == FriendLinkType.SUGGESTED.value - Filter.DELETED -> it.friendLinkType == FriendLinkType.DELETED.value - Filter.BUSINESS_ACCOUNTS -> it.businessCategory > 0 - } + val newFriends = context.database.getAllFriends().let { friends -> + filterFriends(friends, filter, nameFilter) }.toMutableList() when (sortBy) { SortBy.NONE -> {} @@ -158,6 +172,8 @@ class BulkMessagingAction : AbstractAction() { } if (sortReverseOrder) newFriends.reverse() withContext(Dispatchers.Main) { + if (clearSelected) selectedFriends.clear() + friends.clear() friends.addAll(newFriends) } } @@ -245,32 +261,59 @@ class BulkMessagingAction : AbstractAction() { .weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp) ) { - item { - if (friends.isNotEmpty()) { - Row( - modifier = Modifier.fillMaxWidth().padding(bottom = 2.dp), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(text = "Selected " + selectedFriends.size + " friends", fontSize = 12.sp, fontWeight = FontWeight.Light) - Checkbox( - checked = selectedFriends.size == friends.size, - onCheckedChange = { state -> - if (state) { - friends.mapNotNull { it.userId }.forEach { userId -> - if (!selectedFriends.contains(userId)) { - selectedFriends.add(userId) - } + stickyHeader { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(bottom = 2.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + TextField( + value = nameFilter, + onValueChange = { + nameFilter = it + coroutineScope.launch { refreshList(clearSelected = false) } + }, + placeholder = { Text(text = "Search by name") }, + singleLine = true, + modifier = Modifier + .padding(end = 5.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent + ), + ) + + Checkbox( + checked = if (friends.isEmpty() || selectedFriends.size < friends.size) false else friends.all { friend -> selectedFriends.contains(friend.userId) }, + onCheckedChange = { state -> + if (state) { + friends.mapNotNull { it.userId }.forEach { userId -> + if (!selectedFriends.contains(userId)) { + selectedFriends.add(userId) + } + } + } else { + if (nameFilter.isNotBlank()) { + filterFriends(friends, filter, nameFilter).mapNotNull { it.userId }.forEach { userId -> + selectedFriends.remove(userId) } - } else selectedFriends.clear() + } else { + selectedFriends.clear() + } } - ) - } - } else { + } + ) + } + } + item { + if (friends.isEmpty()) { Text(text = "No friends found", fontSize = 12.sp, fontWeight = FontWeight.Light, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center) } } - items(friends) { friendInfo -> + items(friends, key = { it.userId!! }) { friendInfo -> var bitmojiBitmap by remember(friendInfo) { mutableStateOf(bitmojiCache[friendInfo.bitmojiAvatarId]) } fun selectFriend(state: Boolean) { @@ -393,7 +436,7 @@ class BulkMessagingAction : AbstractAction() { context.shortToast("Failed to clear conversation: $error") }) }.invokeOnCompletion { - context.coroutineScope.launch { refreshList() } + coroutineScope.launch { refreshList() } } } }) @@ -415,7 +458,7 @@ class BulkMessagingAction : AbstractAction() { removeAction(ctx, selectedFriends.toList().also { selectedFriends.clear() }) { removeFriend(it) }.invokeOnCompletion { - context.coroutineScope.launch { refreshList() } + coroutineScope.launch { refreshList() } } } }, @@ -426,8 +469,18 @@ class BulkMessagingAction : AbstractAction() { } } - LaunchedEffect(filter, sortBy, sortReverseOrder) { - refreshList() + LaunchedEffect(sortBy, sortReverseOrder) { + coroutineScope.launch { + refreshList(clearSelected = false) + } + focusManager.clearFocus() + } + + LaunchedEffect(filter) { + coroutineScope.launch { + refreshList() + } + focusManager.clearFocus() } }