Merge c1b275367b into e9b16f2a28
This commit is contained in:
commit
d452ec1bf3
|
|
@ -0,0 +1,66 @@
|
||||||
|
package chat.stoat.api.routes.microservices.gifbox
|
||||||
|
|
||||||
|
import chat.stoat.api.StoatHttp
|
||||||
|
import chat.stoat.api.StoatAPI
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
private const val GIFBOX_BASE = "https://gifbox.stoat.chat"
|
||||||
|
|
||||||
|
private val gifboxJson = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GifCategory(
|
||||||
|
val title: String,
|
||||||
|
val image: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GifMediaFormat(
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GifResult(
|
||||||
|
val url: String,
|
||||||
|
@SerialName("media_formats")
|
||||||
|
val mediaFormats: Map<String, GifMediaFormat> = emptyMap()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GifSearchResponse(
|
||||||
|
val results: List<GifResult> = emptyList(),
|
||||||
|
val next: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
object Gifbox {
|
||||||
|
suspend fun fetchCategories(): List<GifCategory> {
|
||||||
|
val response = StoatHttp.get("$GIFBOX_BASE/categories") {
|
||||||
|
parameter("locale", "en_US")
|
||||||
|
header(StoatAPI.TOKEN_HEADER_NAME, StoatAPI.sessionToken)
|
||||||
|
}
|
||||||
|
return gifboxJson.decodeFromString(response.bodyAsText())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchTrending(limit: Int = 50): GifSearchResponse {
|
||||||
|
val response = StoatHttp.get("$GIFBOX_BASE/trending") {
|
||||||
|
parameter("locale", "en_US")
|
||||||
|
parameter("limit", limit.toString())
|
||||||
|
header(StoatAPI.TOKEN_HEADER_NAME, StoatAPI.sessionToken)
|
||||||
|
}
|
||||||
|
return gifboxJson.decodeFromString(response.bodyAsText())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun search(query: String, limit: Int = 50): GifSearchResponse {
|
||||||
|
val response = StoatHttp.get("$GIFBOX_BASE/search") {
|
||||||
|
parameter("locale", "en_US")
|
||||||
|
parameter("query", query)
|
||||||
|
parameter("limit", limit.toString())
|
||||||
|
header(StoatAPI.TOKEN_HEADER_NAME, StoatAPI.sessionToken)
|
||||||
|
}
|
||||||
|
return gifboxJson.decodeFromString(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -438,9 +438,17 @@ fun Message(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isOnlyGIF = message.content != null &&
|
||||||
|
message.embeds?.size == 1 &&
|
||||||
|
message.embeds!![0].type == "Website" &&
|
||||||
|
(message.embeds!![0].special?.type == "GIF" ||
|
||||||
|
message.embeds!![0].originalURL?.startsWith("https://giphy.com") == true) &&
|
||||||
|
message.content!!.trim().let { c -> c.startsWith("http://") || c.startsWith("https://") } &&
|
||||||
|
!message.content!!.trim().contains(' ')
|
||||||
|
|
||||||
key(message.content) {
|
key(message.content) {
|
||||||
message.content?.let {
|
message.content?.let {
|
||||||
if (message.content!!.isBlank()) return@let // if only an attachment is sent
|
if (message.content!!.isBlank() || isOnlyGIF) return@let // if only an attachment or GIF is sent
|
||||||
|
|
||||||
if (Experiments.useKotlinBasedMarkdownRenderer.isEnabled) {
|
if (Experiments.useKotlinBasedMarkdownRenderer.isEnabled) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
|
@ -520,21 +528,65 @@ fun Message(
|
||||||
it.forEach { embed ->
|
it.forEach { embed ->
|
||||||
when (embed.type) {
|
when (embed.type) {
|
||||||
"Website", "Text" -> {
|
"Website", "Text" -> {
|
||||||
val embedIsEmpty =
|
val isGIF = embed.type == "Website" && (
|
||||||
embed.title == null && embed.description == null && embed.iconURL == null && embed.image == null
|
embed.special?.type == "GIF" ||
|
||||||
|
embed.originalURL?.startsWith("https://giphy.com") == true
|
||||||
|
)
|
||||||
|
|
||||||
if (embedIsEmpty) {
|
// Prefer image URL; for video (mp4), derive GIF URL from giphy page URL
|
||||||
// if we do not emit anything, compose will cause an internal error.
|
val gifUrl = if (isGIF) {
|
||||||
// FIXME if you are doing fixme's anyways then check if this is still an issue
|
embed.image?.url ?: run {
|
||||||
Box {}
|
val pageUrl = embed.url ?: embed.originalURL
|
||||||
return@forEach
|
val giphyId = pageUrl?.substringAfterLast("/")?.substringAfterLast("-")
|
||||||
|
if (giphyId != null) "https://media.giphy.com/media/$giphyId/giphy.gif" else embed.video?.url
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
val gifW = if (isGIF) (embed.image?.width ?: embed.video?.width) else null
|
||||||
|
val gifH = if (isGIF) (embed.image?.height ?: embed.video?.height) else null
|
||||||
|
if (gifUrl != null) {
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.clickable {
|
||||||
|
embed.url?.let {
|
||||||
|
viewUrlInBrowser(context, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val imgWidth = gifW?.toInt()?.dp ?: maxWidth
|
||||||
|
val aspectRatio = if (gifW != null && gifH != null && gifH > 0) {
|
||||||
|
gifW.toFloat() / gifH.toFloat()
|
||||||
|
} else null
|
||||||
|
|
||||||
|
RemoteImage(
|
||||||
|
url = asJanuaryProxyUrl(gifUrl),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(imgWidth)
|
||||||
|
.then(
|
||||||
|
if (aspectRatio != null) Modifier.aspectRatio(aspectRatio)
|
||||||
|
else Modifier
|
||||||
|
),
|
||||||
|
description = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
} else {
|
||||||
|
val embedIsEmpty =
|
||||||
|
embed.title == null && embed.description == null && embed.iconURL == null && embed.image == null
|
||||||
|
|
||||||
|
if (embedIsEmpty) {
|
||||||
|
Box {}
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Embed(embed = embed, onLinkClick = {
|
||||||
|
viewUrlInBrowser(context, it)
|
||||||
|
})
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Embed(embed = embed, onLinkClick = {
|
|
||||||
viewUrlInBrowser(context, it)
|
|
||||||
})
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"Image" -> {
|
"Image" -> {
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,7 @@ fun MessageField(
|
||||||
onAddAttachment: () -> Unit,
|
onAddAttachment: () -> Unit,
|
||||||
onCommitAttachment: (Uri) -> Unit,
|
onCommitAttachment: (Uri) -> Unit,
|
||||||
onPickEmoji: () -> Unit,
|
onPickEmoji: () -> Unit,
|
||||||
|
onPickGif: () -> Unit = {},
|
||||||
onSendMessage: () -> Unit,
|
onSendMessage: () -> Unit,
|
||||||
channelType: ChannelType,
|
channelType: ChannelType,
|
||||||
channelName: String,
|
channelName: String,
|
||||||
|
|
@ -596,6 +597,21 @@ fun MessageField(
|
||||||
.testTag("pick_emoji")
|
.testTag("pick_emoji")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_gif_24dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||||
|
contentDescription = "Pick GIF",
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.size(32.dp)
|
||||||
|
.clickable {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
onPickGif()
|
||||||
|
}
|
||||||
|
.padding(4.dp)
|
||||||
|
.testTag("pick_gif")
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
package chat.stoat.composables.gif
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.stoat.api.routes.microservices.gifbox.GifCategory
|
||||||
|
import chat.stoat.api.routes.microservices.gifbox.GifResult
|
||||||
|
import chat.stoat.api.routes.microservices.gifbox.Gifbox
|
||||||
|
import chat.stoat.composables.generic.RemoteImage
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GifPicker(
|
||||||
|
onGifSelected: (String) -> Unit,
|
||||||
|
onSearchFocus: (Boolean) -> Unit = {},
|
||||||
|
bottomInset: Dp = 0.dp
|
||||||
|
) {
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var categories by remember { mutableStateOf<List<GifCategory>>(emptyList()) }
|
||||||
|
var results by remember { mutableStateOf<List<GifResult>>(emptyList()) }
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
var activeQuery by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var searchJob by remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
|
// Load categories on mount
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
try {
|
||||||
|
categories = Gifbox.fetchCategories()
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced search
|
||||||
|
fun doSearch(query: String) {
|
||||||
|
searchJob?.cancel()
|
||||||
|
if (query.isBlank()) {
|
||||||
|
activeQuery = null
|
||||||
|
results = emptyList()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
searchJob = scope.launch {
|
||||||
|
delay(400)
|
||||||
|
activeQuery = query
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
results = Gifbox.search(query).results
|
||||||
|
} catch (_: Exception) {
|
||||||
|
results = emptyList()
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
// Search bar
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
) {
|
||||||
|
val textFieldState = rememberTextFieldState()
|
||||||
|
BasicTextField(
|
||||||
|
state = textFieldState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
.onFocusChanged { onSearchFocus(it.isFocused) },
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||||
|
),
|
||||||
|
decorator = { innerTextField ->
|
||||||
|
if (textFieldState.text.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
"Search GIFs...",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
},
|
||||||
|
onTextLayout = { },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sync text field state with search query
|
||||||
|
LaunchedEffect(textFieldState.text) {
|
||||||
|
val newText = textFieldState.text.toString()
|
||||||
|
if (newText != searchQuery) {
|
||||||
|
searchQuery = newText
|
||||||
|
doSearch(newText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(32.dp))
|
||||||
|
}
|
||||||
|
} else if (activeQuery != null) {
|
||||||
|
// Search results
|
||||||
|
GifGrid(
|
||||||
|
gifs = results,
|
||||||
|
onGifSelected = onGifSelected,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
bottomInset = bottomInset
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Category grid
|
||||||
|
CategoryGrid(
|
||||||
|
categories = categories,
|
||||||
|
onCategorySelected = { category ->
|
||||||
|
searchQuery = category.title
|
||||||
|
doSearch(category.title)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
bottomInset = bottomInset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CategoryGrid(
|
||||||
|
categories: List<GifCategory>,
|
||||||
|
onCategorySelected: (GifCategory) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
bottomInset: Dp = 0.dp
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
contentPadding = PaddingValues(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp + bottomInset),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
items(categories, key = { it.title }) { category ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(5f / 3f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable { onCategorySelected(category) }
|
||||||
|
) {
|
||||||
|
RemoteImage(
|
||||||
|
url = category.image,
|
||||||
|
description = category.title,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = category.title,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GifGrid(
|
||||||
|
gifs: List<GifResult>,
|
||||||
|
onGifSelected: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
bottomInset: Dp = 0.dp
|
||||||
|
) {
|
||||||
|
if (gifs.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"No GIFs found",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
contentPadding = PaddingValues(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp + bottomInset),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
items(gifs, key = { it.url }) { gif ->
|
||||||
|
val previewUrl = gif.mediaFormats["tinywebm"]?.url
|
||||||
|
?: gif.mediaFormats["webm"]?.url
|
||||||
|
?: gif.url
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(5f / 3f)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable { onGifSelected(gif.url) }
|
||||||
|
) {
|
||||||
|
RemoteImage(
|
||||||
|
url = previewUrl,
|
||||||
|
description = "GIF",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -171,6 +171,7 @@ sealed class ChannelScreenItem {
|
||||||
sealed class ChannelScreenActivePane {
|
sealed class ChannelScreenActivePane {
|
||||||
data object None : ChannelScreenActivePane()
|
data object None : ChannelScreenActivePane()
|
||||||
data object EmojiPicker : ChannelScreenActivePane()
|
data object EmojiPicker : ChannelScreenActivePane()
|
||||||
|
data object GifPicker : ChannelScreenActivePane()
|
||||||
data object AttachmentPicker : ChannelScreenActivePane()
|
data object AttachmentPicker : ChannelScreenActivePane()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1069,6 +1070,15 @@ fun ChannelScreen(
|
||||||
ChannelScreenActivePane.EmojiPicker
|
ChannelScreenActivePane.EmojiPicker
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPickGif = {
|
||||||
|
if (viewModel.activePane == ChannelScreenActivePane.GifPicker) {
|
||||||
|
viewModel.activePane =
|
||||||
|
ChannelScreenActivePane.None
|
||||||
|
} else {
|
||||||
|
viewModel.activePane =
|
||||||
|
ChannelScreenActivePane.GifPicker
|
||||||
|
}
|
||||||
|
},
|
||||||
onSendMessage = viewModel::sendPendingMessage,
|
onSendMessage = viewModel::sendPendingMessage,
|
||||||
channelType = viewModel.channel?.channelType
|
channelType = viewModel.channel?.channelType
|
||||||
?: ChannelType.TextChannel,
|
?: ChannelType.TextChannel,
|
||||||
|
|
@ -1222,6 +1232,39 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChannelScreenActivePane.GifPicker -> {
|
||||||
|
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.GifPicker) {
|
||||||
|
viewModel.activePane =
|
||||||
|
ChannelScreenActivePane.None
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(4.dp)
|
||||||
|
.navigationBarsPadding()
|
||||||
|
) {
|
||||||
|
chat.stoat.composables.gif.GifPicker(
|
||||||
|
onGifSelected = { gifUrl ->
|
||||||
|
viewModel.putDraftContent(gifUrl, true)
|
||||||
|
viewModel.sendPendingMessage()
|
||||||
|
viewModel.activePane =
|
||||||
|
ChannelScreenActivePane.None
|
||||||
|
},
|
||||||
|
onSearchFocus = {
|
||||||
|
emojiSearchFocused = it
|
||||||
|
},
|
||||||
|
bottomInset = pxAsDp(
|
||||||
|
max(
|
||||||
|
imeCurrentInset - navigationBarsInset,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChannelScreenActivePane.AttachmentPicker -> {
|
ChannelScreenActivePane.AttachmentPicker -> {
|
||||||
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) {
|
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) {
|
||||||
viewModel.activePane =
|
viewModel.activePane =
|
||||||
|
|
@ -1283,6 +1326,39 @@ fun ChannelScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (viewModel.activePane == ChannelScreenActivePane.GifPicker) {
|
||||||
|
BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.GifPicker) {
|
||||||
|
viewModel.activePane =
|
||||||
|
ChannelScreenActivePane.None
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(600.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(4.dp)
|
||||||
|
.navigationBarsPadding()
|
||||||
|
) {
|
||||||
|
chat.stoat.composables.gif.GifPicker(
|
||||||
|
onGifSelected = { gifUrl ->
|
||||||
|
viewModel.putDraftContent(gifUrl, true)
|
||||||
|
viewModel.sendPendingMessage()
|
||||||
|
viewModel.activePane =
|
||||||
|
ChannelScreenActivePane.None
|
||||||
|
},
|
||||||
|
onSearchFocus = {
|
||||||
|
emojiSearchFocused = it
|
||||||
|
},
|
||||||
|
bottomInset = pxAsDp(
|
||||||
|
max(
|
||||||
|
imeCurrentInset - navigationBarsInset,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.imePadding()
|
.imePadding()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M11.5,9H13v6h-1.5zM9,9H6c-0.6,0 -1,0.5 -1,1v4c0,0.5 0.4,1 1,1h3c0.6,0 1,-0.5 1,-1v-2H8.5v1H6.5v-3h3.5V10C10,9.5 9.6,9 9,9zM19,10.5V9h-4.5v6H16v-2h2v-1.5h-2v-1z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -62,6 +62,7 @@ data class Embed(
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val image: Image? = null,
|
val image: Image? = null,
|
||||||
|
val video: Video? = null,
|
||||||
|
|
||||||
@SerialName("icon_url")
|
@SerialName("icon_url")
|
||||||
val iconURL: String? = null,
|
val iconURL: String? = null,
|
||||||
|
|
@ -83,6 +84,13 @@ data class Image(
|
||||||
val size: String? = null
|
val size: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Video(
|
||||||
|
val url: String? = null,
|
||||||
|
val width: Long? = null,
|
||||||
|
val height: Long? = null
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Special(
|
data class Special(
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue