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 }