Skip to content

Commit

Permalink
feat: improve support for embedded images (creation from the composer…
Browse files Browse the repository at this point in the history
…, layout) (#792)

* update l10n

* add feature query

* add button to formatting bar

* create dialog to edit image alt text

* update composer contract

* update composer UI

* update composer BL

* filter inline images from attachments

* better support for inline images (vertical spacing before and after)
  • Loading branch information
AkesiSeli authored Feb 5, 2025
1 parent fe67f5c commit d6d4f99
Show file tree
Hide file tree
Showing 17 changed files with 343 additions and 87 deletions.
2 changes: 2 additions & 0 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<string name="action_grant_permission">Grant</string>
<string name="action_hide_content">Hide content</string>
<string name="action_hide_results">Show results</string>
<string name="action_insert_inline_image">Insert inline imagee</string>
<string name="action_insert_link">Insert link</string>
<string name="action_insert_list">Insert list</string>
<string name="action_logout">Logout</string>
Expand Down Expand Up @@ -192,6 +193,7 @@
<string name="gallery_title">Gallery</string>
<string name="help_me_choose_an_instance">Help me choose an instance</string>
<string name="highest_score">highest score</string>
<string name="image_field_alt_text">Alternate text</string>
<string name="image_loading_mode_always">Always</string>
<string name="image_loading_mode_on_demand">On demand</string>
<string name="image_loading_mode_on_wi_fi">In WiFi</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import raccoonforfriendica.composeapp.generated.resources.action_go_back
import raccoonforfriendica.composeapp.generated.resources.action_grant_permission
import raccoonforfriendica.composeapp.generated.resources.action_hide_content
import raccoonforfriendica.composeapp.generated.resources.action_hide_results
import raccoonforfriendica.composeapp.generated.resources.action_insert_inline_image
import raccoonforfriendica.composeapp.generated.resources.action_insert_link
import raccoonforfriendica.composeapp.generated.resources.action_insert_list
import raccoonforfriendica.composeapp.generated.resources.action_logout
Expand Down Expand Up @@ -209,6 +210,7 @@ import raccoonforfriendica.composeapp.generated.resources.gallery_title
import raccoonforfriendica.composeapp.generated.resources.hashtag_people_using
import raccoonforfriendica.composeapp.generated.resources.help_me_choose_an_instance
import raccoonforfriendica.composeapp.generated.resources.highest_score
import raccoonforfriendica.composeapp.generated.resources.image_field_alt_text
import raccoonforfriendica.composeapp.generated.resources.image_loading_mode_always
import raccoonforfriendica.composeapp.generated.resources.image_loading_mode_on_demand
import raccoonforfriendica.composeapp.generated.resources.image_loading_mode_on_wi_fi
Expand Down Expand Up @@ -539,6 +541,8 @@ class SharedStrings : Strings {
@Composable get() = stringResource(Res.string.action_hide_content)
override val actionHideResults: String
@Composable get() = stringResource(Res.string.action_hide_results)
override val actionInsertInlineImage: String
@Composable get() = stringResource(Res.string.action_insert_inline_image)
override val actionInsertLink: String
@Composable get() = stringResource(Res.string.action_insert_link)
override val actionInsertList: String
Expand Down Expand Up @@ -845,6 +849,8 @@ class SharedStrings : Strings {
@Composable get() = stringResource(Res.string.help_me_choose_an_instance)
override val highestScore: String
@Composable get() = stringResource(Res.string.highest_score)
override val imageFieldAltText: String
@Composable get() = stringResource(Res.string.image_field_alt_text)
override val imageLoadingModeAlways: String
@Composable get() = stringResource(Res.string.image_loading_mode_always)
override val imageLoadingModeOnDemand: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.Custom
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.MediaType
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.TimelineEntryModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.UserModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.attachmentsToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.contentToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.pollToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.spoilerToDisplay
Expand Down Expand Up @@ -249,8 +248,7 @@ internal fun CardTimelineItem(
}

// attachments
val attachments =
entry.attachmentsToDisplay.filter { it.url !in entry.embeddedImageUrls }
val attachments = entry.attachmentsToDisplayWithoutInlineImages
val visualAttachments =
attachments.filter { it.type == MediaType.Image || it.type == MediaType.Video }
val audioAttachments = attachments.filter { it.type == MediaType.Audio }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.Custom
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.MediaType
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.TimelineEntryModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.UserModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.attachmentsToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.contentToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.pollToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.spoilerToDisplay
Expand Down Expand Up @@ -190,8 +189,7 @@ internal fun CompactTimelineItem(
modifier = Modifier.padding(horizontal = contentHorizontalPadding),
visible = spoilerActive || spoiler.isEmpty(),
) {
val attachments =
entry.attachmentsToDisplay.filter { it.url !in entry.embeddedImageUrls }
val attachments = entry.attachmentsToDisplayWithoutInlineImages
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
Expand All @@ -12,13 +13,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.CornerSize
import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.Spacing
import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.ancillaryTextAlpha
import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.CustomImage
import com.livefast.eattrash.raccoonforfriendica.core.htmlparse.parseHtml
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.EmojiModel

// lazy wildcard matcher after element name, optional closing "/"
internal val IMAGE_REGEX = Regex("<img.*?/?>")
internal val IMAGE_REGEX = Regex("(img.*?/?)|(<a href=\".*?\"><img.*?/?></a>)")

@Composable
fun ContentBody(
Expand All @@ -42,6 +44,7 @@ fun ContentBody(
modifier =
Modifier
.fillMaxSize()
.padding(vertical = Spacing.s)
.clip(RoundedCornerShape(CornerSize.xl))
.clickable {
onOpenImage?.invoke(data.url)
Expand Down Expand Up @@ -92,7 +95,13 @@ private fun String.splitTextAndImages(): List<String> =
var index = 0
for (match in matches) {
val range = match.range
add(original.substring(index, range.first).trim())
val chunkBefore =
original
.substring(index, range.first)
.trim()
// remove empty paragraph at the end
.replace(Regex("<p( /)?>\$"), "")
add(chunkBefore)
val htmlImage = original.substring(range)
val data = extractImageData(htmlImage)
if (data != null && data.description?.looksLikeAnEmoji != true) {
Expand All @@ -101,6 +110,7 @@ private fun String.splitTextAndImages(): List<String> =
index = range.last + 1
}
if (index < original.length) {
add(original.substring(index, original.length).trim())
val chunkAfter = original.substring(index, original.length).trim()
add(chunkAfter)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.livefast.eattrash.raccoonforfriendica.core.commonui.content

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.CornerSize
import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.Spacing
import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.CustomImage
import com.livefast.eattrash.raccoonforfriendica.core.l10n.LocalStrings
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.AttachmentModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditAttachmentDescriptionDialog(
attachment: AttachmentModel,
onClose: ((String?) -> Unit)? = null,
) {
var textFieldValue by remember {
mutableStateOf(TextFieldValue(text = attachment.description.orEmpty()))
}

BasicAlertDialog(
modifier = Modifier.clip(RoundedCornerShape(CornerSize.xxl)),
onDismissRequest = {
onClose?.invoke(null)
},
) {
Column(
modifier =
Modifier
.background(color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp))
.padding(Spacing.m),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
Text(
text = LocalStrings.current.pictureDescriptionPlaceholder,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.height(Spacing.s))

val url = attachment.url
if (url.isNotEmpty()) {
CustomImage(
modifier = Modifier.height(200.dp),
url = url,
contentScale = ContentScale.Crop,
)
}

TextField(
modifier = Modifier.fillMaxWidth(),
maxLines = 4,
colors =
TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
),
label = {
Text(
text = LocalStrings.current.imageFieldAltText,
style = MaterialTheme.typography.bodyMedium,
)
},
textStyle = MaterialTheme.typography.bodyMedium,
value = textFieldValue,
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Text,
),
onValueChange = { value ->
textFieldValue = value
},
)

Spacer(modifier = Modifier.height(Spacing.xs))
Button(
onClick = {
onClose?.invoke(textFieldValue.text)
},
) {
Text(text = LocalStrings.current.buttonConfirm)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.Custom
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.MediaType
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.TimelineEntryModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.UserModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.attachmentsToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.cardToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.contentToDisplay
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.pollToDisplay
Expand Down Expand Up @@ -232,8 +231,7 @@ internal fun FullTimelineItem(
}

// attachments
val attachments =
entry.attachmentsToDisplay.filter { it.url !in entry.embeddedImageUrls }
val attachments = entry.attachmentsToDisplayWithoutInlineImages
val visualAttachments =
attachments.filter { it.type == MediaType.Image || it.type == MediaType.Video }
val audioAttachments = attachments.filter { it.type == MediaType.Audio }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.livefast.eattrash.raccoonforfriendica.core.commonui.content

import com.livefast.eattrash.raccoonforfriendica.domain.content.data.AttachmentModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.TimelineEntryModel
import com.livefast.eattrash.raccoonforfriendica.domain.content.data.attachmentsToDisplay
import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlHandler
import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser

Expand Down Expand Up @@ -33,11 +35,12 @@ internal fun extractImageData(html: String): ImageData? {
return url?.let { ImageData(url = it, description = description) }
}

internal val TimelineEntryModel.embeddedImageUrls: List<String>
get() {
val data = extractImagesData(content)
return data.map { it.url }
}
internal val TimelineEntryModel.attachmentsToDisplayWithoutInlineImages: List<AttachmentModel>
get() =
run {
val inlineImagesData = extractImagesData(content)
attachmentsToDisplay.filter { attachment -> inlineImagesData.none { it.description == attachment.description } }
}

private fun extractImagesData(html: String): List<ImageData> {
var url: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ fun String.parseHtml(
"p" ->
if (builder.length != 0) {
// separate paragraphs with a blank line
builder.appendLine()
builder.appendLine()
builder.appendLine().appendLine()
}

"span" -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface Strings {
val actionGrantPermission: String @Composable get
val actionHideContent: String @Composable get
val actionHideResults: String @Composable get
val actionInsertInlineImage: String @Composable get
val actionInsertLink: String @Composable get
val actionInsertList: String @Composable get
val actionLogout: String @Composable get
Expand Down Expand Up @@ -196,6 +197,7 @@ interface Strings {
val galleryTitle: String @Composable get
val helpMeChooseAnInstance: String @Composable get
val highestScore: String @Composable get
val imageFieldAltText: String @Composable get
val imageLoadingModeAlways: String @Composable get
val imageLoadingModeOnDemand: String @Composable get
val imageLoadingModeOnWiFi: String @Composable get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class MockStrings : Strings {
@Composable get() = retrieve("actionHideContent")
override val actionHideResults: String
@Composable get() = retrieve("actionHideResults")
override val actionInsertInlineImage: String
@Composable get() = retrieve("actionInsertInlineImage")
override val actionInsertLink: String
@Composable get() = retrieve("actionInsertLink")
override val actionInsertList: String
Expand Down Expand Up @@ -399,6 +401,8 @@ class MockStrings : Strings {
@Composable get() = retrieve("helpMeChooseAnInstance")
override val highestScore: String
@Composable get() = retrieve("highestScore")
override val imageFieldAltText: String
@Composable get() = retrieve("imageFieldAltText")
override val imageLoadingModeAlways: String
@Composable get() = retrieve("imageLoadingModeAlways")
override val imageLoadingModeOnDemand: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ data class NodeFeatures(
val supportsAnnouncements: Boolean = false,
val supportsDislike: Boolean = false,
val supportsTranslation: Boolean = false,
val supportsInlineImages: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal class DefaultSupportedFeatureRepository(
supportsAnnouncements = info.isMastodon,
supportsDislike = info.isFriendica,
supportsTranslation = info.isMastodon,
supportsInlineImages = info.isFriendica,
)
}
}
Expand Down
Loading

0 comments on commit d6d4f99

Please sign in to comment.