Merge pull request #248 from ouchadam/feature/clearer-redactions
Clearer redactions
This commit is contained in:
commit
a3dd5541e4
|
@ -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(
|
||||||
|
|
|
@ -28,8 +28,7 @@ fun anEncryptedRoomMessageEvent(
|
||||||
author: RoomMember = aRoomMember(),
|
author: RoomMember = aRoomMember(),
|
||||||
meta: MessageMeta = MessageMeta.FromServer,
|
meta: MessageMeta = MessageMeta.FromServer,
|
||||||
edited: Boolean = false,
|
edited: Boolean = false,
|
||||||
redacted: Boolean = false,
|
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited)
|
||||||
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited, redacted)
|
|
||||||
|
|
||||||
fun aRoomImageMessageEvent(
|
fun aRoomImageMessageEvent(
|
||||||
eventId: EventId = anEventId(),
|
eventId: EventId = anEventId(),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
|
@ -28,7 +28,6 @@ private const val READ_RECEIPTS_ARE_DISABLED = true
|
||||||
private val A_ROOM_ID = aRoomId("messenger state room id")
|
private val A_ROOM_ID = aRoomId("messenger state room id")
|
||||||
private const val A_MESSAGE_CONTENT = "message content"
|
private const val A_MESSAGE_CONTENT = "message content"
|
||||||
private val AN_EVENT_ID = anEventId("state event")
|
private val AN_EVENT_ID = anEventId("state event")
|
||||||
private const val ROOM_IS_MUTED = true
|
|
||||||
private val A_SELF_ID = aUserId("self")
|
private val A_SELF_ID = aUserId("self")
|
||||||
private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
private val A_MESSENGER_PAGE_STATE = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||||
private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image)
|
private val A_MESSAGE_ATTACHMENT = MessageAttachment(AndroidUri("a-uri"), MimeType.Image)
|
||||||
|
@ -314,6 +313,7 @@ class MessengerReducerTest {
|
||||||
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")
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +96,7 @@ internal class TimelineEventMapper(
|
||||||
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
|
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "redacted")
|
private suspend fun ApiTimelineEvent.TimelineMessage.toFallbackTextMessage() = this.toTextMessage(content = this.asTextContent().body ?: "")
|
||||||
|
|
||||||
private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(editedEventId: EventId, lookup: Lookup): RoomEvent? {
|
private suspend fun ApiTimelineEvent.TimelineMessage.handleEdit(editedEventId: EventId, lookup: Lookup): RoomEvent? {
|
||||||
return lookup(editedEventId).fold(
|
return lookup(editedEventId).fold(
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +148,7 @@ internal class TimelineEventMapper(
|
||||||
|
|
||||||
is ApiTimelineEvent.TimelineMessage.Content.Text -> original.toTextMessage(
|
is ApiTimelineEvent.TimelineMessage.Content.Text -> original.toTextMessage(
|
||||||
utcTimestamp = incomingEdit.utcTimestamp,
|
utcTimestamp = incomingEdit.utcTimestamp,
|
||||||
content = incomingEdit.asTextContent().let { it.formattedBody ?: it.body }?.removePrefix(" * ") ?: "redacted",
|
content = incomingEdit.asTextContent().let { it.formattedBody ?: it.body }?.removePrefix(" * ") ?: "",
|
||||||
edited = true,
|
edited = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -151,7 +158,7 @@ internal class TimelineEventMapper(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy(
|
private fun RoomEvent.Message.edited(edit: ApiTimelineEvent.TimelineMessage) = this.copy(
|
||||||
content = richMessageParser.parse(edit.asTextContent().let { it.formattedBody ?: it.body }?.removePrefix(" * ") ?: "redacted"),
|
content = richMessageParser.parse(edit.asTextContent().let { it.formattedBody ?: it.body }?.removePrefix(" * ") ?: ""),
|
||||||
edited = true,
|
edited = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -160,7 +167,7 @@ internal class TimelineEventMapper(
|
||||||
is ApiTimelineEvent.TimelineMessage.Content.Image -> source.toImageMessage(userCredentials, roomId)
|
is ApiTimelineEvent.TimelineMessage.Content.Image -> source.toImageMessage(userCredentials, roomId)
|
||||||
is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage(
|
is ApiTimelineEvent.TimelineMessage.Content.Text -> source.toTextMessage(
|
||||||
roomId,
|
roomId,
|
||||||
content = source.asTextContent().formattedBody ?: source.content.body ?: "redacted"
|
content = source.asTextContent().formattedBody ?: source.content.body ?: ""
|
||||||
)
|
)
|
||||||
|
|
||||||
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
|
ApiTimelineEvent.TimelineMessage.Content.Ignored -> throw IllegalStateException()
|
||||||
|
@ -168,7 +175,7 @@ internal class TimelineEventMapper(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage(
|
private suspend fun ApiTimelineEvent.TimelineMessage.toTextMessage(
|
||||||
content: String = this.asTextContent().formattedBody ?: this.asTextContent().body ?: "redacted",
|
content: String = this.asTextContent().formattedBody ?: this.asTextContent().body ?: "",
|
||||||
edited: Boolean = false,
|
edited: Boolean = false,
|
||||||
utcTimestamp: Long = this.utcTimestamp,
|
utcTimestamp: Long = this.utcTimestamp,
|
||||||
) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) }
|
) = with(roomEventFactory) { toTextMessage(roomId, content, edited, utcTimestamp) }
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -37,15 +37,20 @@ internal class PartBuilder {
|
||||||
|
|
||||||
fun build(): List<RichText.Part> {
|
fun build(): List<RichText.Part> {
|
||||||
flushNormalBuffer()
|
flushNormalBuffer()
|
||||||
val last = parts.last()
|
return when(parts.isEmpty()) {
|
||||||
if (last is RichText.Part.Normal) {
|
true -> parts
|
||||||
parts.removeLast()
|
else -> {
|
||||||
val newContent = last.content.trimEnd()
|
val last = parts.last()
|
||||||
if (newContent.isNotEmpty()) {
|
if (last is RichText.Part.Normal) {
|
||||||
parts.add(last.copy(content = newContent))
|
parts.removeLast()
|
||||||
|
val newContent = last.content.trimEnd()
|
||||||
|
if (newContent.isNotEmpty()) {
|
||||||
|
parts.add(last.copy(content = newContent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flushNormalBuffer() {
|
private fun flushNormalBuffer() {
|
||||||
|
|
|
@ -95,5 +95,4 @@ private fun RoomEvent.Encrypted.toText(text: String) = RoomEvent.Message(
|
||||||
this.author,
|
this.author,
|
||||||
this.meta,
|
this.meta,
|
||||||
this.edited,
|
this.edited,
|
||||||
this.redacted,
|
|
||||||
)
|
)
|
|
@ -88,7 +88,7 @@ internal class RoomEventCreatorTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given text event without body then maps to redacted room message`() = runTest {
|
fun `given text event without body then maps to empty room message`() = runTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
|
|
||||||
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||||
|
@ -96,7 +96,7 @@ internal class RoomEventCreatorTest {
|
||||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
||||||
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
|
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
|
||||||
content = RichText.of("redacted"),
|
content = RichText(emptyList()),
|
||||||
author = A_SENDER,
|
author = A_SENDER,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,7 @@ fun anEncryptedRoomMessageEvent(
|
||||||
meta: MessageMeta = MessageMeta.FromServer,
|
meta: MessageMeta = MessageMeta.FromServer,
|
||||||
encryptedContent: RoomEvent.Encrypted.MegOlmV1 = aMegolmV1(),
|
encryptedContent: RoomEvent.Encrypted.MegOlmV1 = aMegolmV1(),
|
||||||
edited: Boolean = false,
|
edited: Boolean = false,
|
||||||
redacted: Boolean = false,
|
) = RoomEvent.Encrypted(eventId, utcTimestamp, author, meta, edited, encryptedContent)
|
||||||
) = RoomEvent.Encrypted(eventId, utcTimestamp, author, meta, edited, redacted, encryptedContent)
|
|
||||||
|
|
||||||
fun aMegolmV1(
|
fun aMegolmV1(
|
||||||
cipherText: CipherText = CipherText("a-cipher"),
|
cipherText: CipherText = CipherText("a-cipher"),
|
||||||
|
|
Loading…
Reference in New Issue