Browse Source

Add intentional mentions (#1843)

* Add intentional mentions
pull/1848/head
Jorge Martin Espinosa 10 months ago committed by GitHub
parent
commit
97e9528e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      changelog.d/1591.feature
  2. 16
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  3. 66
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
  4. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  5. 22
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/Mention.kt
  6. 26
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/Mention.kt
  7. 19
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  8. 17
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  9. 2
      libraries/textcomposer/impl/build.gradle.kts

1
changelog.d/1591.feature

@ -0,0 +1 @@ @@ -0,0 +1 @@
Add intentional mentions to messages.

16
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -47,6 +47,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags @@ -47,6 +47,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
@ -327,15 +328,25 @@ class MessageComposerPresenter @Inject constructor( @@ -327,15 +328,25 @@ class MessageComposerPresenter @Inject constructor(
richTextEditorState: RichTextEditorState,
) = launch {
val capturedMode = messageComposerContext.composerMode
val mentions = richTextEditorState.mentionsState?.let { state ->
buildList {
if (state.hasAtRoomMention) {
add(Mention.AtRoom)
}
for (userId in state.userIds) {
add(Mention.User(userId))
}
}
}.orEmpty()
// Reset composer right away
richTextEditorState.setHtml("")
updateComposerMode(MessageComposerMode.Normal)
when (capturedMode) {
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html)
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = mentions)
is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId
room.editMessage(eventId, transactionId, message.markdown, message.html)
room.editMessage(eventId, transactionId, message.markdown, message.html, mentions)
}
is MessageComposerMode.Quote -> TODO()
@ -343,6 +354,7 @@ class MessageComposerPresenter @Inject constructor( @@ -343,6 +354,7 @@ class MessageComposerPresenter @Inject constructor(
capturedMode.eventId,
message.markdown,
message.html,
mentions
)
}
analyticsService.capture(

66
features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt

@ -26,12 +26,12 @@ import app.cash.turbine.ReceiveTurbine @@ -26,12 +26,12 @@ import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo @@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
@ -79,10 +80,12 @@ import io.element.android.tests.testutils.waitForPredicate @@ -79,10 +80,12 @@ import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import okhttp3.internal.immutableListOf
import org.junit.Rule
import org.junit.Test
import uniffi.wysiwyg_composer.MentionsState
import java.io.File
@Suppress("LargeClass")
@ -835,6 +838,67 @@ class MessageComposerPresenterTest { @@ -835,6 +838,67 @@ class MessageComposerPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - send messages with intentional mentions`() = runTest {
val room = FakeMatrixRoom()
val presenter = createPresenter(room = room, coroutineScope = this)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
// Check intentional mentions on message sent
val mentionUser1 = listOf(A_USER_ID.value)
initialState.richTextEditorState.mentionsState = MentionsState(
userIds = mentionUser1,
roomIds = emptyList(),
roomAliases = emptyList(),
hasAtRoomMention = false
)
initialState.richTextEditorState.setHtml(A_MESSAGE)
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
advanceUntilIdle()
assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID.value)))
// Check intentional mentions on reply sent
initialState.eventSink(MessageComposerEvents.SetMode(aReplyMode()))
val mentionUser2 = listOf(A_USER_ID_2.value)
awaitItem().richTextEditorState.mentionsState = MentionsState(
userIds = mentionUser2,
roomIds = emptyList(),
roomAliases = emptyList(),
hasAtRoomMention = false
)
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
advanceUntilIdle()
assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID_2.value)))
// Check intentional mentions on edit message
skipItems(1)
initialState.eventSink(MessageComposerEvents.SetMode(anEditMode()))
val mentionUser3 = listOf(A_USER_ID_3.value)
awaitItem().richTextEditorState.mentionsState = MentionsState(
userIds = mentionUser3,
roomIds = emptyList(),
roomAliases = emptyList(),
hasAtRoomMention = false
)
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
advanceUntilIdle()
assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID_3.value)))
skipItems(1)
}
}
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount)

6
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt

@ -90,13 +90,13 @@ interface MatrixRoom : Closeable { @@ -90,13 +90,13 @@ interface MatrixRoom : Closeable {
suspend fun userAvatarUrl(userId: UserId): Result<String?>
suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit>
suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit>
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
suspend fun enterSpecialMode(eventId: EventId?): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit>
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>

22
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/Mention.kt

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.room
sealed interface Mention {
data class User(val userId: String): Mention
data object AtRoom: Mention
}

26
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/Mention.kt

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.Mention
import org.matrix.rustcomponents.sdk.Mentions
fun List<Mention>.map(): Mentions {
val hasAtRoom = any { it is Mention.AtRoom }
val userIds = filterIsInstance<Mention.User>().map { it.userId }
return Mentions(userIds, hasAtRoom)
}

19
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
@ -250,22 +251,28 @@ class RustMatrixRoom( @@ -250,22 +251,28 @@ class RustMatrixRoom(
}
}
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
messageEventContentFromParts(body, htmlBody).use { content ->
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(roomDispatcher) {
messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content ->
runCatching {
innerRoom.send(content)
}
}
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> =
override suspend fun editMessage(
originalEventId: EventId?,
transactionId: TransactionId?,
body: String,
htmlBody: String?,
mentions: List<Mention>,
): Result<Unit> =
withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
val editedEvent = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(originalEventId.value)
editedEvent.use {
innerRoom.edit(
newContent = messageEventContentFromParts(body, htmlBody),
newContent = messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()),
editItem = it,
)
}
@ -289,11 +296,11 @@ class RustMatrixRoom( @@ -289,11 +296,11 @@ class RustMatrixRoom(
}
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = withContext(roomDispatcher) {
runCatching {
val inReplyTo = specialModeEventTimelineItem ?: innerRoom.getEventTimelineItemByEventId(eventId.value)
inReplyTo.use { eventTimelineItem ->
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventTimelineItem)
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()), eventTimelineItem)
}
specialModeEventTimelineItem = null
}

17
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@ -108,6 +109,7 @@ class FakeMatrixRoom( @@ -108,6 +109,7 @@ class FakeMatrixRoom(
private var generateWidgetWebViewUrlResult = Result.success("https://call.element.io")
private var getWidgetDriverResult: Result<MatrixWidgetDriver> = Result.success(FakeWidgetDriver())
private var canUserTriggerRoomNotificationResult: Result<Boolean> = Result.success(true)
var sendMessageMentions = emptyList<Mention>()
val editMessageCalls = mutableListOf<Pair<String, String?>>()
var sendMediaCount = 0
@ -190,7 +192,8 @@ class FakeMatrixRoom( @@ -190,7 +192,8 @@ class FakeMatrixRoom(
userAvatarUrlResult
}
override suspend fun sendMessage(body: String, htmlBody: String?) = simulateLongTask {
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>) = simulateLongTask {
sendMessageMentions = mentions
Result.success(Unit)
}
@ -219,7 +222,14 @@ class FakeMatrixRoom( @@ -219,7 +222,14 @@ class FakeMatrixRoom(
return cancelSendResult
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> {
override suspend fun editMessage(
originalEventId: EventId?,
transactionId: TransactionId?,
body: String,
htmlBody: String?,
mentions: List<Mention>
): Result<Unit> {
sendMessageMentions = mentions
editMessageCalls += body to htmlBody
return Result.success(Unit)
}
@ -231,7 +241,8 @@ class FakeMatrixRoom( @@ -231,7 +241,8 @@ class FakeMatrixRoom(
return Result.success(Unit)
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> {
sendMessageMentions = mentions
replyMessageParameter = body to htmlBody
return Result.success(Unit)
}

2
libraries/textcomposer/impl/build.gradle.kts

@ -34,7 +34,7 @@ dependencies { @@ -34,7 +34,7 @@ dependencies {
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiUtils)
implementation(libs.matrix.richtexteditor)
api(libs.matrix.richtexteditor)
api(libs.matrix.richtexteditor.compose)
ksp(libs.showkase.processor)

Loading…
Cancel
Save