diff --git a/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin b/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin index 2a101cf..42f6e75 100644 Binary files a/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin and b/client-mobile/.gradle/8.5/executionHistory/executionHistory.bin differ diff --git a/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock b/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock index 200f1b7..8f60357 100644 Binary files a/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock and b/client-mobile/.gradle/8.5/executionHistory/executionHistory.lock differ diff --git a/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin b/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin index f67fedc..d2927bf 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin and b/client-mobile/.gradle/8.5/fileHashes/fileHashes.bin differ diff --git a/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock b/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock index e41221d..4963c9b 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock and b/client-mobile/.gradle/8.5/fileHashes/fileHashes.lock differ diff --git a/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin b/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin index b745ddf..7b720d6 100644 Binary files a/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin and b/client-mobile/.gradle/8.5/fileHashes/resourceHashesCache.bin differ diff --git a/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock index bef2ac7..8060032 100644 Binary files a/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/client-mobile/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin b/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin index f00f540..525a487 100644 Binary files a/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin and b/client-mobile/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/client-mobile/chats/data/remote/api/ChatApi.kt b/client-mobile/chats/data/remote/api/ChatApi.kt index 9853b8b..b5535cb 100644 --- a/client-mobile/chats/data/remote/api/ChatApi.kt +++ b/client-mobile/chats/data/remote/api/ChatApi.kt @@ -39,13 +39,13 @@ interface ChatApi { // Klipy GIF API @GET("klipy/trending") - suspend fun getTrendingGifs(@Query("page") page: Int): List + suspend fun getTrendingGifs(@Query("page") page: Int): KlipyResponse @GET("klipy/search") - suspend fun searchGifs(@Query("q") query: String, @Query("page") page: Int): List + suspend fun searchGifs(@Query("q") query: String, @Query("page") page: Int): KlipyResponse @GET("klipy/categories") - suspend fun getGifCategories(): List + suspend fun getGifCategories(): GifCategoriesResponse @POST("klipy/shared/{id}") suspend fun markGifShared(@Path("id") id: String, @Body query: String) @@ -60,15 +60,27 @@ interface ChatApi { suspend fun markMessagesAsRead(@Path("chatId") chatId: String, @Body lastMessageId: String) } +data class KlipyResponse( + val data: KlipyDataWrapper +) + +data class KlipyDataWrapper( + val data: List +) + data class KlipyGifDto( val id: String, - val images: GifImagesDto?, - val title: String? + val images: GifImagesDto? = null, + val files: Map>? = null, + val file: Map>? = null, + val media_formats: Map? = null, + val title: String? = null ) data class GifImagesDto( - val fixed_height: GifImageSourceDto?, - val original: GifImageSourceDto? + val fixed_height: GifImageSourceDto? = null, + val original: GifImageSourceDto? = null, + val fixed_height_small: GifImageSourceDto? = null ) data class GifImageSourceDto( @@ -86,3 +98,11 @@ data class FileUploadResponse( val filename: String, val size: Long ) + +data class GifCategoriesResponse( + val data: GifCategoriesData +) + +data class GifCategoriesData( + val categories: List +) diff --git a/client-mobile/chats/data/repository/ChatRepositoryImpl.kt b/client-mobile/chats/data/repository/ChatRepositoryImpl.kt index e2ded3c..8962e36 100644 --- a/client-mobile/chats/data/repository/ChatRepositoryImpl.kt +++ b/client-mobile/chats/data/repository/ChatRepositoryImpl.kt @@ -55,6 +55,18 @@ class ChatRepositoryImpl @Inject constructor( val body = MultipartBody.Part.createFormData("file", file.name, requestFile) return api.uploadFile(body).url } + + override suspend fun getTrendingGifs(page: Int): List { + return api.getTrendingGifs(page).data.data + } + + override suspend fun searchGifs(query: String, page: Int): List { + return api.searchGifs(query, page).data.data + } + + override suspend fun getGifCategories(): List { + return api.getGifCategories().data.categories + } } // Mappers diff --git a/client-mobile/chats/domain/repository/ChatRepository.kt b/client-mobile/chats/domain/repository/ChatRepository.kt index 259060b..312b6f2 100644 --- a/client-mobile/chats/domain/repository/ChatRepository.kt +++ b/client-mobile/chats/domain/repository/ChatRepository.kt @@ -11,5 +11,8 @@ interface ChatRepository { suspend fun sendTypingStatus(chatId: String) suspend fun markMessagesAsRead(chatId: String, lastMessageId: String) suspend fun uploadMedia(file: java.io.File): String + suspend fun getTrendingGifs(page: Int = 0): List + suspend fun searchGifs(query: String, page: Int = 0): List + suspend fun getGifCategories(): List } diff --git a/client-mobile/chats/presentation/chat_detail/ChatDetailScreen.kt b/client-mobile/chats/presentation/chat_detail/ChatDetailScreen.kt index d46e6fb..3418594 100644 --- a/client-mobile/chats/presentation/chat_detail/ChatDetailScreen.kt +++ b/client-mobile/chats/presentation/chat_detail/ChatDetailScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import chats.presentation.components.EmojiPicker +import chats.presentation.components.MediaPicker import chats.presentation.components.MessageBubble import core.presentation.components.AppAvatar import core.presentation.components.AppMediaLightbox @@ -246,7 +246,30 @@ fun ChatDetailScreen( } if (isEmojiPickerVisible) { - EmojiPicker(onEmojiSelected = { textInput += it }) + LaunchedEffect(Unit) { + viewModel.loadTrendingGifs() + } + MediaPicker( + trendingGifs = state.trendingGifs, + searchedGifs = state.searchedGifs, + recentGifs = state.recentGifs, + gifCategories = state.gifCategories, + isGifsLoading = state.isGifsLoading, + error = state.error, + onEmojiSelected = { textInput += it }, + onGifSelected = { url -> + viewModel.sendGif(url) + isEmojiPickerVisible = false + }, + onGifSearch = { query -> + viewModel.searchGifs(query) + } + ) + + LaunchedEffect(Unit) { + viewModel.loadTrendingGifs() + viewModel.loadGifCategories() + } } } } diff --git a/client-mobile/chats/presentation/chat_detail/ChatDetailViewModel.kt b/client-mobile/chats/presentation/chat_detail/ChatDetailViewModel.kt index 84ca565..f8abd35 100644 --- a/client-mobile/chats/presentation/chat_detail/ChatDetailViewModel.kt +++ b/client-mobile/chats/presentation/chat_detail/ChatDetailViewModel.kt @@ -17,6 +17,8 @@ import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject +import chats.data.remote.api.KlipyGifDto + data class ChatDetailState( val messages: List = emptyList(), val chatName: String? = null, @@ -26,7 +28,12 @@ data class ChatDetailState( val typingUser: String? = null, val error: String? = null, val canCall: Boolean = true, - val maxFileSize: Long = 100 * 1024 * 1024 + val maxFileSize: Long = 100 * 1024 * 1024, + val trendingGifs: List = emptyList(), + val searchedGifs: List = emptyList(), + val recentGifs: List = emptyList(), + val gifCategories: List = emptyList(), + val isGifsLoading: Boolean = false ) @HiltViewModel @@ -182,5 +189,78 @@ class ChatDetailViewModel @Inject constructor( } } } + + fun loadTrendingGifs() { + if (_state.value.trendingGifs.isNotEmpty()) return + + viewModelScope.launch { + _state.update { it.copy(isGifsLoading = true) } + try { + val gifs = repository.getTrendingGifs(0) + _state.update { it.copy(trendingGifs = gifs, isGifsLoading = false) } + } catch (e: Exception) { + _state.update { it.copy(isGifsLoading = false, error = "GIF load error: ${e.message}") } + } + } + } + + fun loadGifCategories() { + if (_state.value.gifCategories.isNotEmpty()) return + viewModelScope.launch { + try { + val categories = repository.getGifCategories() + _state.update { it.copy(gifCategories = categories) } + } catch (e: Exception) { + // Ignore failure + } + } + } + + private var searchJob: Job? = null + fun searchGifs(query: String) { + searchJob?.cancel() + if (query.isBlank()) { + _state.update { it.copy(searchedGifs = emptyList()) } + return + } + searchJob = viewModelScope.launch { + delay(500) + _state.update { it.copy(isGifsLoading = true) } + try { + val gifs = repository.searchGifs(query, 0) + _state.update { it.copy(searchedGifs = gifs, isGifsLoading = false) } + } catch (e: Exception) { + _state.update { it.copy(isGifsLoading = false) } + } + } + } + + fun sendGif(url: String) { + val chatId = currentChatId ?: return + viewModelScope.launch { + try { + repository.sendMessage(chatId, url) + + // Add to recent + val allGifs = _state.value.trendingGifs + _state.value.searchedGifs + _state.value.recentGifs + val selectedGif = allGifs.find { gif -> + val gifUrl = gif.files?.get("hd")?.get("gif")?.url + ?: gif.files?.get("sd")?.get("gif")?.url + ?: gif.file?.get("hd")?.get("gif")?.url + ?: gif.file?.get("sd")?.get("gif")?.url + ?: gif.media_formats?.get("gif")?.url + ?: gif.images?.original?.url + gifUrl == url + } + + selectedGif?.let { gif -> + val newList = (listOf(gif) + _state.value.recentGifs).distinctBy { it.id }.take(20) + _state.update { it.copy(recentGifs = newList) } + } + } catch (e: Exception) { + _state.update { it.copy(error = e.localizedMessage) } + } + } + } } diff --git a/client-mobile/chats/presentation/components/MediaPicker.kt b/client-mobile/chats/presentation/components/MediaPicker.kt new file mode 100644 index 0000000..583dd18 --- /dev/null +++ b/client-mobile/chats/presentation/components/MediaPicker.kt @@ -0,0 +1,412 @@ +package chats.presentation.components + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import chats.data.remote.api.KlipyGifDto +import core.utils.CoilUtils +import kotlinx.coroutines.launch + +enum class MediaTab { EMOJI, GIF } + +@Composable +fun MediaPicker( + trendingGifs: List, + searchedGifs: List, + recentGifs: List = emptyList(), + gifCategories: List = emptyList(), + isGifsLoading: Boolean, + error: String?, + onEmojiSelected: (String) -> Unit, + onGifSelected: (String) -> Unit, + onGifSearch: (String) -> Unit, + modifier: Modifier = Modifier +) { + var activeTab by remember { mutableStateOf(MediaTab.EMOJI) } + + // Вычисляем индексы для прыжков (учитываем заголовки) + val emojiCategories = remember { + listOf( + EmojiCategory("Недавние", Icons.Rounded.Schedule, listOf("👍", "❤️", "😂", "😍", "🙏", "🔥", "😭", "😊")), + EmojiCategory("Смайлы", Icons.Rounded.EmojiEmotions, listOf("😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇")), + EmojiCategory("Животные", Icons.Rounded.Pets, listOf("🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷")), + EmojiCategory("Еда", Icons.Rounded.Fastfood, listOf("🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭")), + EmojiCategory("Спорт", Icons.Rounded.SportsFootball, listOf("⚽", "🏀", "🏈", "⚾", "🎾", "🏐", "🏉", "🎱", "🏓", "🏸", "🥅", "🏒")), + EmojiCategory("Транспорт", Icons.Rounded.DirectionsCar, listOf("🚗", "🚕", "🚙", "🚌", "🚎", "🏎️", "🚓", "🚑", "🚒", "🚐", "🚚", "🚛")), + EmojiCategory("Объекты", Icons.Rounded.Lightbulb, listOf("💡", "🔦", "🕯️", "🪔", "📔", "📕", "📖", "💚", "💙", "💜", "🤎", "🖤")), + EmojiCategory("Музыка", Icons.Rounded.MusicNote, listOf("🎵", "🎶", "🎼", "🎹", "🎸", "🎻", "🎷", "🎺", "🥁", "🎤", "🎧", "📻")), + EmojiCategory("Флаги", Icons.Rounded.Flag, listOf("🏁", "🚩", "🎌", "🏴", "🏳️", "🏳️‍🌈", "🏳️‍⚧️", "🏴‍☠️", "🇦🇱", "🇩🇿", "🇦🇸", "🇦🇩")) + ) + } + + val categoryIndices = remember(emojiCategories) { + var currentIdx = 0 + emojiCategories.map { cat -> + val startIdx = currentIdx + currentIdx += 1 + cat.emojis.size + cat.icon to startIdx + } + } + + Column( + modifier = modifier + .fillMaxWidth() + .height(450.dp) + .background(Color(0xFF1C1C1C), RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + ) { + // Табы (Эмодзи / GIF) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .background(Color(0xFF2C2C2C), RoundedCornerShape(12.dp)) + .padding(2.dp) + ) { + MediaTabItem( + title = "ЭМОДЗИ", + isSelected = activeTab == MediaTab.EMOJI, + onClick = { activeTab = MediaTab.EMOJI }, + modifier = Modifier.weight(1f) + ) + MediaTabItem( + title = "GIF", + isSelected = activeTab == MediaTab.GIF, + onClick = { activeTab = MediaTab.GIF }, + modifier = Modifier.weight(1f) + ) + } + + Crossfade(targetState = activeTab, label = "media_tab") { tab -> + when (tab) { + MediaTab.EMOJI -> EmojiContent( + emojiCategories = emojiCategories, + categoryIndices = categoryIndices, + onEmojiSelected = onEmojiSelected + ) + MediaTab.GIF -> GifContent( + trendingGifs = trendingGifs, + searchedGifs = searchedGifs, + recentGifs = recentGifs, + gifCategories = gifCategories, + isGifsLoading = isGifsLoading, + error = error, + onGifSelected = onGifSelected, + onGifSearch = onGifSearch + ) + } + } + } +} + +@Composable +private fun MediaTabItem( + title: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(10.dp)) + .background(if (isSelected) Color(0xFF3C3C3C) else Color.Transparent) + .clickable { onClick() } + .padding(vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + color = if (isSelected) Color.White else Color.Gray, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun EmojiContent( + emojiCategories: List, + categoryIndices: List>, + onEmojiSelected: (String) -> Unit +) { + val scrollState = androidx.compose.foundation.lazy.grid.rememberLazyGridState() + val scope = rememberCoroutineScope() + + Column(modifier = Modifier.fillMaxSize()) { + // Категории + LazyRow( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(categoryIndices) { (icon, index) -> + Icon( + imageVector = icon, + contentDescription = null, + tint = Color.Gray, + modifier = Modifier + .size(24.dp) + .clickable { + scope.launch { + scrollState.animateScrollToItem(index) + } + } + ) + } + } + + // Поиск + TextField( + value = "", + onValueChange = {}, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .clip(RoundedCornerShape(8.dp)), + placeholder = { Text("Поиск", color = Color.Gray) }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, tint = Color.Gray) }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color(0xFF2C2C2C), + unfocusedContainerColor = Color(0xFF2C2C2C), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(8), + state = scrollState, + modifier = Modifier.weight(1f).padding(horizontal = 4.dp) + ) { + emojiCategories.forEach { category -> + // Заголовок категории + item(span = { androidx.compose.foundation.lazy.grid.GridItemSpan(8) }) { + Text( + text = category.name, + color = Color.Gray, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(8.dp) + ) + } + + items(category.emojis) { emoji -> + Box( + modifier = Modifier + .aspectRatio(1f) + .clickable { onEmojiSelected(emoji) }, + contentAlignment = Alignment.Center + ) { + Text(text = emoji, fontSize = 28.sp) + } + } + } + } + } +} + +data class EmojiCategory(val name: String, val icon: ImageVector, val emojis: List) + +@Composable +private fun GifContent( + trendingGifs: List, + searchedGifs: List, + recentGifs: List, + gifCategories: List, + isGifsLoading: Boolean, + error: String?, + onGifSelected: (String) -> Unit, + onGifSearch: (String) -> Unit +) { + var query by remember { mutableStateOf("") } + var subTab by remember { mutableStateOf("trending") } + val context = LocalContext.current + val imageLoader = CoilUtils.getGifImageLoader(context) + + Column(modifier = Modifier.fillMaxSize()) { + TextField( + value = query, + onValueChange = { + query = it + if (it.isNotEmpty()) subTab = "search" + else if (subTab == "search") subTab = "trending" + onGifSearch(it) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp) + .clip(RoundedCornerShape(8.dp)), + placeholder = { Text("Поиск GIF...", color = Color.Gray) }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, tint = Color.Gray) }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color(0xFF2C2C2C), + unfocusedContainerColor = Color(0xFF2C2C2C), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ) + ) + + // Под-вкладки GIF (Recent, Trending, Categories) + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Rounded.Schedule, + contentDescription = null, + tint = if (subTab == "recent") Color(0xFF3B82F6) else Color.Gray, + modifier = Modifier.size(20.dp).clickable { subTab = "recent"; query = "" } + ) + Icon( + Icons.Rounded.TrendingUp, + contentDescription = null, + tint = if (subTab == "trending") Color(0xFF3B82F6) else Color.Gray, + modifier = Modifier.size(20.dp).clickable { subTab = "trending"; query = "" } + ) + Icon( + Icons.Rounded.GridView, + contentDescription = null, + tint = if (subTab == "categories") Color(0xFF3B82F6) else Color.Gray, + modifier = Modifier.size(20.dp).clickable { subTab = "categories"; query = "" } + ) + } + + if (subTab == "categories") { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.weight(1f).padding(4.dp), + contentPadding = PaddingValues(4.dp) + ) { + items(gifCategories) { category -> + Box( + modifier = Modifier + .padding(4.dp) + .aspectRatio(1.77f) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF2C2C2C)) + .clickable { + query = category.query + subTab = "search" + onGifSearch(category.query) + } + ) { + AsyncImage( + model = category.preview_url, + contentDescription = category.category, + modifier = Modifier.fillMaxSize().alpha(0.6f), + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = category.category.uppercase(), + color = Color.White, + fontSize = 10.sp, + fontWeight = FontWeight.Black, + letterSpacing = 2.sp + ) + } + } + } + } + } else { + val displayGifs = when (subTab) { + "recent" -> recentGifs + "search" -> searchedGifs + else -> trendingGifs + } + + if (isGifsLoading && displayGifs.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = Color(0xFF3B82F6)) + } + } else if (!isGifsLoading && displayGifs.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(error ?: "Ничего не найдено", color = Color.Gray) + if (error != null) { + Button(onClick = { onGifSearch(query) }) { + Text("Повторить") + } + } + } + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier.weight(1f).padding(4.dp), + contentPadding = PaddingValues(4.dp) + ) { + items(displayGifs) { gif -> + val url = gif.files?.get("hd")?.get("gif")?.url + ?: gif.files?.get("sd")?.get("gif")?.url + ?: gif.file?.get("hd")?.get("gif")?.url + ?: gif.file?.get("sd")?.get("gif")?.url + ?: gif.media_formats?.get("gif")?.url + ?: gif.images?.original?.url + + val preview = gif.files?.get("sd")?.get("webp")?.url + ?: gif.files?.get("sd")?.get("gif")?.url + ?: gif.file?.get("sd")?.get("webp")?.url + ?: gif.media_formats?.get("tinygif")?.url + ?: gif.images?.fixed_height_small?.url + ?: url + + if (url != null) { + AsyncImage( + model = preview ?: url, + imageLoader = imageLoader, + contentDescription = gif.title, + modifier = Modifier + .padding(2.dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF2C2C2C)) + .clickable { onGifSelected(url) }, + contentScale = ContentScale.Crop + ) + } else { + Box( + modifier = Modifier + .padding(2.dp) + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFF3C3C3C)), + contentAlignment = Alignment.Center + ) { + Text(gif.title ?: "GIF", color = Color.Gray, fontSize = 10.sp) + } + } + } + } + } + } + } +}