AddFriendDialog.kt (11733B) - raw


      1 package me.rhunk.snapenhance.ui.manager.pages.social
      2 
      3 import androidx.compose.foundation.clickable
      4 import androidx.compose.foundation.layout.*
      5 import androidx.compose.foundation.lazy.LazyColumn
      6 import androidx.compose.foundation.text.KeyboardOptions
      7 import androidx.compose.material.icons.Icons
      8 import androidx.compose.material.icons.filled.Search
      9 import androidx.compose.material3.*
     10 import androidx.compose.runtime.*
     11 import androidx.compose.ui.Alignment
     12 import androidx.compose.ui.Modifier
     13 import androidx.compose.ui.text.font.FontWeight
     14 import androidx.compose.ui.text.input.ImeAction
     15 import androidx.compose.ui.text.input.KeyboardType
     16 import androidx.compose.ui.unit.dp
     17 import androidx.compose.ui.unit.sp
     18 import kotlinx.coroutines.*
     19 import me.rhunk.snapenhance.RemoteSideContext
     20 import me.rhunk.snapenhance.common.ReceiversConfig
     21 import me.rhunk.snapenhance.common.data.MessagingFriendInfo
     22 import me.rhunk.snapenhance.common.data.MessagingGroupInfo
     23 import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState
     24 import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
     25 import me.rhunk.snapenhance.common.util.snap.SnapWidgetBroadcastReceiverHelper
     26 import me.rhunk.snapenhance.ui.util.coil.BitmojiImage
     27 
     28 class AddFriendDialog(
     29     private val context: RemoteSideContext,
     30     private val actionHandler: Actions,
     31     private val pinnedIds: List<String>? = null
     32 ) {
     33     class Actions(
     34         val onFriendState: (friend: MessagingFriendInfo, state: Boolean) -> Unit,
     35         val onGroupState: (group: MessagingGroupInfo, state: Boolean) -> Unit,
     36         val getFriendState: (friend: MessagingFriendInfo) -> Boolean,
     37         val getGroupState: (group: MessagingGroupInfo) -> Boolean,
     38     )
     39 
     40     private val stateCache = mutableMapOf<String, Boolean>()
     41     private val translation by lazy { context.translation.getCategory("manager.dialogs.add_friend")}
     42 
     43     @Composable
     44     private fun ListCardEntry(
     45         id: String,
     46         bitmoji: String? = null,
     47         name: String,
     48         participantsCount: Int? = null,
     49         getCurrentState: () -> Boolean,
     50         onState: (Boolean) -> Unit = {},
     51     ) {
     52         var currentState by rememberAsyncMutableState(defaultValue = stateCache[id] ?: false) {
     53             getCurrentState().also { stateCache[id] = it }
     54         }
     55         val coroutineScope = rememberCoroutineScope()
     56 
     57         Row(
     58             modifier = Modifier
     59                 .fillMaxWidth()
     60                 .clickable {
     61                     currentState = !currentState
     62                     stateCache[id] = currentState
     63                     coroutineScope.launch(Dispatchers.IO) {
     64                         onState(currentState)
     65                     }
     66                 }
     67                 .padding(4.dp),
     68             horizontalArrangement = Arrangement.spacedBy(4.dp),
     69             verticalAlignment = Alignment.CenterVertically
     70         ) {
     71             BitmojiImage(
     72                 context = this@AddFriendDialog.context,
     73                 url = bitmoji,
     74                 modifier = Modifier.padding(end = 2.dp),
     75                 size = 32,
     76             )
     77 
     78             Column(
     79                 modifier = Modifier
     80                     .weight(1f)
     81             ) {
     82                 Text(
     83                     text = name,
     84                     fontSize = 15.sp,
     85                 )
     86 
     87                 participantsCount?.let {
     88                     Text(
     89                         text = translation.format("participants_text", "count" to it.toString()),
     90                         fontSize = 12.sp,
     91                         lineHeight = 12.sp,
     92                         color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
     93                     )
     94                 }
     95             }
     96 
     97             Checkbox(
     98                 checked = currentState,
     99                 onCheckedChange = {
    100                     currentState = it
    101                     stateCache[id] = currentState
    102                     coroutineScope.launch(Dispatchers.IO) {
    103                         onState(currentState)
    104                     }
    105                 }
    106             )
    107         }
    108     }
    109 
    110     @Composable
    111     private fun DialogHeader(searchKeyword: MutableState<String>) {
    112         Column(
    113             modifier = Modifier
    114                 .fillMaxWidth()
    115                 .padding(10.dp),
    116         ) {
    117             Text(
    118                 text = translation["title"],
    119                 fontSize = 23.sp,
    120                 fontWeight = FontWeight.ExtraBold,
    121                 modifier = Modifier
    122                     .align(alignment = Alignment.CenterHorizontally)
    123             )
    124         }
    125 
    126         Row(
    127             modifier = Modifier
    128                 .fillMaxWidth()
    129                 .padding(10.dp),
    130             verticalAlignment = Alignment.CenterVertically
    131         ) {
    132             TextField(
    133                 value = searchKeyword.value,
    134                 onValueChange = { searchKeyword.value = it },
    135                 label = {
    136                     Text(text = translation["search_hint"])
    137                 },
    138                 modifier = Modifier
    139                     .weight(1f)
    140                     .padding(end = 10.dp),
    141                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
    142                 leadingIcon = {
    143                     Icon(Icons.Filled.Search, contentDescription = "Search")
    144                 }
    145             )
    146         }
    147     }
    148 
    149 
    150     @Composable
    151     fun Content(dismiss: () -> Unit = { }) {
    152         var cachedFriends by remember { mutableStateOf(null as List<MessagingFriendInfo>?) }
    153         var cachedGroups by remember { mutableStateOf(null as List<MessagingGroupInfo>?) }
    154 
    155         val coroutineScope = rememberCoroutineScope()
    156 
    157         var timeoutJob: Job? = null
    158         var hasFetchError by remember { mutableStateOf(false) }
    159 
    160         LaunchedEffect(Unit) {
    161             context.database.receiveMessagingDataCallback = { friends, groups ->
    162                 cachedFriends = friends.run {
    163                     if (pinnedIds != null) {
    164                         sortedBy { -pinnedIds.indexOf(it.userId) }
    165                     } else friends
    166                 }
    167                 cachedGroups = groups.run {
    168                     if (pinnedIds != null) {
    169                         sortedBy { -pinnedIds.indexOf(it.conversationId) }
    170                     } else groups
    171                 }
    172                 timeoutJob?.cancel()
    173                 hasFetchError = false
    174             }
    175             SnapWidgetBroadcastReceiverHelper.create(ReceiversConfig.BRIDGE_SYNC_ACTION) {}.also {
    176                 runCatching {
    177                     context.androidContext.sendBroadcast(it)
    178                 }.onFailure {
    179                     context.log.error("Failed to send broadcast", it)
    180                     hasFetchError = true
    181                 }
    182             }
    183             timeoutJob = coroutineScope.launch {
    184                 withContext(Dispatchers.IO) {
    185                     delay(20000)
    186                     hasFetchError = true
    187                 }
    188             }
    189         }
    190 
    191         me.rhunk.snapenhance.ui.util.Dialog(
    192             onDismissRequest = {
    193                 timeoutJob?.cancel()
    194                 dismiss()
    195             },
    196             properties = me.rhunk.snapenhance.ui.util.DialogProperties(usePlatformDefaultWidth = false)
    197         ) {
    198             Card(
    199                 colors = CardDefaults.elevatedCardColors(),
    200                 modifier = Modifier
    201                     .fillMaxSize()
    202                     .fillMaxWidth()
    203                     .padding(all = 20.dp)
    204             ) {
    205                 if (cachedGroups == null || cachedFriends == null) {
    206                     Column(
    207                         modifier = Modifier
    208                             .fillMaxSize()
    209                             .padding(10.dp),
    210                         verticalArrangement = Arrangement.Center,
    211                         horizontalAlignment = Alignment.CenterHorizontally
    212                     ) {
    213                         if (hasFetchError) {
    214                             Text(
    215                                 text = translation["fetch_error"],
    216                                 fontSize = 20.sp,
    217                                 fontWeight = FontWeight.Bold,
    218                                 modifier = Modifier.padding(bottom = 10.dp, top = 10.dp)
    219                             )
    220                             return@Card
    221                         }
    222                         CircularProgressIndicator(
    223                             modifier = Modifier
    224                                 .padding()
    225                                 .size(30.dp),
    226                             strokeWidth = 3.dp,
    227                             color = MaterialTheme.colorScheme.primary
    228                         )
    229                     }
    230                     return@Card
    231                 }
    232 
    233                 val searchKeyword = remember { mutableStateOf("") }
    234 
    235                 val filteredGroups = cachedGroups!!.takeIf { searchKeyword.value.isNotBlank() }?.filter {
    236                     it.name.contains(searchKeyword.value, ignoreCase = true)
    237                 } ?: cachedGroups!!
    238 
    239                 val filteredFriends = cachedFriends!!.takeIf { searchKeyword.value.isNotBlank() }?.filter {
    240                     it.mutableUsername.contains(searchKeyword.value, ignoreCase = true) ||
    241                     it.displayName?.contains(searchKeyword.value, ignoreCase = true) == true
    242                 } ?: cachedFriends!!
    243 
    244                 DialogHeader(searchKeyword)
    245 
    246                 LazyColumn(
    247                     modifier = Modifier
    248                         .fillMaxSize()
    249                         .padding(10.dp)
    250                 ) {
    251                     item {
    252                         if (filteredGroups.isEmpty()) return@item
    253                         Text(text = translation["category_groups"],
    254                             fontSize = 20.sp,
    255                             fontWeight = FontWeight.Bold,
    256                             modifier = Modifier.padding(bottom = 10.dp, top = 10.dp)
    257                         )
    258                     }
    259 
    260                     items(filteredGroups.size) {
    261                         val group = filteredGroups[it]
    262                         ListCardEntry(
    263                             id = group.conversationId,
    264                             name = group.name,
    265                             participantsCount = group.participantsCount,
    266                             getCurrentState = { actionHandler.getGroupState(group) }
    267                         ) { state ->
    268                             actionHandler.onGroupState(group, state)
    269                         }
    270                     }
    271 
    272                     item {
    273                         if (filteredFriends.isEmpty()) return@item
    274                         Text(text = translation["category_friends"],
    275                             fontSize = 20.sp,
    276                             fontWeight = FontWeight.Bold,
    277                             modifier = Modifier.padding(bottom = 10.dp, top = 10.dp)
    278                         )
    279                     }
    280 
    281                     items(filteredFriends.size) { index ->
    282                         val friend = filteredFriends[index]
    283 
    284                         ListCardEntry(
    285                             id = friend.userId,
    286                             bitmoji = friend.takeIf { it.bitmojiId != null }?.let {
    287                                 BitmojiSelfie.getBitmojiSelfie(it.selfieId, it.bitmojiId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D)
    288                             },
    289                             name = friend.displayName?.takeIf { name -> name.isNotBlank() } ?: friend.mutableUsername,
    290                             getCurrentState = { actionHandler.getFriendState(friend) }
    291                         ) { state ->
    292                             actionHandler.onFriendState(friend, state)
    293                         }
    294                     }
    295                 }
    296             }
    297         }
    298     }
    299 }