Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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())
}
}
82 changes: 67 additions & 15 deletions app/src/main/java/chat/stoat/composables/chat/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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) {
CompositionLocalProvider(
Expand Down Expand Up @@ -520,21 +528,65 @@ fun Message(
it.forEach { embed ->
when (embed.type) {
"Website", "Text" -> {
val embedIsEmpty =
embed.title == null && embed.description == null && embed.iconURL == null && embed.image == null

if (embedIsEmpty) {
// if we do not emit anything, compose will cause an internal error.
// FIXME if you are doing fixme's anyways then check if this is still an issue
Box {}
return@forEach
}
val isGIF = embed.type == "Website" && (
embed.special?.type == "GIF" ||
embed.originalURL?.startsWith("https://giphy.com") == true
)

// Prefer image URL; for video (mp4), derive GIF URL from giphy page URL
val gifUrl = if (isGIF) {
embed.image?.url ?: run {
val pageUrl = embed.url ?: embed.originalURL
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" -> {
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/chat/stoat/composables/chat/MessageField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ fun MessageField(
onAddAttachment: () -> Unit,
onCommitAttachment: (Uri) -> Unit,
onPickEmoji: () -> Unit,
onPickGif: () -> Unit = {},
onSendMessage: () -> Unit,
channelType: ChannelType,
channelName: String,
Expand Down Expand Up @@ -596,6 +597,21 @@ fun MessageField(
.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))

AnimatedVisibility(
Expand Down
Loading
Loading