add dedicated redacted message type in order to render it separately

This commit is contained in:
Adam Brown 2022-11-04 09:10:48 +00:00
parent 82de03e963
commit 7a426ab1e7
13 changed files with 79 additions and 17 deletions

View File

@ -124,6 +124,15 @@ sealed class RoomEvent {
} }
data class Redacted(
override val eventId: EventId,
override val utcTimestamp: Long,
override val author: RoomMember,
) : RoomEvent() {
override val edited: Boolean = false
override val meta: MessageMeta = MessageMeta.FromServer
}
data class Message( data class Message(
override val eventId: EventId, override val eventId: EventId,
override val utcTimestamp: Long, override val utcTimestamp: Long,
@ -131,7 +140,6 @@ sealed class RoomEvent {
override val author: RoomMember, override val author: RoomMember,
override val meta: MessageMeta, override val meta: MessageMeta,
override val edited: Boolean = false, override val edited: Boolean = false,
val redacted: Boolean = false,
) : RoomEvent() ) : RoomEvent()
data class Reply( data class Reply(

View File

@ -8,6 +8,11 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Recycling
import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -31,12 +36,14 @@ import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest import coil.request.ImageRequest
private val ENCRYPTED_MESSAGE = RichText(listOf(RichText.Part.Normal("Encrypted message"))) private val ENCRYPTED_MESSAGE = RichText(listOf(RichText.Part.Normal("Encrypted message")))
private val DELETED_MESSAGE = RichText(listOf(RichText.Part.Italic("Message deleted")))
sealed interface BubbleModel { sealed interface BubbleModel {
val event: Event val event: Event
data class Text(val content: RichText, override val event: Event) : BubbleModel data class Text(val content: RichText, override val event: Event) : BubbleModel
data class Encrypted(override val event: Event) : BubbleModel data class Encrypted(override val event: Event) : BubbleModel
data class Redacted(override val event: Event) : BubbleModel
data class Image(val imageContent: ImageContent, val imageRequest: ImageRequest, override val event: Event) : BubbleModel { data class Image(val imageContent: ImageContent, val imageRequest: ImageRequest, override val event: Event) : BubbleModel {
data class ImageContent(val width: Int?, val height: Int?, val url: String) data class ImageContent(val width: Int?, val height: Int?, val url: String)
} }
@ -64,6 +71,7 @@ fun MessageBubble(bubble: BubbleMeta, model: BubbleModel, status: @Composable ()
is BubbleModel.Encrypted -> EncryptedBubble(bubble, model, status, itemisedLongClick) is BubbleModel.Encrypted -> EncryptedBubble(bubble, model, status, itemisedLongClick)
is BubbleModel.Image -> ImageBubble(bubble, model, status, onItemClick = { actions.onImageClick(model) }, itemisedLongClick) is BubbleModel.Image -> ImageBubble(bubble, model, status, onItemClick = { actions.onImageClick(model) }, itemisedLongClick)
is BubbleModel.Reply -> ReplyBubble(bubble, model, status, itemisedLongClick) is BubbleModel.Reply -> ReplyBubble(bubble, model, status, itemisedLongClick)
is BubbleModel.Redacted -> RedactedBubble(bubble, model, status)
} }
} }
@ -156,6 +164,8 @@ private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @C
is BubbleModel.Reply -> { is BubbleModel.Reply -> {
// TODO - a reply to a reply // TODO - a reply to a reply
} }
is BubbleModel.Redacted -> RedactedContent(bubble)
} }
} }
@ -180,6 +190,8 @@ private fun ReplyBubble(bubble: BubbleMeta, model: BubbleModel.Reply, status: @C
is BubbleModel.Reply -> { is BubbleModel.Reply -> {
// TODO - a reply to a reply // TODO - a reply to a reply
} }
is BubbleModel.Redacted -> RedactedContent(bubble)
} }
Footer(model.event, bubble, status) Footer(model.event, bubble, status)
@ -206,10 +218,29 @@ private fun Int.scalerFor(max: Float): Float {
return max / this return max / this
} }
@Composable
private fun RedactedBubble(bubble: BubbleMeta, model: BubbleModel.Redacted, status: @Composable () -> Unit) {
Bubble(bubble) {
if (bubble.isNotSelf()) {
AuthorName(model.event, bubble)
}
RedactedContent(bubble)
Footer(model.event, bubble, status)
}
}
@Composable
private fun RedactedContent(bubble: BubbleMeta) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 4.dp, end = 4.dp)) {
Icon(modifier = Modifier.height(20.dp), imageVector = Icons.Outlined.DeleteOutline, contentDescription = null)
Spacer(Modifier.width(4.dp))
TextContent(bubble, text = DELETED_MESSAGE, fontSize = 13)
}
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun Bubble(bubble: BubbleMeta, onItemClick: () -> Unit, onLongClick: () -> Unit, content: @Composable () -> Unit) { private fun Bubble(bubble: BubbleMeta, onItemClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
Box(modifier = Modifier.padding(start = 6.dp)) { Box(modifier = Modifier.padding(start = 6.dp)) {
Box( Box(
Modifier Modifier
@ -217,7 +248,7 @@ private fun Bubble(bubble: BubbleMeta, onItemClick: () -> Unit, onLongClick: ()
.clip(bubble.shape) .clip(bubble.shape)
.background(bubble.background) .background(bubble.background)
.height(IntrinsicSize.Max) .height(IntrinsicSize.Max)
.combinedClickable(onLongClick = onLongClick, onClick = onItemClick), .combinedClickable(onLongClick = onLongClick, onClick = onItemClick ?: {}),
) { ) {
Column( Column(
Modifier Modifier
@ -247,12 +278,16 @@ private fun Footer(event: BubbleModel.Event, bubble: BubbleMeta, status: @Compos
} }
@Composable @Composable
private fun TextContent(bubble: BubbleMeta, text: RichText) { private fun TextContent(bubble: BubbleMeta, text: RichText, isAlternative: Boolean = false, fontSize: Int = 15) {
val annotatedText = text.toAnnotatedText() val annotatedText = text.toAnnotatedText()
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
ClickableText( ClickableText(
text = annotatedText, text = annotatedText,
style = TextStyle(color = bubble.textColor(), fontSize = 15.sp, textAlign = TextAlign.Start), style = TextStyle(
color = if (isAlternative) bubble.textColor().copy(alpha = 0.8f) else bubble.textColor(),
fontSize = fontSize.sp,
textAlign = TextAlign.Start
),
modifier = Modifier.wrapContentSize(), modifier = Modifier.wrapContentSize(),
onClick = { onClick = {
annotatedText.getStringAnnotations("url", it, it).firstOrNull()?.let { annotatedText.getStringAnnotations("url", it, it).firstOrNull()?.let {

View File

@ -316,6 +316,7 @@ private fun RoomEvent.toModel(): BubbleModel {
return when (this) { return when (this) {
is RoomEvent.Message -> BubbleModel.Text(this.content.toApp(), event) is RoomEvent.Message -> BubbleModel.Text(this.content.toApp(), event)
is RoomEvent.Encrypted -> BubbleModel.Encrypted(event) is RoomEvent.Encrypted -> BubbleModel.Encrypted(event)
is RoomEvent.Redacted -> BubbleModel.Redacted(event)
is RoomEvent.Image -> { is RoomEvent.Image -> {
val imageRequest = LocalImageRequestFactory.current val imageRequest = LocalImageRequestFactory.current
.memoryCacheKey(this.imageMeta.url) .memoryCacheKey(this.imageMeta.url)

View File

@ -176,6 +176,7 @@ private fun RoomEvent.toSendMessageReply() = SendMessage.TextMessage.Reply(
originalMessage = when (this) { originalMessage = when (this) {
is RoomEvent.Image -> TODO() is RoomEvent.Image -> TODO()
is RoomEvent.Reply -> TODO() is RoomEvent.Reply -> TODO()
is RoomEvent.Redacted -> TODO()
is RoomEvent.Message -> this.content.asString() is RoomEvent.Message -> this.content.asString()
is RoomEvent.Encrypted -> error("Should never happen") is RoomEvent.Encrypted -> error("Should never happen")
}, },
@ -190,6 +191,7 @@ private fun initialComposerState(initialAttachments: List<MessageAttachment>?) =
private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) { private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) {
is BubbleModel.Encrypted -> CopyableResult.NothingToCopy is BubbleModel.Encrypted -> CopyableResult.NothingToCopy
is BubbleModel.Redacted -> CopyableResult.NothingToCopy
is BubbleModel.Image -> CopyableResult.NothingToCopy is BubbleModel.Image -> CopyableResult.NothingToCopy
is BubbleModel.Reply -> this.reply.findCopyableContent() is BubbleModel.Reply -> this.reply.findCopyableContent()
is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content.asString())) is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content.asString()))

View File

@ -15,6 +15,7 @@ class RoomEventsToNotifiableMapper {
is RoomEvent.Message -> this.content.asString() is RoomEvent.Message -> this.content.asString()
is RoomEvent.Reply -> this.message.toNotifiableContent() is RoomEvent.Reply -> this.message.toNotifiableContent()
is RoomEvent.Encrypted -> "Encrypted message" is RoomEvent.Encrypted -> "Encrypted message"
is RoomEvent.Redacted -> "Deleted message"
} }
} }

View File

@ -48,6 +48,7 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo)) is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo))
is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo)) is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo))
is RoomEvent.Encrypted -> this.copy(meta = metaMapper.toMeta(echo)) is RoomEvent.Encrypted -> this.copy(meta = metaMapper.toMeta(echo))
is RoomEvent.Redacted -> this
} }
} }

View File

@ -88,9 +88,10 @@ fun MatrixRoomState.engine() = RoomState(
fun MatrixRoomEvent.engine(): RoomEvent = when (this) { fun MatrixRoomEvent.engine(): RoomEvent = when (this) {
is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited) is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited)
is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited, this.redacted) is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited)
is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine()) is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine())
is MatrixRoomEvent.Encrypted -> RoomEvent.Encrypted(this.eventId, this.utcTimestamp, this.author, this.meta.engine()) is MatrixRoomEvent.Encrypted -> RoomEvent.Encrypted(this.eventId, this.utcTimestamp, this.author, this.meta.engine())
is MatrixRoomEvent.Redacted -> RoomEvent.Redacted(this.eventId, this.utcTimestamp, this.author)
} }
fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta( fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta(

View File

@ -16,7 +16,6 @@ sealed class RoomEvent {
abstract val utcTimestamp: Long abstract val utcTimestamp: Long
abstract val author: RoomMember abstract val author: RoomMember
abstract val meta: MessageMeta abstract val meta: MessageMeta
abstract val redacted: Boolean
@Serializable @Serializable
@SerialName("encrypted") @SerialName("encrypted")
@ -26,7 +25,6 @@ sealed class RoomEvent {
@SerialName("author") override val author: RoomMember, @SerialName("author") override val author: RoomMember,
@SerialName("meta") override val meta: MessageMeta, @SerialName("meta") override val meta: MessageMeta,
@SerialName("edited") val edited: Boolean = false, @SerialName("edited") val edited: Boolean = false,
@SerialName("redacted") override val redacted: Boolean = false,
@SerialName("encrypted_content") val encryptedContent: MegOlmV1, @SerialName("encrypted_content") val encryptedContent: MegOlmV1,
) : RoomEvent() { ) : RoomEvent() {
@ -40,6 +38,16 @@ sealed class RoomEvent {
} }
@Serializable
@SerialName("redacted")
data class Redacted(
@SerialName("event_id") override val eventId: EventId,
@SerialName("timestamp") override val utcTimestamp: Long,
@SerialName("author") override val author: RoomMember,
) : RoomEvent() {
override val meta: MessageMeta = MessageMeta.FromServer
}
@Serializable @Serializable
@SerialName("message") @SerialName("message")
data class Message( data class Message(
@ -49,7 +57,6 @@ sealed class RoomEvent {
@SerialName("author") override val author: RoomMember, @SerialName("author") override val author: RoomMember,
@SerialName("meta") override val meta: MessageMeta, @SerialName("meta") override val meta: MessageMeta,
@SerialName("edited") val edited: Boolean = false, @SerialName("edited") val edited: Boolean = false,
@SerialName("redacted") override val redacted: Boolean = false,
) : RoomEvent() ) : RoomEvent()
@Serializable @Serializable
@ -63,7 +70,6 @@ sealed class RoomEvent {
override val utcTimestamp: Long = message.utcTimestamp override val utcTimestamp: Long = message.utcTimestamp
override val author: RoomMember = message.author override val author: RoomMember = message.author
override val meta: MessageMeta = message.meta override val meta: MessageMeta = message.meta
override val redacted: Boolean = message.redacted
} }
@ -76,7 +82,6 @@ sealed class RoomEvent {
@SerialName("author") override val author: RoomMember, @SerialName("author") override val author: RoomMember,
@SerialName("meta") override val meta: MessageMeta, @SerialName("meta") override val meta: MessageMeta,
@SerialName("edited") val edited: Boolean = false, @SerialName("edited") val edited: Boolean = false,
@SerialName("redacted") override val redacted: Boolean = false,
) : RoomEvent() { ) : RoomEvent() {
@Serializable @Serializable

View File

@ -29,6 +29,7 @@ internal class RoomEventsDecrypter(
) )
is RoomEvent.Image -> event is RoomEvent.Image -> event
is RoomEvent.Redacted -> event
} }
private suspend fun RoomEvent.Encrypted.decrypt(userCredentials: UserCredentials) = when (val result = this.decryptContent()) { private suspend fun RoomEvent.Encrypted.decrypt(userCredentials: UserCredentials) = when (val result = this.decryptContent()) {
@ -51,7 +52,6 @@ internal class RoomEventsDecrypter(
author = this.author, author = this.author,
meta = this.meta, meta = this.meta,
edited = this.edited, edited = this.edited,
redacted = this.redacted,
content = richMessageParser.parse(content.body ?: "") content = richMessageParser.parse(content.body ?: "")
) )
@ -61,7 +61,6 @@ internal class RoomEventsDecrypter(
author = this.author, author = this.author,
meta = this.meta, meta = this.meta,
edited = this.edited, edited = this.edited,
redacted = this.redacted,
imageMeta = RoomEvent.Image.ImageMeta( imageMeta = RoomEvent.Image.ImageMeta(
width = content.info?.width, width = content.info?.width,
height = content.info?.height, height = content.info?.height,

View File

@ -33,8 +33,8 @@ class RoomDataSource(
roomStore.remove(roomsLeft) roomStore.remove(roomsLeft)
} }
suspend fun redact(roomId: RoomId, event: EventId) { suspend fun redact(roomId: RoomId, eventId: EventId) {
val eventToRedactFromCache = roomCache[roomId]?.events?.find { it.eventId == event } val eventToRedactFromCache = roomCache[roomId]?.events?.find { it.eventId == eventId }
val redactedEvent = when { val redactedEvent = when {
eventToRedactFromCache != null -> { eventToRedactFromCache != null -> {
eventToRedactFromCache.redact().also { redacted -> eventToRedactFromCache.redact().also { redacted ->
@ -44,14 +44,14 @@ class RoomDataSource(
} }
} }
else -> roomStore.findEvent(event)?.redact() else -> roomStore.findEvent(eventId)?.redact()
} }
redactedEvent?.let { roomStore.persist(roomId, listOf(it)) } redactedEvent?.let { roomStore.persist(roomId, listOf(it)) }
} }
} }
private fun RoomEvent.redact() = RoomEvent.Message(this.eventId, this.utcTimestamp, RichText.of("Redacted"), this.author, this.meta, redacted = true) private fun RoomEvent.redact() = RoomEvent.Redacted(this.eventId, this.utcTimestamp, this.author)
private fun RoomState.replaceEvent(old: RoomEvent, new: RoomEvent): RoomState { private fun RoomState.replaceEvent(old: RoomEvent, new: RoomEvent): RoomState {
val updatedEvents = this.events.toMutableList().apply { val updatedEvents = this.events.toMutableList().apply {

View File

@ -83,6 +83,7 @@ internal class TimelineEventMapper(
is RoomEvent.Reply -> relationEvent.message is RoomEvent.Reply -> relationEvent.message
is RoomEvent.Image -> relationEvent is RoomEvent.Image -> relationEvent
is RoomEvent.Encrypted -> relationEvent is RoomEvent.Encrypted -> relationEvent
is RoomEvent.Redacted -> relationEvent
} }
) )
} }
@ -115,6 +116,7 @@ internal class TimelineEventMapper(
is RoomEvent.Message -> original.message.edited(incomingEdit) is RoomEvent.Message -> original.message.edited(incomingEdit)
is RoomEvent.Reply -> original.message is RoomEvent.Reply -> original.message
is RoomEvent.Encrypted -> original.message is RoomEvent.Encrypted -> original.message
is RoomEvent.Redacted -> original.message
} }
) )
@ -127,6 +129,11 @@ internal class TimelineEventMapper(
// can't edit encrypted messages // can't edit encrypted messages
null null
} }
is RoomEvent.Redacted -> {
// can't edit redacted
null
}
} }
} }
} }

View File

@ -79,4 +79,5 @@ private fun RoomEvent.toTextContent(): String = when (this) {
is RoomEvent.Message -> this.content.asString() is RoomEvent.Message -> this.content.asString()
is RoomEvent.Reply -> this.message.toTextContent() is RoomEvent.Reply -> this.message.toTextContent()
is RoomEvent.Encrypted -> "Encrypted message" is RoomEvent.Encrypted -> "Encrypted message"
is RoomEvent.Redacted -> "Message deleted"
} }

View File

@ -47,6 +47,7 @@ internal class UnreadEventsProcessor(
is RoomEvent.Reply -> it.message.author.id == selfId is RoomEvent.Reply -> it.message.author.id == selfId
is RoomEvent.Image -> it.author.id == selfId is RoomEvent.Image -> it.author.id == selfId
is RoomEvent.Encrypted -> it.author.id == selfId is RoomEvent.Encrypted -> it.author.id == selfId
is RoomEvent.Redacted -> it.author.id == selfId
} }
}.map { it.eventId } }.map { it.eventId }
roomStore.insertUnread(overview.roomId, eventsFromOthers) roomStore.insertUnread(overview.roomId, eventsFromOthers)