=
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("\$"), "")
+ add(chunkBefore)
val htmlImage = original.substring(range)
val data = extractImageData(htmlImage)
if (data != null && data.description?.looksLikeAnEmoji != true) {
@@ -101,6 +110,7 @@ private fun String.splitTextAndImages(): List =
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)
}
}
diff --git a/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/EditAttachmentDescriptionDialog.kt b/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/EditAttachmentDescriptionDialog.kt
new file mode 100644
index 000000000..44cb3d91e
--- /dev/null
+++ b/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/EditAttachmentDescriptionDialog.kt
@@ -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)
+ }
+ }
+ }
+}
diff --git a/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/FullTimelineItem.kt b/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/FullTimelineItem.kt
index 442bfda4c..1cccf450d 100644
--- a/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/FullTimelineItem.kt
+++ b/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/FullTimelineItem.kt
@@ -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
@@ -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 }
diff --git a/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/ImageData.kt b/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/ImageData.kt
index 3d56acea6..c23651255 100644
--- a/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/ImageData.kt
+++ b/core/commonui/content/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/commonui/content/ImageData.kt
@@ -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
@@ -33,11 +35,12 @@ internal fun extractImageData(html: String): ImageData? {
return url?.let { ImageData(url = it, description = description) }
}
-internal val TimelineEntryModel.embeddedImageUrls: List
- get() {
- val data = extractImagesData(content)
- return data.map { it.url }
- }
+internal val TimelineEntryModel.attachmentsToDisplayWithoutInlineImages: List
+ get() =
+ run {
+ val inlineImagesData = extractImagesData(content)
+ attachmentsToDisplay.filter { attachment -> inlineImagesData.none { it.description == attachment.description } }
+ }
private fun extractImagesData(html: String): List {
var url: String? = null
diff --git a/core/htmlparse/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/htmlparse/ParseHtml.kt b/core/htmlparse/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/htmlparse/ParseHtml.kt
index b3c2ba962..a4e391141 100644
--- a/core/htmlparse/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/htmlparse/ParseHtml.kt
+++ b/core/htmlparse/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/htmlparse/ParseHtml.kt
@@ -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
diff --git a/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/Strings.kt b/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/Strings.kt
index cb4866efb..f4a1b2ac1 100644
--- a/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/Strings.kt
+++ b/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/Strings.kt
@@ -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
@@ -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
diff --git a/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/testutils/MockStrings.kt b/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/testutils/MockStrings.kt
index 782e6f07d..9b5f5a58a 100644
--- a/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/testutils/MockStrings.kt
+++ b/core/l10n/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/core/l10n/testutils/MockStrings.kt
@@ -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
@@ -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
diff --git a/domain/content/data/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/data/NodeFeatures.kt b/domain/content/data/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/data/NodeFeatures.kt
index 92708db18..9640d0b5e 100644
--- a/domain/content/data/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/data/NodeFeatures.kt
+++ b/domain/content/data/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/data/NodeFeatures.kt
@@ -14,4 +14,5 @@ data class NodeFeatures(
val supportsAnnouncements: Boolean = false,
val supportsDislike: Boolean = false,
val supportsTranslation: Boolean = false,
+ val supportsInlineImages: Boolean = false,
)
diff --git a/domain/content/repository/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/repository/DefaultSupportedFeatureRepository.kt b/domain/content/repository/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/repository/DefaultSupportedFeatureRepository.kt
index 756f47aea..9d5dcb52c 100644
--- a/domain/content/repository/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/repository/DefaultSupportedFeatureRepository.kt
+++ b/domain/content/repository/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/domain/content/repository/DefaultSupportedFeatureRepository.kt
@@ -27,6 +27,7 @@ internal class DefaultSupportedFeatureRepository(
supportsAnnouncements = info.isMastodon,
supportsDislike = info.isFriendica,
supportsTranslation = info.isMastodon,
+ supportsInlineImages = info.isFriendica,
)
}
}
diff --git a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerMviModel.kt b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerMviModel.kt
index b906c7fd3..98a6835b5 100644
--- a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerMviModel.kt
+++ b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerMviModel.kt
@@ -80,6 +80,21 @@ interface ComposerMviModel :
override fun hashCode(): Int = byteArray.contentHashCode()
}
+ data class AddInlineImageStep1(
+ val byteArray: ByteArray,
+ ) : Intent {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || this::class != other::class) return false
+
+ other as AddAttachment
+
+ return byteArray.contentEquals(other.byteArray)
+ }
+
+ override fun hashCode(): Int = byteArray.contentHashCode()
+ }
+
data class AddAttachmentsFromGallery(
val attachments: List,
) : Intent
@@ -89,6 +104,11 @@ interface ComposerMviModel :
val description: String,
) : Intent
+ data class AddInlineImageStep2(
+ val attachment: AttachmentModel,
+ val description: String,
+ ) : Intent
+
data class RemoveAttachment(
val attachment: AttachmentModel,
) : Intent
@@ -246,6 +266,7 @@ interface ComposerMviModel :
val shouldShowHashtagSuggestions: Boolean = false,
val hashtagSuggestionsLoading: Boolean = false,
val hashtagSuggestions: List = emptyList(),
+ val inlineImagesSupported: Boolean = false,
)
sealed interface Effect {
@@ -273,7 +294,15 @@ interface ComposerMviModel :
data class OpenPreview(
val entry: TimelineEntryModel,
- ) : ValidationError
+ ) : Effect
+
+ data class TriggerAttachmentEdit(
+ val attachment: AttachmentModel,
+ ) : Effect
+
+ data class TriggerInlineImageEdit(
+ val attachment: AttachmentModel,
+ ) : Effect
}
}
diff --git a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerScreen.kt b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerScreen.kt
index 42d585cc9..191cae0c2 100644
--- a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerScreen.kt
+++ b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerScreen.kt
@@ -65,9 +65,9 @@ import com.livefast.eattrash.raccoonforfriendica.core.appearance.theme.toWindowI
import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.CustomDropDown
import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.CustomModalBottomSheet
import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.CustomModalBottomSheetItem
-import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.EditTextualInfoDialog
import com.livefast.eattrash.raccoonforfriendica.core.commonui.components.ProgressHud
import com.livefast.eattrash.raccoonforfriendica.core.commonui.content.CustomConfirmDialog
+import com.livefast.eattrash.raccoonforfriendica.core.commonui.content.EditAttachmentDescriptionDialog
import com.livefast.eattrash.raccoonforfriendica.core.commonui.content.InsertEmojiBottomSheet
import com.livefast.eattrash.raccoonforfriendica.core.commonui.content.OptionId
import com.livefast.eattrash.raccoonforfriendica.core.commonui.content.SettingsSwitchRow
@@ -130,19 +130,29 @@ class ComposerScreen(
val pastScheduleDateError = LocalStrings.current.messageScheduleDateInThePast
val invalidPollError = LocalStrings.current.messageInvalidPollError
val genericError = LocalStrings.current.messageGenericError
- var openImagePicker by remember { mutableStateOf(false) }
- if (openImagePicker) {
+ var imagePickerRequest by remember { mutableStateOf(null) }
+ if (imagePickerRequest != null) {
galleryHelper.getImageFromGallery { bytes ->
- openImagePicker = false
+ val originalRequest = imagePickerRequest
+ imagePickerRequest = null
if (bytes.isNotEmpty()) {
- model.reduce(ComposerMviModel.Intent.AddAttachment(bytes))
+ when (originalRequest) {
+ ImagePickerRequest.Attachment ->
+ model.reduce(ComposerMviModel.Intent.AddAttachment(bytes))
+
+ ImagePickerRequest.InlineImage ->
+ model.reduce(ComposerMviModel.Intent.AddInlineImageStep1(bytes))
+
+ else -> Unit
+ }
}
}
}
var photoGalleryPickerOpen by remember { mutableStateOf(false) }
var linkDialogOpen by remember { mutableStateOf(false) }
var selectCircleDialogOpen by remember { mutableStateOf(false) }
- var attachmentWithDescriptionBeingEdited by remember { mutableStateOf(null) }
+ var attachmentBeingEdited by remember { mutableStateOf(null) }
+ var inlineImageBeingEdited by remember { mutableStateOf(null) }
var hasSpoilerFieldFocus by remember { mutableStateOf(false) }
var hasTitleFocus by remember { mutableStateOf(false) }
val isBeingEdited = remember { scheduledPostId != null || editedPostId != null }
@@ -236,6 +246,11 @@ class ComposerScreen(
ComposerMviModel.Effect.Success -> navigationCoordinator.pop()
is ComposerMviModel.Effect.OpenPreview -> previewEntry = event.entry
+ is ComposerMviModel.Effect.TriggerAttachmentEdit ->
+ attachmentBeingEdited = event.attachment
+
+ is ComposerMviModel.Effect.TriggerInlineImageEdit ->
+ inlineImageBeingEdited = event.attachment
}
}.launchIn(this)
}
@@ -366,13 +381,6 @@ class ComposerScreen(
)
}
- if (uiState.galleryFeatureSupported && uiState.poll == null) {
- this +=
- CustomOptions.SelectFromGallery.toOption(
- label = LocalStrings.current.actionAddImageFromGallery,
- )
- }
-
if (uiState.pollFeatureSupported && uiState.attachments.isEmpty()) {
this +=
CustomOptions.TogglePoll.toOption(
@@ -463,22 +471,6 @@ class ComposerScreen(
),
)
- CustomOptions.SelectAttachment -> {
- val limit =
- uiState.attachmentLimit ?: Int.MAX_VALUE
- if (uiState.attachments.size < limit) {
- openImagePicker = true
- }
- }
-
- CustomOptions.SelectFromGallery -> {
- val limit =
- uiState.attachmentLimit ?: Int.MAX_VALUE
- if (uiState.attachments.size < limit) {
- photoGalleryPickerOpen = true
- }
- }
-
CustomOptions.TogglePoll ->
if (uiState.poll == null) {
model.reduce(ComposerMviModel.Intent.AddPoll)
@@ -559,18 +551,22 @@ class ComposerScreen(
}
UtilsBar(
modifier = Modifier.fillMaxWidth(),
- onAttachmentClicked = {
- val limit = uiState.attachmentLimit ?: Int.MAX_VALUE
- if (uiState.attachments.size < limit) {
- openImagePicker = true
- }
- },
supportsRichEditing = uiState.supportsRichEditing,
+ supportsInlineImages = uiState.inlineImagesSupported,
hasPoll = uiState.poll != null,
publicationType = uiState.publicationType,
onLinkClicked = {
linkDialogOpen = true
},
+ onAttachmentClicked = {
+ val limit = uiState.attachmentLimit ?: Int.MAX_VALUE
+ if (uiState.attachments.size < limit) {
+ imagePickerRequest = ImagePickerRequest.Attachment
+ }
+ },
+ onInlineImageClicked = {
+ imagePickerRequest = ImagePickerRequest.InlineImage
+ },
onBoldClicked = {
model.reduce(
ComposerMviModel.Intent.AddBoldFormat(
@@ -827,7 +823,7 @@ class ComposerScreen(
model.reduce(ComposerMviModel.Intent.RemoveAttachment(attachment))
},
onEditDescription = { attachment ->
- attachmentWithDescriptionBeingEdited = attachment
+ attachmentBeingEdited = attachment
},
)
}
@@ -883,22 +879,36 @@ class ComposerScreen(
)
}
- if (attachmentWithDescriptionBeingEdited != null) {
- EditTextualInfoDialog(
- label = LocalStrings.current.pictureDescriptionPlaceholder,
- value = attachmentWithDescriptionBeingEdited?.description.orEmpty(),
- minLines = 3,
+ val editedAttachment = attachmentBeingEdited
+ val editedInlineImage = inlineImageBeingEdited
+ if (editedAttachment != null) {
+ EditAttachmentDescriptionDialog(
+ attachment = editedAttachment,
onClose = { newValue ->
- val attachment = attachmentWithDescriptionBeingEdited
- if (attachment != null && newValue != null) {
+ if (newValue != null) {
model.reduce(
ComposerMviModel.Intent.EditAttachmentDescription(
- attachment = attachment,
+ attachment = editedAttachment,
description = newValue,
),
)
}
- attachmentWithDescriptionBeingEdited = null
+ attachmentBeingEdited = null
+ },
+ )
+ } else if (editedInlineImage != null) {
+ EditAttachmentDescriptionDialog(
+ attachment = editedInlineImage,
+ onClose = { newValue ->
+ if (newValue != null) {
+ model.reduce(
+ ComposerMviModel.Intent.AddInlineImageStep2(
+ attachment = editedInlineImage,
+ description = newValue,
+ ),
+ )
+ }
+ inlineImageBeingEdited = null
},
)
}
@@ -1104,10 +1114,6 @@ class ComposerScreen(
}
private sealed interface CustomOptions : OptionId.Custom {
- data object SelectAttachment : CustomOptions
-
- data object SelectFromGallery : CustomOptions
-
data object TogglePoll : CustomOptions
data object ToggleTitle : CustomOptions
@@ -1130,3 +1136,9 @@ private sealed interface CustomOptions : OptionId.Custom {
data object ChangeMarkupMode : CustomOptions
}
+
+private sealed interface ImagePickerRequest {
+ data object Attachment : ImagePickerRequest
+
+ data object InlineImage : ImagePickerRequest
+}
diff --git a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerViewModel.kt b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerViewModel.kt
index 3db17e299..7e46ad5f1 100644
--- a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerViewModel.kt
+++ b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/ComposerViewModel.kt
@@ -128,6 +128,7 @@ class ComposerViewModel(
titleFeatureSupported = features.supportsEntryTitles,
galleryFeatureSupported = features.supportsPhotoGallery,
pollFeatureSupported = features.supportsPolls,
+ inlineImagesSupported = features.supportsInlineImages,
availableVisibilities =
buildList {
this += Visibility.Public
@@ -275,7 +276,11 @@ class ComposerViewModel(
}
}
- is ComposerMviModel.Intent.AddAttachment -> uploadAttachment(intent.byteArray)
+ is ComposerMviModel.Intent.AddAttachment ->
+ uploadAttachment(
+ byteArray = intent.byteArray,
+ isInlineImage = false,
+ )
is ComposerMviModel.Intent.EditAttachmentDescription ->
updateAttachmentDescription(intent.attachment, intent.description)
@@ -509,6 +514,17 @@ class ComposerViewModel(
)
is ComposerMviModel.Intent.ChangeMarkupMode -> changeMarkupMode(intent.mode)
+ is ComposerMviModel.Intent.AddInlineImageStep1 ->
+ uploadAttachment(
+ byteArray = intent.byteArray,
+ isInlineImage = true,
+ )
+
+ is ComposerMviModel.Intent.AddInlineImageStep2 ->
+ insertInlineImage(
+ intent.attachment,
+ intent.description,
+ )
}
}
@@ -1209,18 +1225,25 @@ class ComposerViewModel(
}
}
- private fun uploadAttachment(byteArray: ByteArray) {
+ private fun uploadAttachment(
+ byteArray: ByteArray,
+ isInlineImage: Boolean,
+ ) {
screenModelScope.launch {
- updateState {
- it.copy(
- attachments =
- it.attachments +
- AttachmentModel(
- id = PLACEHOLDER_ID,
- url = "",
- loading = true,
- ),
- )
+ if (isInlineImage) {
+ updateState { it.copy(loading = true) }
+ } else {
+ updateState {
+ it.copy(
+ attachments =
+ it.attachments +
+ AttachmentModel(
+ id = PLACEHOLDER_ID,
+ url = "",
+ loading = true,
+ ),
+ )
+ }
}
val attachment =
if (shouldUserPhotoRepository) {
@@ -1229,12 +1252,18 @@ class ComposerViewModel(
mediaRepository.create(byteArray)
}
if (attachment != null) {
- newAttachmentIds += attachment.id
- updateState {
- it.copy(
- attachments = it.attachments.filter { a -> a.id != PLACEHOLDER_ID } + attachment,
- hasUnsavedChanges = true,
- )
+ if (isInlineImage) {
+ updateState { it.copy(loading = false) }
+ emitEffect(ComposerMviModel.Effect.TriggerInlineImageEdit(attachment))
+ } else {
+ newAttachmentIds += attachment.id
+ updateState {
+ it.copy(
+ attachments = it.attachments.filter { a -> a.id != PLACEHOLDER_ID } + attachment,
+ hasUnsavedChanges = true,
+ )
+ }
+ emitEffect(ComposerMviModel.Effect.TriggerAttachmentEdit(attachment))
}
} else {
emitEffect(ComposerMviModel.Effect.Failure())
@@ -1584,6 +1613,38 @@ class ComposerViewModel(
}
}
+ private fun insertInlineImage(
+ attachment: AttachmentModel,
+ description: String,
+ ) {
+ screenModelScope.launch {
+ val primaryUrl = attachment.url
+ val secondaryUrl = attachment.thumbnail ?: attachment.previewUrl ?: primaryUrl
+ val additionalPart =
+ buildString {
+ append("\n")
+ append("[url=$primaryUrl]")
+ append("[img=$secondaryUrl]")
+ append(description)
+ append("[/img]")
+ append("[/url]")
+ append("\n")
+ }
+ val newValue =
+ getNewTextFieldValue(
+ value = uiState.value.bodyValue,
+ additionalPart = additionalPart,
+ offsetAfter = additionalPart.length,
+ )
+ updateState {
+ it.copy(
+ bodyValue = newValue,
+ hasUnsavedChanges = true,
+ )
+ }
+ }
+ }
+
private fun submit(
enableAltTextCheck: Boolean,
enableParentVisibilityCheck: Boolean,
diff --git a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/components/UtilsBar.kt b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/components/UtilsBar.kt
index 8c14a40e5..830d39805 100644
--- a/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/components/UtilsBar.kt
+++ b/feature/composer/src/commonMain/kotlin/com/livefast/eattrash/raccoonforfriendica/feature/composer/components/UtilsBar.kt
@@ -8,6 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ScheduleSend
import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.FormatBold
import androidx.compose.material.icons.filled.FormatItalic
@@ -31,6 +32,7 @@ internal fun UtilsBar(
modifier: Modifier = Modifier,
hasPoll: Boolean = false,
supportsRichEditing: Boolean = true,
+ supportsInlineImages: Boolean = false,
publicationType: PublicationType,
onLinkClicked: (() -> Unit)? = null,
onAttachmentClicked: (() -> Unit)? = null,
@@ -40,6 +42,7 @@ internal fun UtilsBar(
onStrikethroughClicked: (() -> Unit)? = null,
onCodeClicked: (() -> Unit)? = null,
onSubmitClicked: (() -> Unit)? = null,
+ onInlineImageClicked: (() -> Unit)? = null,
) {
Row(
modifier = modifier.padding(horizontal = Spacing.xxs),
@@ -63,6 +66,20 @@ internal fun UtilsBar(
)
}
+ if (supportsInlineImages) {
+ IconButton(
+ onClick = {
+ onInlineImageClicked?.invoke()
+ },
+ ) {
+ Icon(
+ imageVector = Icons.Default.AddPhotoAlternate,
+ contentDescription = LocalStrings.current.actionInsertInlineImage,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+
if (supportsRichEditing) {
// insert link button
IconButton(