Browse Source

Move PermalinkBuilder, MatrixToConverter and PermalinkParser content to the `impl` project in order to remove projects.appconfig dependency from matrix.api module.

pull/2639/head
Benoit Marty 6 months ago committed by Benoit Marty
parent
commit
1e5a61a49c
  1. 28
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
  2. 5
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
  3. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  4. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt
  5. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt
  6. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
  7. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt
  8. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  9. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt
  10. 40
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  11. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
  12. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
  13. 11
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
  14. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
  15. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt
  16. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt
  17. 4
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  18. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt
  19. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt
  20. 15
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  21. 9
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt
  22. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
  23. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
  24. 35
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt
  25. 7
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
  26. 3
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
  27. 3
      libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt
  28. 4
      libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
  29. 4
      libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt
  30. 3
      libraries/matrix/api/build.gradle.kts
  31. 28
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt
  32. 65
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt
  33. 117
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt
  34. 3
      libraries/matrix/impl/build.gradle.kts
  35. 61
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt
  36. 92
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt
  37. 158
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt
  38. 16
      libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt
  39. 22
      libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilderTest.kt
  40. 67
      libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt
  41. 37
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkBuilder.kt
  42. 37
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt
  43. 1
      libraries/matrixui/build.gradle.kts
  44. 15
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt
  45. 16
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt
  46. 37
      libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt
  47. 9
      libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt
  48. 4
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
  49. 2
      libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt
  50. 12
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  51. 36
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt
  52. 23
      libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderTest.kt

28
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt

@ -16,9 +16,12 @@ @@ -16,9 +16,12 @@
package io.element.android.features.messages.impl
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@ -31,11 +34,14 @@ import io.element.android.features.messages.impl.attachments.Attachment @@ -31,11 +34,14 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer
@ -52,6 +58,7 @@ class MessagesNode @AssistedInject constructor( @@ -52,6 +58,7 @@ class MessagesNode @AssistedInject constructor(
presenterFactory: MessagesPresenter.Factory,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser,
) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(this)
private val callback = plugins<Callback>().firstOrNull()
@ -96,6 +103,25 @@ class MessagesNode @AssistedInject constructor( @@ -96,6 +103,25 @@ class MessagesNode @AssistedInject constructor(
private fun onUserDataClicked(userId: UserId) {
callback?.onUserDataClicked(userId)
}
private fun onLinkClicked(
context: Context,
url: String,
) {
when (val permalink = permalinkParser.parse(Uri.parse(url))) {
is PermalinkData.UserLink -> {
onUserDataClicked(UserId(permalink.userId))
}
is PermalinkData.RoomLink -> {
// TODO: Implement room link handling
}
is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> {
context.openUrlInExternalApp(url)
}
}
}
override fun onShowEventDebugInfoClicked(eventId: EventId?, debugInfo: TimelineItemDebugInfo) {
callback?.onShowEventDebugInfoClicked(eventId, debugInfo)
}
@ -126,6 +152,7 @@ class MessagesNode @AssistedInject constructor( @@ -126,6 +152,7 @@ class MessagesNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) {
@ -137,6 +164,7 @@ class MessagesNode @AssistedInject constructor( @@ -137,6 +164,7 @@ class MessagesNode @AssistedInject constructor(
onEventClicked = this::onEventClicked,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClicked = this::onUserDataClicked,
onLinkClicked = { onLinkClicked(context, it) },
onSendLocationClicked = this::onSendLocationClicked,
onCreatePollClicked = this::onCreatePollClicked,
onJoinCallClicked = this::onJoinCallClicked,

5
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

@ -119,6 +119,7 @@ fun MessagesView( @@ -119,6 +119,7 @@ fun MessagesView(
onRoomDetailsClicked: () -> Unit,
onEventClicked: (event: TimelineItem.Event) -> Boolean,
onUserDataClicked: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
@ -213,6 +214,7 @@ fun MessagesView( @@ -213,6 +214,7 @@ fun MessagesView(
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked,
onUserDataClicked = onUserDataClicked,
onLinkClicked = onLinkClicked,
onTimestampClicked = { event ->
if (event.localSendState is LocalEventSendState.SendingFailed) {
state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event))
@ -313,6 +315,7 @@ private fun MessagesViewContent( @@ -313,6 +315,7 @@ private fun MessagesViewContent(
state: MessagesState,
onMessageClicked: (TimelineItem.Event) -> Unit,
onUserDataClicked: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onReactionClicked: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClicked: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClicked: (TimelineItem.Event) -> Unit,
@ -386,6 +389,7 @@ private fun MessagesViewContent( @@ -386,6 +389,7 @@ private fun MessagesViewContent(
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
onUserDataClicked = onUserDataClicked,
onLinkClicked = onLinkClicked,
onTimestampClicked = onTimestampClicked,
onReactionClicked = onReactionClicked,
onReactionLongClicked = onReactionLongClicked,
@ -570,6 +574,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) @@ -570,6 +574,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onEventClicked = { false },
onPreviewAttachments = {},
onUserDataClicked = {},
onLinkClicked = {},
onSendLocationClicked = {},
onCreatePollClicked = {},
onJoinCallClicked = {},

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

@ -49,6 +49,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags @@ -49,6 +49,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.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
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
@ -96,6 +97,8 @@ class MessageComposerPresenter @Inject constructor( @@ -96,6 +97,8 @@ class MessageComposerPresenter @Inject constructor(
private val messageComposerContext: MessageComposerContextImpl,
private val richTextEditorStateFactory: RichTextEditorStateFactory,
private val currentSessionIdHolder: CurrentSessionIdHolder,
private val permalinkParser: PermalinkParser,
private val permalinkBuilder: PermalinkBuilder,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<MessageComposerState> {
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
@ -334,7 +337,7 @@ class MessageComposerPresenter @Inject constructor( @@ -334,7 +337,7 @@ class MessageComposerPresenter @Inject constructor(
}
is MentionSuggestion.Member -> {
val text = mention.roomMember.displayName?.prependIndent("@") ?: mention.roomMember.userId.value
val link = PermalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
val link = permalinkBuilder.permalinkForUser(mention.roomMember.userId).getOrNull() ?: return@launch
richTextEditorState.insertMentionAtSuggestion(text = text, link = link)
}
}
@ -345,6 +348,7 @@ class MessageComposerPresenter @Inject constructor( @@ -345,6 +348,7 @@ class MessageComposerPresenter @Inject constructor(
return MessageComposerState(
richTextEditorState = richTextEditorState,
permalinkParser = permalinkParser,
isFullScreen = isFullScreen.value,
mode = messageComposerContext.composerMode,
showAttachmentSourcePicker = showAttachmentSourcePicker,

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt

@ -21,6 +21,7 @@ import androidx.compose.runtime.Stable @@ -21,6 +21,7 @@ import androidx.compose.runtime.Stable
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.ImmutableList
@ -28,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList @@ -28,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList
@Stable
data class MessageComposerState(
val richTextEditorState: RichTextEditorState,
val permalinkParser: PermalinkParser,
val isFullScreen: Boolean,
val mode: MessageComposerMode,
val showAttachmentSourcePicker: Boolean,

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt

@ -16,9 +16,12 @@ @@ -16,9 +16,12 @@
package io.element.android.features.messages.impl.messagecomposer
import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.ImmutableList
@ -43,6 +46,10 @@ fun aMessageComposerState( @@ -43,6 +46,10 @@ fun aMessageComposerState(
memberSuggestions: ImmutableList<MentionSuggestion> = persistentListOf(),
) = MessageComposerState(
richTextEditorState = richTextEditorState,
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData = TODO()
override fun parse(uri: Uri): PermalinkData = TODO()
},
isFullScreen = isFullScreen,
mode = mode,
showTextFormatting = showTextFormatting,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt

@ -109,6 +109,7 @@ internal fun MessageComposerView( @@ -109,6 +109,7 @@ internal fun MessageComposerView(
modifier = modifier,
state = state.richTextEditorState,
voiceMessageState = voiceMessageState.voiceMessageState,
permalinkParser = state.permalinkParser,
subcomposing = subcomposing,
onRequestFocus = ::onRequestFocus,
onSendMessage = ::sendMessage,

10
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProvider.kt

@ -28,6 +28,7 @@ import io.element.android.libraries.core.bool.orFalse @@ -28,6 +28,7 @@ import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
import io.element.android.wysiwyg.compose.StyledHtmlConverter
@ -39,7 +40,9 @@ import javax.inject.Inject @@ -39,7 +40,9 @@ import javax.inject.Inject
@ContributesBinding(SessionScope::class)
@SingleIn(SessionScope::class)
class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider {
class DefaultHtmlConverterProvider @Inject constructor(
private val permalinkParser: PermalinkParser,
) : HtmlConverterProvider {
private val htmlConverter: MutableState<HtmlConverter?> = mutableStateOf(null)
@Composable
@ -50,7 +53,10 @@ class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider @@ -50,7 +53,10 @@ class DefaultHtmlConverterProvider @Inject constructor() : HtmlConverterProvider
}
val editorStyle = ElementRichTextEditorStyle.textStyle()
val mentionSpanProvider = rememberMentionSpanProvider(currentUserId = currentUserId)
val mentionSpanProvider = rememberMentionSpanProvider(
currentUserId = currentUserId,
permalinkParser = permalinkParser,
)
val context = LocalContext.current

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

@ -81,6 +81,7 @@ fun TimelineView( @@ -81,6 +81,7 @@ fun TimelineView(
typingNotificationState: TypingNotificationState,
roomName: String?,
onUserDataClicked: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onMessageClicked: (TimelineItem.Event) -> Unit,
onMessageLongClicked: (TimelineItem.Event) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
@ -140,6 +141,7 @@ fun TimelineView( @@ -140,6 +141,7 @@ fun TimelineView(
onClick = onMessageClicked,
onLongClick = onMessageLongClicked,
onUserDataClick = onUserDataClicked,
onLinkClicked = onLinkClicked,
inReplyToClick = ::inReplyToClicked,
onReactionClick = onReactionClicked,
onReactionLongClick = onReactionLongClicked,
@ -276,6 +278,7 @@ internal fun TimelineViewPreview( @@ -276,6 +278,7 @@ internal fun TimelineViewPreview(
onMessageClicked = {},
onTimestampClicked = {},
onUserDataClicked = {},
onLinkClicked = {},
onMessageLongClicked = {},
onReactionClicked = { _, _ -> },
onReactionLongClicked = { _, _ -> },

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt

@ -38,6 +38,7 @@ internal fun ATimelineItemEventRow( @@ -38,6 +38,7 @@ internal fun ATimelineItemEventRow(
onClick = {},
onLongClick = {},
onUserDataClick = {},
onLinkClicked = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },

40
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

@ -17,7 +17,6 @@ @@ -17,7 +17,6 @@
package io.element.android.features.messages.impl.timeline.components
import android.annotation.SuppressLint
import android.net.Uri
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -50,7 +49,6 @@ import androidx.compose.ui.Modifier @@ -50,7 +49,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.res.stringResource
@ -94,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI @@ -94,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
import io.element.android.features.messages.impl.timeline.model.metadata
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -109,9 +106,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon @@ -109,9 +106,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
@ -128,6 +122,7 @@ fun TimelineItemEventRow( @@ -128,6 +122,7 @@ fun TimelineItemEventRow(
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onLinkClicked: (String) -> Unit,
onUserDataClick: (UserId) -> Unit,
inReplyToClick: (EventId) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
@ -151,13 +146,6 @@ fun TimelineItemEventRow( @@ -151,13 +146,6 @@ fun TimelineItemEventRow(
inReplyToClick(inReplyToEventId)
}
fun onMentionClicked(mention: Mention) {
when (mention) {
is Mention.User -> onUserDataClick(mention.userId)
else -> Unit // TODO implement actions for other mentions being clicked
}
}
Column(modifier = modifier.fillMaxWidth()) {
if (event.groupPosition.isNew()) {
Spacer(modifier = Modifier.height(16.dp))
@ -203,7 +191,7 @@ fun TimelineItemEventRow( @@ -203,7 +191,7 @@ fun TimelineItemEventRow(
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
onMentionClicked = ::onMentionClicked,
onLinkClicked = onLinkClicked,
eventSink = eventSink,
)
}
@ -222,7 +210,7 @@ fun TimelineItemEventRow( @@ -222,7 +210,7 @@ fun TimelineItemEventRow(
onReactionClicked = { emoji -> onReactionClick(emoji, event) },
onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) },
onMoreReactionsClicked = { onMoreReactionsClick(event) },
onMentionClicked = ::onMentionClicked,
onLinkClicked = onLinkClicked,
eventSink = eventSink,
)
}
@ -278,7 +266,7 @@ private fun TimelineItemEventRowContent( @@ -278,7 +266,7 @@ private fun TimelineItemEventRowContent(
onReactionClicked: (emoji: String) -> Unit,
onReactionLongClicked: (emoji: String) -> Unit,
onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit,
onMentionClicked: (Mention) -> Unit,
onLinkClicked: (String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier,
) {
@ -346,7 +334,7 @@ private fun TimelineItemEventRowContent( @@ -346,7 +334,7 @@ private fun TimelineItemEventRowContent(
onTimestampClicked = {
onTimestampClicked(event)
},
onMentionClicked = onMentionClicked,
onLinkClicked = onLinkClicked,
eventSink = eventSink,
)
}
@ -429,7 +417,7 @@ private fun MessageEventBubbleContent( @@ -429,7 +417,7 @@ private fun MessageEventBubbleContent(
@Suppress("UNUSED_PARAMETER")
inReplyToClick: () -> Unit,
onTimestampClicked: () -> Unit,
onMentionClicked: (Mention) -> Unit,
onLinkClicked: (String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
@SuppressLint("ModifierParameter")
// need to rename this modifier to prevent linter false positives
@ -530,7 +518,6 @@ private fun MessageEventBubbleContent( @@ -530,7 +518,6 @@ private fun MessageEventBubbleContent(
modifier: Modifier = Modifier,
canShrinkContent: Boolean = false,
) {
val context = LocalContext.current
val timestampLayoutModifier: Modifier
val contentModifier: Modifier
when {
@ -566,20 +553,7 @@ private fun MessageEventBubbleContent( @@ -566,20 +553,7 @@ private fun MessageEventBubbleContent(
) { onContentLayoutChanged ->
TimelineItemEventContentView(
content = event.content,
onLinkClicked = { url ->
when (val permalink = PermalinkParser.parse(Uri.parse(url))) {
is PermalinkData.UserLink -> {
onMentionClicked(Mention.User(UserId(permalink.userId)))
}
is PermalinkData.RoomLink -> {
onMentionClicked(Mention.Room(permalink.getRoomId(), permalink.getRoomAlias()))
}
is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> {
context.openUrlInExternalApp(url)
}
}
},
onLinkClicked = onLinkClicked,
eventSink = eventSink,
onContentLayoutChanged = onContentLayoutChanged,
modifier = contentModifier

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt

@ -51,6 +51,7 @@ fun TimelineItemGroupedEventsRow( @@ -51,6 +51,7 @@ fun TimelineItemGroupedEventsRow(
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
@ -78,6 +79,7 @@ fun TimelineItemGroupedEventsRow( @@ -78,6 +79,7 @@ fun TimelineItemGroupedEventsRow(
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked,
onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
@ -102,6 +104,7 @@ private fun TimelineItemGroupedEventsRowContent( @@ -102,6 +104,7 @@ private fun TimelineItemGroupedEventsRowContent(
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onTimestampClicked: (TimelineItem.Event) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
@ -135,6 +138,7 @@ private fun TimelineItemGroupedEventsRowContent( @@ -135,6 +138,7 @@ private fun TimelineItemGroupedEventsRowContent(
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked,
onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
@ -175,6 +179,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi @@ -175,6 +179,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
onLongClick = {},
inReplyToClick = {},
onUserDataClick = {},
onLinkClicked = {},
onTimestampClicked = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },
@ -200,6 +205,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi @@ -200,6 +205,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
onLongClick = {},
inReplyToClick = {},
onUserDataClick = {},
onLinkClicked = {},
onTimestampClicked = {},
onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> },

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt

@ -36,6 +36,7 @@ internal fun TimelineItemRow( @@ -36,6 +36,7 @@ internal fun TimelineItemRow(
highlightedItem: String?,
sessionState: SessionState,
onUserDataClick: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
@ -79,6 +80,7 @@ internal fun TimelineItemRow( @@ -79,6 +80,7 @@ internal fun TimelineItemRow(
onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
@ -103,6 +105,7 @@ internal fun TimelineItemRow( @@ -103,6 +105,7 @@ internal fun TimelineItemRow(
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked,
onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,

11
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt

@ -41,6 +41,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes @@ -41,6 +41,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
@ -67,6 +68,7 @@ class TimelineItemContentMessageFactory @Inject constructor( @@ -67,6 +68,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
private val fileExtensionExtractor: FileExtensionExtractor,
private val featureFlagService: FeatureFlagService,
private val htmlConverterProvider: HtmlConverterProvider,
private val permalinkParser: PermalinkParser,
) {
suspend fun create(content: MessageContent, senderDisplayName: String, eventId: EventId?): TimelineItemEventContent {
return when (val messageType = content.type) {
@ -74,7 +76,10 @@ class TimelineItemContentMessageFactory @Inject constructor( @@ -74,7 +76,10 @@ class TimelineItemContentMessageFactory @Inject constructor(
val emoteBody = "* $senderDisplayName ${messageType.body.trimEnd()}"
TimelineItemEmoteContent(
body = emoteBody,
htmlDocument = messageType.formatted?.toHtmlDocument(prefix = "* $senderDisplayName"),
htmlDocument = messageType.formatted?.toHtmlDocument(
permalinkParser = permalinkParser,
prefix = "* $senderDisplayName",
),
formattedBody = parseHtml(messageType.formatted, prefix = "* $senderDisplayName") ?: emoteBody.withLinks(),
isEdited = content.isEdited,
)
@ -197,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor( @@ -197,7 +202,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
val body = messageType.body.trimEnd()
TimelineItemNoticeContent(
body = body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser),
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
isEdited = content.isEdited,
)
@ -206,7 +211,7 @@ class TimelineItemContentMessageFactory @Inject constructor( @@ -206,7 +211,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
val body = messageType.body.trimEnd()
TimelineItemTextContent(
body = body,
htmlDocument = messageType.formatted?.toHtmlDocument(),
htmlDocument = messageType.formatted?.toHtmlDocument(permalinkParser = permalinkParser),
formattedBody = parseHtml(messageType.formatted) ?: body.withLinks(),
isEdited = content.isEdited,
)

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt

@ -30,6 +30,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat @@ -30,6 +30,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
@ -43,6 +44,7 @@ class TimelineItemEventFactory @Inject constructor( @@ -43,6 +44,7 @@ class TimelineItemEventFactory @Inject constructor(
private val contentFactory: TimelineItemContentFactory,
private val matrixClient: MatrixClient,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val permalinkParser: PermalinkParser,
) {
suspend fun create(
currentTimelineItem: MatrixTimelineItem.Event,
@ -80,7 +82,7 @@ class TimelineItemEventFactory @Inject constructor( @@ -80,7 +82,7 @@ class TimelineItemEventFactory @Inject constructor(
reactionsState = currentTimelineItem.computeReactionsState(),
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
localSendState = currentTimelineItem.event.localSendState,
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(),
inReplyTo = currentTimelineItem.event.inReplyTo()?.map(permalinkParser = permalinkParser),
isThreaded = currentTimelineItem.event.isThreaded(),
debugInfo = currentTimelineItem.event.debugInfo,
origin = currentTimelineItem.event.origin,

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt

@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
@ -34,7 +35,9 @@ data class InReplyToDetails( @@ -34,7 +35,9 @@ data class InReplyToDetails(
val textContent: String?,
)
fun InReplyTo.map() = when (this) {
fun InReplyTo.map(
permalinkParser: PermalinkParser,
) = when (this) {
is InReplyTo.Ready -> InReplyToDetails(
eventId = eventId,
senderId = senderId,
@ -44,7 +47,7 @@ fun InReplyTo.map() = when (this) { @@ -44,7 +47,7 @@ fun InReplyTo.map() = when (this) {
textContent = when (content) {
is MessageContent -> {
val messageContent = content as MessageContent
(messageContent.type as? TextMessageType)?.toPlainText() ?: messageContent.body
(messageContent.type as? TextMessageType)?.toPlainText(permalinkParser = permalinkParser) ?: messageContent.body
}
is StickerContent -> {
val stickerContent = content as StickerContent

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt

@ -35,6 +35,7 @@ internal fun MessagesViewWithTypingPreview( @@ -35,6 +35,7 @@ internal fun MessagesViewWithTypingPreview(
onEventClicked = { false },
onPreviewAttachments = {},
onUserDataClicked = {},
onLinkClicked = {},
onSendLocationClicked = {},
onCreatePollClicked = {},
onJoinCallClicked = {},

4
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

@ -77,6 +77,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 @@ -77,6 +77,8 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember
@ -725,6 +727,8 @@ class MessagesPresenterTest { @@ -725,6 +727,8 @@ class MessagesPresenterTest {
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
permissionsPresenterFactory = permissionsPresenterFactory,
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
permalinkParser = FakePermalinkParser(),
permalinkBuilder = FakePermalinkBuilder(),
)
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
this,

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt

@ -478,6 +478,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa @@ -478,6 +478,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onRoomDetailsClicked: () -> Unit = EnsureNeverCalled(),
onEventClicked: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
onUserDataClicked: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClicked: (String) -> Unit = EnsureNeverCalledWithParam(),
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit = EnsureNeverCalledWithParam(),
onSendLocationClicked: () -> Unit = EnsureNeverCalled(),
onCreatePollClicked: () -> Unit = EnsureNeverCalled(),
@ -492,6 +493,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa @@ -492,6 +493,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onRoomDetailsClicked = onRoomDetailsClicked,
onEventClicked = onEventClicked,
onUserDataClicked = onUserDataClicked,
onLinkClicked = onLinkClicked,
onPreviewAttachments = onPreviewAttachments,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,

3
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt

@ -41,6 +41,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter @@ -41,6 +41,7 @@ import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
@ -57,6 +58,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { @@ -57,6 +58,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
featureFlagService = FakeFeatureFlagService(),
htmlConverterProvider = FakeHtmlConverterProvider(),
permalinkParser = FakePermalinkParser(),
),
redactedMessageFactory = TimelineItemContentRedactedFactory(),
stickerFactory = TimelineItemContentStickerFactory(
@ -73,6 +75,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { @@ -73,6 +75,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
),
matrixClient = matrixClient,
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
permalinkParser = FakePermalinkParser(),
),
virtualItemFactory = TimelineItemVirtualFactory(
daySeparatorFactory = TimelineItemDaySeparatorFactory(

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

@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.EventId @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
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.permalink.PermalinkBuilder
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
@ -60,6 +61,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID_3 @@ -60,6 +61,8 @@ import io.element.android.libraries.matrix.test.A_USER_ID_3
import io.element.android.libraries.matrix.test.A_USER_ID_4
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.mediapickers.api.PickerProvider
@ -805,7 +808,14 @@ class MessageComposerPresenterTest { @@ -805,7 +808,14 @@ class MessageComposerPresenterTest {
@Test
fun `present - insertMention`() = runTest {
val presenter = createPresenter(this)
val presenter = createPresenter(
coroutineScope = this,
permalinkBuilder = FakePermalinkBuilder(
result = {
Result.success("https://matrix.to/#/${A_USER_ID_2.value}")
}
)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -941,6 +951,7 @@ class MessageComposerPresenterTest { @@ -941,6 +951,7 @@ class MessageComposerPresenterTest {
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder()
) = MessageComposerPresenter(
coroutineScope,
room,
@ -955,6 +966,8 @@ class MessageComposerPresenterTest { @@ -955,6 +966,8 @@ class MessageComposerPresenterTest {
TestRichTextEditorStateFactory(),
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
permalinkParser = FakePermalinkParser(),
permalinkBuilder = permalinkBuilder,
)
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {

9
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/DefaultHtmlConverterProviderTest.kt

@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalInspectionMode @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.createComposeRule
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -32,7 +33,9 @@ class DefaultHtmlConverterProviderTest { @@ -32,7 +33,9 @@ class DefaultHtmlConverterProviderTest {
@Test
fun `calling provide without calling Update first should throw an exception`() {
val provider = DefaultHtmlConverterProvider()
val provider = DefaultHtmlConverterProvider(
permalinkParser = FakePermalinkParser(),
)
val exception = runCatching { provider.provide() }.exceptionOrNull()
@ -41,7 +44,9 @@ class DefaultHtmlConverterProviderTest { @@ -41,7 +44,9 @@ class DefaultHtmlConverterProviderTest {
@Test
fun `calling provide after calling Update first should return an HtmlConverter`() {
val provider = DefaultHtmlConverterProvider()
val provider = DefaultHtmlConverterProvider(
permalinkParser = FakePermalinkParser(),
)
composeTestRule.setContent {
CompositionLocalProvider(LocalInspectionMode provides true) {
provider.Update(currentUserId = A_USER_ID)

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt

@ -45,6 +45,7 @@ class TimelineViewTest { @@ -45,6 +45,7 @@ class TimelineViewTest {
typingNotificationState = aTypingNotificationState(),
roomName = null,
onUserDataClicked = EnsureNeverCalledWithParam(),
onLinkClicked = EnsureNeverCalledWithParam(),
onMessageClicked = EnsureNeverCalledWithParam(),
onMessageLongClicked = EnsureNeverCalledWithParam(),
onTimestampClicked = EnsureNeverCalledWithParam(),
@ -72,6 +73,7 @@ class TimelineViewTest { @@ -72,6 +73,7 @@ class TimelineViewTest {
typingNotificationState = aTypingNotificationState(),
roomName = null,
onUserDataClicked = EnsureNeverCalledWithParam(),
onLinkClicked = EnsureNeverCalledWithParam(),
onMessageClicked = EnsureNeverCalledWithParam(),
onMessageLongClicked = EnsureNeverCalledWithParam(),
onTimestampClicked = EnsureNeverCalledWithParam(),

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt

@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy @@ -63,6 +63,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import kotlinx.collections.immutable.persistentListOf
@ -664,6 +665,7 @@ class TimelineItemContentMessageFactoryTest { @@ -664,6 +665,7 @@ class TimelineItemContentMessageFactoryTest {
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
featureFlagService = featureFlagService,
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
permalinkParser = FakePermalinkParser(),
)
private fun createStickerContent(

35
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetailTest.kt

@ -26,14 +26,27 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi @@ -26,14 +26,27 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershi
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import org.junit.Test
class InReplyToDetailTest {
@Test
fun `map - with a not ready InReplyTo does not work`() {
assertThat(InReplyTo.Pending.map()).isNull()
assertThat(InReplyTo.NotLoaded(AN_EVENT_ID).map()).isNull()
assertThat(InReplyTo.Error.map()).isNull()
assertThat(
InReplyTo.Pending.map(
permalinkParser = FakePermalinkParser()
)
).isNull()
assertThat(
InReplyTo.NotLoaded(AN_EVENT_ID).map(
permalinkParser = FakePermalinkParser()
)
).isNull()
assertThat(
InReplyTo.Error.map(
permalinkParser = FakePermalinkParser()
)
).isNull()
}
@Test
@ -48,7 +61,9 @@ class InReplyToDetailTest { @@ -48,7 +61,9 @@ class InReplyToDetailTest {
change = MembershipChange.INVITED,
)
)
val inReplyToDetails = inReplyTo.map()
val inReplyToDetails = inReplyTo.map(
permalinkParser = FakePermalinkParser()
)
assertThat(inReplyToDetails).isNotNull()
assertThat(inReplyToDetails?.textContent).isNull()
}
@ -74,7 +89,11 @@ class InReplyToDetailTest { @@ -74,7 +89,11 @@ class InReplyToDetailTest {
)
)
)
assertThat(inReplyTo.map()?.textContent).isEqualTo("Hello!")
assertThat(
inReplyTo.map(
permalinkParser = FakePermalinkParser()
)?.textContent
).isEqualTo("Hello!")
}
@Test
@ -95,6 +114,10 @@ class InReplyToDetailTest { @@ -95,6 +114,10 @@ class InReplyToDetailTest {
)
)
)
assertThat(inReplyTo.map()?.textContent).isEqualTo("**Hello!**")
assertThat(
inReplyTo.map(
permalinkParser = FakePermalinkParser()
)?.textContent
).isEqualTo("**Hello!**")
}
}

7
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt

@ -45,6 +45,7 @@ class RoomDetailsNode @AssistedInject constructor( @@ -45,6 +45,7 @@ class RoomDetailsNode @AssistedInject constructor(
private val presenter: RoomDetailsPresenter,
private val room: MatrixRoom,
private val analyticsService: AnalyticsService,
private val permalinkBuilder: PermalinkBuilder,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun openRoomMemberList()
@ -84,8 +85,8 @@ class RoomDetailsNode @AssistedInject constructor( @@ -84,8 +85,8 @@ class RoomDetailsNode @AssistedInject constructor(
private fun onShareRoom(context: Context) {
val alias = room.alias ?: room.alternativeAliases.firstOrNull()
val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) }
?: PermalinkBuilder.permalinkForRoomId(room.roomId)
val permalinkResult = alias?.let { permalinkBuilder.permalinkForRoomAlias(it) }
?: permalinkBuilder.permalinkForRoomId(room.roomId)
permalinkResult.onSuccess { permalink ->
context.startSharePlainTextIntent(
activityResultLauncher = null,
@ -99,7 +100,7 @@ class RoomDetailsNode @AssistedInject constructor( @@ -99,7 +100,7 @@ class RoomDetailsNode @AssistedInject constructor(
}
private fun onShareMember(context: Context, member: RoomMember) {
val permalinkResult = PermalinkBuilder.permalinkForUser(member.userId)
val permalinkResult = permalinkBuilder.permalinkForUser(member.userId)
permalinkResult.onSuccess { permalink ->
context.startSharePlainTextIntent(
activityResultLauncher = null,

3
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt

@ -46,6 +46,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( @@ -46,6 +46,7 @@ class RoomMemberDetailsNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val analyticsService: AnalyticsService,
private val permalinkBuilder: PermalinkBuilder,
presenterFactory: RoomMemberDetailsPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
interface Callback : NodeInputs {
@ -74,7 +75,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( @@ -74,7 +75,7 @@ class RoomMemberDetailsNode @AssistedInject constructor(
val context = LocalContext.current
fun onShareUser() {
val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.roomMemberId)
val permalinkResult = permalinkBuilder.permalinkForUser(inputs.roomMemberId)
permalinkResult.onSuccess { permalink ->
context.startSharePlainTextIntent(
activityResultLauncher = null,

3
libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt

@ -31,9 +31,10 @@ class InviteFriendsUseCase @Inject constructor( @@ -31,9 +31,10 @@ class InviteFriendsUseCase @Inject constructor(
private val stringProvider: StringProvider,
private val matrixClient: MatrixClient,
private val buildMeta: BuildMeta,
private val permalinkBuilder: PermalinkBuilder,
) {
fun execute(activity: Activity) {
val permalinkResult = PermalinkBuilder.permalinkForUser(matrixClient.sessionId)
val permalinkResult = permalinkBuilder.permalinkForUser(matrixClient.sessionId)
permalinkResult.fold(
onSuccess = { permalink ->
val appName = buildMeta.applicationName

4
libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt

@ -25,6 +25,7 @@ import com.squareup.anvil.annotations.ContributesBinding @@ -25,6 +25,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.impl.mode.RenderingMode
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
@ -62,6 +63,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( @@ -62,6 +63,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
private val roomMembershipContentFormatter: RoomMembershipContentFormatter,
private val profileChangeContentFormatter: ProfileChangeContentFormatter,
private val stateContentFormatter: StateContentFormatter,
private val permalinkParser: PermalinkParser
) : RoomLastMessageFormatter {
companion object {
// Max characters to display in the last message. This works around https://github.com/element-hq/element-x-android/issues/2105
@ -121,7 +123,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( @@ -121,7 +123,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
return "* $senderDisplayName ${messageType.body}"
}
is TextMessageType -> {
messageType.toPlainText()
messageType.toPlainText(permalinkParser)
}
is VideoMessageType -> {
sp.getString(CommonStrings.common_video)

4
libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt

@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT @@ -51,6 +51,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
@ -78,7 +79,8 @@ class DefaultRoomLastMessageFormatterTest { @@ -78,7 +79,8 @@ class DefaultRoomLastMessageFormatterTest {
sp = AndroidStringProvider(context.resources),
roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider),
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
stateContentFormatter = StateContentFormatter(stringProvider)
stateContentFormatter = StateContentFormatter(stringProvider),
permalinkParser = FakePermalinkParser(),
)
}

3
libraries/matrix/api/build.gradle.kts

@ -34,7 +34,6 @@ anvil { @@ -34,7 +34,6 @@ anvil {
}
dependencies {
implementation(projects.appconfig)
implementation(projects.libraries.di)
implementation(libs.dagger)
implementation(projects.libraries.androidutils)
@ -45,7 +44,5 @@ dependencies { @@ -45,7 +44,5 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(projects.tests.testutils)
testImplementation(projects.libraries.matrix.test)
}

28
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt

@ -17,40 +17,18 @@ @@ -17,40 +17,18 @@
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
import io.element.android.appconfig.MatrixConfiguration
/**
* Mapping of an input URI to a matrix.to compliant URI.
*/
object MatrixToConverter {
interface MatrixToConverter {
/**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url.
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS].
* To be successfully converted, URL path should contain one of the [DefaultMatrixToConverter.SUPPORTED_PATHS].
* Examples:
* - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
*/
fun convert(uri: Uri): Uri? {
val uriString = uri.toString()
val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL
return when {
// URL is already a matrix.to
uriString.startsWith(baseUrl) -> uri
// Web or client url
SUPPORTED_PATHS.any { it in uriString } -> {
val path = SUPPORTED_PATHS.first { it in uriString }
Uri.parse(baseUrl + uriString.substringAfter(path))
}
// URL is not supported
else -> null
}
}
private val SUPPORTED_PATHS = listOf(
"/#/room/",
"/#/user/",
"/#/group/"
)
fun convert(uri: Uri): Uri?
}

65
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt

@ -16,70 +16,13 @@ @@ -16,70 +16,13 @@
package io.element.android.libraries.matrix.api.permalink
import io.element.android.appconfig.MatrixConfiguration
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
object PermalinkBuilder {
private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/"
private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL).also {
var baseUrl = it
if (!baseUrl.endsWith("/")) {
baseUrl += "/"
}
if (!baseUrl.endsWith("/#/")) {
baseUrl += "/#/"
}
}
fun permalinkForUser(userId: UserId): Result<String> {
return if (MatrixPatterns.isUserId(userId.value)) {
val url = buildString {
append(permalinkBaseUrl)
if (!isMatrixTo()) {
append(USER_PATH)
}
append(userId.value)
}
Result.success(url)
} else {
Result.failure(PermalinkBuilderError.InvalidUserId)
}
}
fun permalinkForRoomAlias(roomAlias: String): Result<String> {
return if (MatrixPatterns.isRoomAlias(roomAlias)) {
Result.success(permalinkForRoomAliasOrId(roomAlias))
} else {
Result.failure(PermalinkBuilderError.InvalidRoomAlias)
}
}
fun permalinkForRoomId(roomId: RoomId): Result<String> {
return if (MatrixPatterns.isRoomId(roomId.value)) {
Result.success(permalinkForRoomAliasOrId(roomId.value))
} else {
Result.failure(PermalinkBuilderError.InvalidRoomId)
}
}
private fun permalinkForRoomAliasOrId(value: String): String {
val id = escapeId(value)
return buildString {
append(permalinkBaseUrl)
if (!isMatrixTo()) {
append(ROOM_PATH)
}
append(id)
}
}
private fun escapeId(value: String) = value.replace("/", "%2F")
private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL)
interface PermalinkBuilder {
fun permalinkForUser(userId: UserId): Result<String>
fun permalinkForRoomAlias(roomAlias: String): Result<String>
fun permalinkForRoomId(roomId: RoomId): Result<String>
}
sealed class PermalinkBuilderError : Throwable() {

117
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt

@ -17,11 +17,6 @@ @@ -17,11 +17,6 @@
package io.element.android.libraries.matrix.api.permalink
import android.net.Uri
import android.net.UrlQuerySanitizer
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
import java.net.URLDecoder
/**
* This class turns a uri to a [PermalinkData].
@ -29,121 +24,15 @@ import java.net.URLDecoder @@ -29,121 +24,15 @@ import java.net.URLDecoder
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org)
*/
object PermalinkParser {
interface PermalinkParser {
/**
* Turns a uri string to a [PermalinkData].
*/
fun parse(uriString: String): PermalinkData {
val uri = Uri.parse(uriString)
return parse(uri)
}
fun parse(uriString: String): PermalinkData
/**
* Turns a uri to a [PermalinkData].
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
fun parse(uri: Uri): PermalinkData {
// the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid
// so convert URI to matrix.to to simplify parsing process
val matrixToUri = MatrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri)
// We can't use uri.fragment as it is decoding to early and it will break the parsing
// of parameters that represents url (like signurl)
val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment
if (fragment.isEmpty()) {
return PermalinkData.FallbackLink(uri)
}
val safeFragment = fragment.substringBefore('?')
val viaQueryParameters = fragment.getViaParameters()
// we are limiting to 2 params
val params = safeFragment
.split(MatrixPatterns.SEP_REGEX)
.filter { it.isNotEmpty() }
.take(2)
val decodedParams = params
.map { URLDecoder.decode(it, "UTF-8") }
val identifier = params.getOrNull(0)
val decodedIdentifier = decodedParams.getOrNull(0)
val extraParameter = decodedParams.getOrNull(1)
return when {
identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier)
MatrixPatterns.isRoomId(decodedIdentifier) -> {
handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters)
}
MatrixPatterns.isRoomAlias(decodedIdentifier) -> {
PermalinkData.RoomLink(
roomIdOrAlias = decodedIdentifier,
isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters.toImmutableList()
)
}
else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier))
}
}
private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List<String>): PermalinkData {
// Can't rely on built in parsing because it's messing around the signurl
val paramList = safeExtractParams(fragment)
val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second
val email = paramList.firstOrNull { it.first == "email" }?.second
return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) {
try {
val signValidUri = Uri.parse(signUrl)
val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException("missing `authority`")
val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException("missing `token`")
val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException("missing `private_key`")
PermalinkData.RoomEmailInviteLink(
roomId = identifier,
email = email!!,
signUrl = signUrl!!,
roomName = paramList.firstOrNull { it.first == "room_name" }?.second,
inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second,
roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second,
roomType = paramList.firstOrNull { it.first == "room_type" }?.second,
identityServer = identityServerHost,
token = token,
privateKey = privateKey
)
} catch (failure: Throwable) {
Timber.i("## Permalink: Failed to parse permalink $signUrl")
PermalinkData.FallbackLink(uri)
}
} else {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters.toImmutableList()
)
}
}
private fun safeExtractParams(fragment: String) =
fragment.substringAfter("?").split('&').mapNotNull {
val splitNameValue = it.split("=")
if (splitNameValue.size == 2) {
Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8"))
} else {
null
}
}
private fun String.getViaParameters(): List<String> {
return runCatching {
UrlQuerySanitizer(this)
.parameterList
.filter {
it.mParameter == "via"
}
.map {
URLDecoder.decode(it.mValue, "UTF-8")
}
}.getOrDefault(emptyList())
}
fun parse(uri: Uri): PermalinkData
}

3
libraries/matrix/impl/build.gradle.kts

@ -36,6 +36,7 @@ dependencies { @@ -36,6 +36,7 @@ dependencies {
} else {
debugImplementation(libs.matrix.sdk)
}
implementation(projects.appconfig)
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.network)
@ -52,8 +53,10 @@ dependencies { @@ -52,8 +53,10 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.coroutines.test)
testImplementation(libs.test.turbine)
}

61
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverter.kt

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
/*
* Copyright (c) 2022 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.permalink
import android.net.Uri
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.MatrixConfiguration
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.permalink.MatrixToConverter
import javax.inject.Inject
/**
* Mapping of an input URI to a matrix.to compliant URI.
*/
@ContributesBinding(AppScope::class)
class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter {
/**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url.
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS].
* Examples:
* - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
*/
override fun convert(uri: Uri): Uri? {
val uriString = uri.toString()
val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL
return when {
// URL is already a matrix.to
uriString.startsWith(baseUrl) -> uri
// Web or client url
SUPPORTED_PATHS.any { it in uriString } -> {
val path = SUPPORTED_PATHS.first { it in uriString }
Uri.parse(baseUrl + uriString.substringAfter(path))
}
// URL is not supported
else -> null
}
}
private val SUPPORTED_PATHS = listOf(
"/#/room/",
"/#/user/",
"/#/group/"
)
}

92
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/*
* 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.permalink
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.MatrixConfiguration
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilderError
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder {
private val permalinkBaseUrl
get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL).also {
var baseUrl = it
if (!baseUrl.endsWith("/")) {
baseUrl += "/"
}
if (!baseUrl.endsWith("/#/")) {
baseUrl += "/#/"
}
}
override fun permalinkForUser(userId: UserId): Result<String> {
return if (MatrixPatterns.isUserId(userId.value)) {
val url = buildString {
append(permalinkBaseUrl)
if (!isMatrixTo()) {
append(USER_PATH)
}
append(userId.value)
}
Result.success(url)
} else {
Result.failure(PermalinkBuilderError.InvalidUserId)
}
}
override fun permalinkForRoomAlias(roomAlias: String): Result<String> {
return if (MatrixPatterns.isRoomAlias(roomAlias)) {
Result.success(permalinkForRoomAliasOrId(roomAlias))
} else {
Result.failure(PermalinkBuilderError.InvalidRoomAlias)
}
}
override fun permalinkForRoomId(roomId: RoomId): Result<String> {
return if (MatrixPatterns.isRoomId(roomId.value)) {
Result.success(permalinkForRoomAliasOrId(roomId.value))
} else {
Result.failure(PermalinkBuilderError.InvalidRoomId)
}
}
private fun permalinkForRoomAliasOrId(value: String): String {
val id = escapeId(value)
return buildString {
append(permalinkBaseUrl)
if (!isMatrixTo()) {
append(ROOM_PATH)
}
append(id)
}
}
private fun escapeId(value: String) = value.replace("/", "%2F")
private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL)
companion object {
private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/"
}
}

158
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
/*
* Copyright (c) 2024 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.permalink
import android.net.Uri
import android.net.UrlQuerySanitizer
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.MatrixPatterns
import io.element.android.libraries.matrix.api.permalink.MatrixToConverter
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
import java.net.URLDecoder
import javax.inject.Inject
/**
* This class turns a uri to a [PermalinkData].
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org)
*/
@ContributesBinding(AppScope::class)
class DefaultPermalinkParser @Inject constructor(
private val matrixToConverter: MatrixToConverter
) : PermalinkParser {
/**
* Turns a uri string to a [PermalinkData].
*/
override fun parse(uriString: String): PermalinkData {
val uri = Uri.parse(uriString)
return parse(uri)
}
/**
* Turns a uri to a [PermalinkData].
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
override fun parse(uri: Uri): PermalinkData {
// the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid
// so convert URI to matrix.to to simplify parsing process
val matrixToUri = matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri)
// We can't use uri.fragment as it is decoding to early and it will break the parsing
// of parameters that represents url (like signurl)
val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment
if (fragment.isEmpty()) {
return PermalinkData.FallbackLink(uri)
}
val safeFragment = fragment.substringBefore('?')
val viaQueryParameters = fragment.getViaParameters()
// we are limiting to 2 params
val params = safeFragment
.split(MatrixPatterns.SEP_REGEX)
.filter { it.isNotEmpty() }
.take(2)
val decodedParams = params
.map { URLDecoder.decode(it, "UTF-8") }
val identifier = params.getOrNull(0)
val decodedIdentifier = decodedParams.getOrNull(0)
val extraParameter = decodedParams.getOrNull(1)
return when {
identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier)
MatrixPatterns.isRoomId(decodedIdentifier) -> {
handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters)
}
MatrixPatterns.isRoomAlias(decodedIdentifier) -> {
PermalinkData.RoomLink(
roomIdOrAlias = decodedIdentifier,
isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters.toImmutableList()
)
}
else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier))
}
}
private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List<String>): PermalinkData {
// Can't rely on built in parsing because it's messing around the signurl
val paramList = safeExtractParams(fragment)
val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second
val email = paramList.firstOrNull { it.first == "email" }?.second
return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) {
try {
val signValidUri = Uri.parse(signUrl)
val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException("missing `authority`")
val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException("missing `token`")
val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException("missing `private_key`")
PermalinkData.RoomEmailInviteLink(
roomId = identifier,
email = email!!,
signUrl = signUrl!!,
roomName = paramList.firstOrNull { it.first == "room_name" }?.second,
inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second,
roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second,
roomType = paramList.firstOrNull { it.first == "room_type" }?.second,
identityServer = identityServerHost,
token = token,
privateKey = privateKey
)
} catch (failure: Throwable) {
Timber.i("## Permalink: Failed to parse permalink $signUrl")
PermalinkData.FallbackLink(uri)
}
} else {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters.toImmutableList()
)
}
}
private fun safeExtractParams(fragment: String) =
fragment.substringAfter("?").split('&').mapNotNull {
val splitNameValue = it.split("=")
if (splitNameValue.size == 2) {
Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8"))
} else {
null
}
}
private fun String.getViaParameters(): List<String> {
return runCatching {
UrlQuerySanitizer(this)
.parameterList
.filter {
it.mParameter == "via"
}
.map {
URLDecoder.decode(it.mValue, "UTF-8")
}
}.getOrDefault(emptyList())
}
}

16
libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverterTest.kt → libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultMatrixToConverterTest.kt

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 New Vector Ltd
* Copyright (c) 2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.permalink
package io.element.android.libraries.matrix.impl.permalink
import android.net.Uri
import com.google.common.truth.Truth.assertThat
@ -23,34 +23,34 @@ import org.junit.runner.RunWith @@ -23,34 +23,34 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class MatrixToConverterTest {
class DefaultMatrixToConverterTest {
@Test
fun `converting a matrix-to url does nothing`() {
val url = Uri.parse("https://matrix.to/#/#element-android:matrix.org")
assertThat(MatrixToConverter.convert(url)).isEqualTo(url)
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(url)
}
@Test
fun `converting a url with a supported room path returns a matrix-to url`() {
val url = Uri.parse("https://riot.im/develop/#/room/#element-android:matrix.org")
assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org"))
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org"))
}
@Test
fun `converting a url with a supported user path returns a matrix-to url`() {
val url = Uri.parse("https://riot.im/develop/#/user/@test:matrix.org")
assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@test:matrix.org"))
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@test:matrix.org"))
}
@Test
fun `converting a url with a supported group path returns a matrix-to url`() {
val url = Uri.parse("https://riot.im/develop/#/group/+group:matrix.org")
assertThat(MatrixToConverter.convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/+group:matrix.org"))
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/+group:matrix.org"))
}
@Test
fun `converting an unsupported url returns null`() {
val url = Uri.parse("https://element.io/")
assertThat(MatrixToConverter.convert(url)).isNull()
assertThat(DefaultMatrixToConverter().convert(url)).isNull()
}
}

22
libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilderTest.kt → libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilderTest.kt

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 New Vector Ltd
* Copyright (c) 2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.permalink
package io.element.android.libraries.matrix.impl.permalink
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.metadata.withReleaseBehavior
@ -23,18 +23,18 @@ import io.element.android.libraries.matrix.api.core.UserId @@ -23,18 +23,18 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.tests.testutils.assertThrowsInDebug
import org.junit.Test
class PermalinkBuilderTest {
class DefaultPermalinkBuilderTest {
fun `building a permalink for an invalid user id throws when verifying the id`() {
assertThrowsInDebug {
val userId = UserId("some invalid user id")
PermalinkBuilder.permalinkForUser(userId)
DefaultPermalinkBuilder().permalinkForUser(userId)
}
}
fun `building a permalink for an invalid room id throws when verifying the id`() {
assertThrowsInDebug {
val roomId = RoomId("some invalid room id")
PermalinkBuilder.permalinkForRoomId(roomId)
DefaultPermalinkBuilder().permalinkForRoomId(roomId)
}
}
@ -42,7 +42,7 @@ class PermalinkBuilderTest { @@ -42,7 +42,7 @@ class PermalinkBuilderTest {
fun `building a permalink for an invalid user id returns failure when not verifying the id`() {
withReleaseBehavior {
val userId = UserId("some invalid user id")
assertThat(PermalinkBuilder.permalinkForUser(userId).isFailure).isTrue()
assertThat(DefaultPermalinkBuilder().permalinkForUser(userId).isFailure).isTrue()
}
}
@ -50,31 +50,31 @@ class PermalinkBuilderTest { @@ -50,31 +50,31 @@ class PermalinkBuilderTest {
fun `building a permalink for an invalid room id returns failure when not verifying the id`() {
withReleaseBehavior {
val roomId = RoomId("some invalid room id")
assertThat(PermalinkBuilder.permalinkForRoomId(roomId).isFailure).isTrue()
assertThat(DefaultPermalinkBuilder().permalinkForRoomId(roomId).isFailure).isTrue()
}
}
@Test
fun `building a permalink for an invalid room alias returns failure`() {
val roomAlias = "an invalid room alias"
assertThat(PermalinkBuilder.permalinkForRoomAlias(roomAlias).isFailure).isTrue()
assertThat(DefaultPermalinkBuilder().permalinkForRoomAlias(roomAlias).isFailure).isTrue()
}
@Test
fun `building a permalink for a valid user id returns a matrix-to url`() {
val userId = UserId("@user:matrix.org")
assertThat(PermalinkBuilder.permalinkForUser(userId).getOrNull()).isEqualTo("https://matrix.to/#/@user:matrix.org")
assertThat(DefaultPermalinkBuilder().permalinkForUser(userId).getOrNull()).isEqualTo("https://matrix.to/#/@user:matrix.org")
}
@Test
fun `building a permalink for a valid room id returns a matrix-to url`() {
val roomId = RoomId("!aBCdEFG1234:matrix.org")
assertThat(PermalinkBuilder.permalinkForRoomId(roomId).getOrNull()).isEqualTo("https://matrix.to/#/!aBCdEFG1234:matrix.org")
assertThat(DefaultPermalinkBuilder().permalinkForRoomId(roomId).getOrNull()).isEqualTo("https://matrix.to/#/!aBCdEFG1234:matrix.org")
}
@Test
fun `building a permalink for a valid room alias returns a matrix-to url`() {
val roomAlias = "#room:matrix.org"
assertThat(PermalinkBuilder.permalinkForRoomAlias(roomAlias).getOrNull()).isEqualTo("https://matrix.to/#/#room:matrix.org")
assertThat(DefaultPermalinkBuilder().permalinkForRoomAlias(roomAlias).getOrNull()).isEqualTo("https://matrix.to/#/#room:matrix.org")
}
}

67
libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParserTest.kt → libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 New Vector Ltd
* Copyright (c) 2024 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.
@ -14,44 +14,60 @@ @@ -14,44 +14,60 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.permalink
package io.element.android.libraries.matrix.impl.permalink
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class PermalinkParserTest {
class DefaultPermalinkParserTest {
@Test
fun `parsing an invalid url returns a fallback link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://element.io"
assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
}
@Test
fun `parsing an invalid url with the right path but no content returns a fallback link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/user"
assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
}
@Test
fun `parsing an invalid url with the right path but empty content returns a fallback link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/user/"
assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
}
@Test
fun `parsing an invalid url with the right path but invalid content returns a fallback link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/user/some%20user!"
assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
}
@Test
fun `parsing a valid user url returns a user link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/user/@test:matrix.org"
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.UserLink(
userId = "@test:matrix.org"
)
@ -60,8 +76,11 @@ class PermalinkParserTest { @@ -60,8 +76,11 @@ class PermalinkParserTest {
@Test
fun `parsing a valid room id url returns a room link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org"
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
@ -73,8 +92,11 @@ class PermalinkParserTest { @@ -73,8 +92,11 @@ class PermalinkParserTest {
@Test
fun `parsing a valid room id with event id url returns a room link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org"
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
@ -86,8 +108,11 @@ class PermalinkParserTest { @@ -86,8 +108,11 @@ class PermalinkParserTest {
@Test
fun `parsing a valid room id with and invalid event id url returns a room link with no event id`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/1234567890abcdef:matrix.org"
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
@ -99,8 +124,11 @@ class PermalinkParserTest { @@ -99,8 +124,11 @@ class PermalinkParserTest {
@Test
fun `parsing a valid room id with event id and via parameters url returns a room link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org?via=matrix.org&via=matrix.com"
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
@ -112,8 +140,11 @@ class PermalinkParserTest { @@ -112,8 +140,11 @@ class PermalinkParserTest {
@Test
fun `parsing a valid room alias url returns a room link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/#element-android:matrix.org"
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "#element-android:matrix.org",
isRoomAlias = true,
@ -125,6 +156,9 @@ class PermalinkParserTest { @@ -125,6 +156,9 @@ class PermalinkParserTest {
@Test
fun `parsing a url with an invalid signurl returns a fallback link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
// This url has no private key
val url = "https://app.element.io/#/room/%21aBCDEF12345%3Amatrix.org" +
"?email=testuser%40element.io" +
@ -135,11 +169,14 @@ class PermalinkParserTest { @@ -135,11 +169,14 @@ class PermalinkParserTest {
"&guest_access_token=" +
"&guest_user_id=" +
"&room_type="
assertThat(PermalinkParser.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
assertThat(sut.parse(url)).isInstanceOf(PermalinkData.FallbackLink::class.java)
}
@Test
fun `parsing a url with signurl returns a room email invite link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/%21aBCDEF12345%3Amatrix.org" +
"?email=testuser%40element.io" +
"&signurl=https%3A%2F%2Fvector.im%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3Da_token%26private_key%3Da_private_key" +
@ -149,7 +186,7 @@ class PermalinkParserTest { @@ -149,7 +186,7 @@ class PermalinkParserTest {
"&guest_access_token=" +
"&guest_user_id=" +
"&room_type="
assertThat(PermalinkParser.parse(url)).isEqualTo(
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomEmailInviteLink(
roomId = "!aBCDEF12345:matrix.org",
email = "testuser@element.io",

37
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkBuilder.kt

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 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.test.permalink
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
class FakePermalinkBuilder(
private val result: () -> Result<String> = { Result.failure(Exception("Not implemented")) }
) : PermalinkBuilder {
override fun permalinkForUser(userId: UserId): Result<String> {
return result()
}
override fun permalinkForRoomAlias(roomAlias: String): Result<String> {
return result()
}
override fun permalinkForRoomId(roomId: RoomId): Result<String> {
return result()
}
}

37
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
/*
* Copyright (c) 2024 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.test.permalink
import android.net.Uri
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
class FakePermalinkParser(
private var result: () -> PermalinkData = { throw Exception("Not implemented") }
) : PermalinkParser {
fun givenResult(result: PermalinkData) {
this.result = { result }
}
override fun parse(uriString: String): PermalinkData {
return result()
}
override fun parse(uri: Uri): PermalinkData {
TODO("Not yet implemented")
}
}

1
libraries/matrixui/build.gradle.kts

@ -47,4 +47,5 @@ dependencies { @@ -47,4 +47,5 @@ dependencies {
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
}

15
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToHtmlDocument.kt

@ -29,9 +29,13 @@ import org.jsoup.nodes.Document @@ -29,9 +29,13 @@ import org.jsoup.nodes.Document
*
* This will also make sure mentions are prefixed with `@`.
*
* @param permalinkParser the parser to use to parse the mentions.
* @param prefix if not null, the prefix will be inserted at the beginning of the message.
*/
fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? {
fun FormattedBody.toHtmlDocument(
permalinkParser: PermalinkParser,
prefix: String? = null,
): Document? {
return takeIf { it.format == MessageFormat.HTML }?.body
// Trim whitespace at the end to avoid having wrong rendering of the message.
// We don't trim the start in case it's used as indentation.
@ -44,17 +48,20 @@ fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? { @@ -44,17 +48,20 @@ fun FormattedBody.toHtmlDocument(prefix: String? = null): Document? {
}
// Prepend `@` to mentions
fixMentions(dom)
fixMentions(dom, permalinkParser)
dom
}
}
private fun fixMentions(dom: Document) {
private fun fixMentions(
dom: Document,
permalinkParser: PermalinkParser,
) {
val links = dom.getElementsByTag("a")
links.forEach {
if (it.hasAttr("href")) {
val link = PermalinkParser.parse(it.attr("href"))
val link = permalinkParser.parse(it.attr("href"))
if (link is PermalinkData.UserLink && !it.text().startsWith("@")) {
it.prependText("@")
}

16
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/ToPlainText.kt

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.ui.messages
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
@ -29,15 +30,24 @@ import org.jsoup.select.NodeVisitor @@ -29,15 +30,24 @@ import org.jsoup.select.NodeVisitor
* Converts the HTML string in [TextMessageType.formatted] to a plain text representation by parsing it and removing all formatting.
* If the message is not formatted or the format is not [MessageFormat.HTML], the [TextMessageType.body] is returned instead.
*/
fun TextMessageType.toPlainText() = formatted?.toPlainText() ?: body
fun TextMessageType.toPlainText(
permalinkParser: PermalinkParser,
) = formatted?.toPlainText(permalinkParser) ?: body
/**
* Converts the HTML string in [FormattedBody.body] to a plain text representation by parsing it and removing all formatting.
* If the message is not formatted or the format is not [MessageFormat.HTML] we return `null`.
* @param permalinkParser the parser to use to parse the mentions.
* @param prefix if not null, the prefix will be inserted at the beginning of the message.
*/
fun FormattedBody.toPlainText(prefix: String? = null): String? {
return this.toHtmlDocument(prefix)?.toPlainText()
fun FormattedBody.toPlainText(
permalinkParser: PermalinkParser,
prefix: String? = null,
): String? {
return this.toHtmlDocument(
permalinkParser = permalinkParser,
prefix = prefix,
)?.toPlainText()
}
/**

37
libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToHtmlDocumentTest.kt

@ -16,9 +16,13 @@ @@ -16,9 +16,13 @@
package io.element.android.libraries.matrixui.messages
import android.net.Uri
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.ui.messages.toHtmlDocument
import org.junit.Test
import org.junit.runner.RunWith
@ -33,7 +37,7 @@ class ToHtmlDocumentTest { @@ -33,7 +37,7 @@ class ToHtmlDocumentTest {
body = "Hello world"
)
val document = body.toHtmlDocument()
val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser())
assertThat(document).isNull()
}
@ -45,7 +49,7 @@ class ToHtmlDocumentTest { @@ -45,7 +49,7 @@ class ToHtmlDocumentTest {
body = "<p>Hello world</p>"
)
val document = body.toHtmlDocument()
val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser())
assertThat(document).isNotNull()
assertThat(document?.text()).isEqualTo("Hello world")
}
@ -57,7 +61,10 @@ class ToHtmlDocumentTest { @@ -57,7 +61,10 @@ class ToHtmlDocumentTest {
body = "<p>Hello world</p>"
)
val document = body.toHtmlDocument(prefix = "@Jorge:")
val document = body.toHtmlDocument(
permalinkParser = FakePermalinkParser(),
prefix = "@Jorge:"
)
assertThat(document).isNotNull()
assertThat(document?.text()).isEqualTo("@Jorge: Hello world")
}
@ -69,7 +76,13 @@ class ToHtmlDocumentTest { @@ -69,7 +76,13 @@ class ToHtmlDocumentTest {
body = "Hey <a href='https://matrix.to/#/@alice:matrix.org'>Alice</a>!"
)
val document = body.toHtmlDocument()
val document = body.toHtmlDocument(permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return PermalinkData.UserLink("@alice:matrix.org")
}
override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented")
})
assertThat(document?.text()).isEqualTo("Hey @Alice!")
}
@ -80,7 +93,13 @@ class ToHtmlDocumentTest { @@ -80,7 +93,13 @@ class ToHtmlDocumentTest {
body = "Hey <a href='https://matrix.to/#/@alice:matrix.org'>@Alice</a>!"
)
val document = body.toHtmlDocument()
val document = body.toHtmlDocument(permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return PermalinkData.UserLink("@alice:matrix.org")
}
override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented")
})
assertThat(document?.text()).isEqualTo("Hey @Alice!")
}
@ -91,7 +110,13 @@ class ToHtmlDocumentTest { @@ -91,7 +110,13 @@ class ToHtmlDocumentTest {
body = "Hey <a href='https://matrix.org'>Alice</a>!"
)
val document = body.toHtmlDocument()
val document = body.toHtmlDocument(permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return PermalinkData.FallbackLink(uri = Uri.parse("https://matrix.org"))
}
override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented")
})
assertThat(document?.text()).isEqualTo("Hey Alice!")
}
}

9
libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrixui/messages/ToPlainTextTest.kt

@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.ui.messages.toPlainText
import org.jsoup.Jsoup
import org.junit.Test
@ -59,7 +60,7 @@ class ToPlainTextTest { @@ -59,7 +60,7 @@ class ToPlainTextTest {
<br />
""".trimIndent()
)
assertThat(formattedBody.toPlainText()).isEqualTo(
assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo(
"""
Hello world
This is an unordered list.
@ -79,7 +80,7 @@ class ToPlainTextTest { @@ -79,7 +80,7 @@ class ToPlainTextTest {
<br />
""".trimIndent()
)
assertThat(formattedBody.toPlainText()).isNull()
assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isNull()
}
@Test
@ -96,7 +97,7 @@ class ToPlainTextTest { @@ -96,7 +97,7 @@ class ToPlainTextTest {
""".trimIndent()
)
)
assertThat(messageType.toPlainText()).isEqualTo(
assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo(
"""
Hello world
This is an unordered list.
@ -119,6 +120,6 @@ class ToPlainTextTest { @@ -119,6 +120,6 @@ class ToPlainTextTest {
""".trimIndent()
)
)
assertThat(messageType.toPlainText()).isEqualTo("This is the fallback text")
assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the fallback text")
}
}

4
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
@ -68,6 +69,7 @@ class NotifiableEventResolver @Inject constructor( @@ -68,6 +69,7 @@ class NotifiableEventResolver @Inject constructor(
private val matrixClientProvider: MatrixClientProvider,
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
@ApplicationContext private val context: Context,
private val permalinkParser: PermalinkParser,
) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session
@ -252,7 +254,7 @@ class NotifiableEventResolver @Inject constructor( @@ -252,7 +254,7 @@ class NotifiableEventResolver @Inject constructor(
is ImageMessageType -> messageType.body
is StickerMessageType -> messageType.body
is NoticeMessageType -> messageType.body
is TextMessageType -> messageType.toPlainText()
is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser)
is VideoMessageType -> messageType.body
is LocationMessageType -> messageType.body
is OtherMessageType -> messageType.body

2
libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt

@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2 @@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@ -549,6 +550,7 @@ class NotifiableEventResolverTest { @@ -549,6 +550,7 @@ class NotifiableEventResolverTest {
matrixClientProvider = matrixClientProvider,
notificationMediaRepoFactory = notificationMediaRepoFactory,
context = context,
permalinkParser = FakePermalinkParser(),
)
}

12
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

@ -63,6 +63,8 @@ import io.element.android.libraries.matrix.api.core.EventId @@ -63,6 +63,8 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
@ -98,6 +100,7 @@ import kotlin.time.Duration.Companion.seconds @@ -98,6 +100,7 @@ import kotlin.time.Duration.Companion.seconds
fun TextComposer(
state: RichTextEditorState,
voiceMessageState: VoiceMessageState,
permalinkParser: PermalinkParser,
composerMode: MessageComposerMode,
enableTextFormatting: Boolean,
enableVoiceMessages: Boolean,
@ -152,7 +155,10 @@ fun TextComposer( @@ -152,7 +155,10 @@ fun TextComposer(
val textInput: @Composable () -> Unit = remember(state, subcomposing, composerMode, onResetComposerMode, onError) {
@Composable {
val mentionSpanProvider = rememberMentionSpanProvider(currentUserId)
val mentionSpanProvider = rememberMentionSpanProvider(
currentUserId = currentUserId,
permalinkParser = permalinkParser,
)
TextInput(
state = state,
subcomposing = subcomposing,
@ -907,6 +913,10 @@ private fun ATextComposer( @@ -907,6 +913,10 @@ private fun ATextComposer(
state = richTextEditorState,
showTextFormatting = showTextFormatting,
voiceMessageState = voiceMessageState,
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData = TODO("Not yet implemented")
override fun parse(uri: Uri): PermalinkData = TODO("Not yet implemented")
},
composerMode = composerMode,
enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages,

36
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt

@ -18,6 +18,7 @@ package io.element.android.libraries.textcomposer.mentions @@ -18,6 +18,7 @@ package io.element.android.libraries.textcomposer.mentions
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.layout.PaddingValues
@ -42,10 +43,12 @@ import io.element.android.libraries.designsystem.theme.mentionPillText @@ -42,10 +43,12 @@ import io.element.android.libraries.designsystem.theme.mentionPillText
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentListOf
@Stable
class MentionSpanProvider(
private val currentSessionId: SessionId,
private val permalinkParser: PermalinkParser,
private var currentUserTextColor: Int = 0,
private var currentUserBackgroundColor: Int = Color.WHITE,
private var otherTextColor: Int = 0,
@ -73,7 +76,7 @@ class MentionSpanProvider( @@ -73,7 +76,7 @@ class MentionSpanProvider(
}
fun getMentionSpanFor(text: String, url: String): MentionSpan {
val permalinkData = PermalinkParser.parse(url)
val permalinkData = permalinkParser.parse(url)
val (startPaddingPx, endPaddingPx) = paddingValuesPx.value
return when {
permalinkData is PermalinkData.UserLink -> {
@ -112,9 +115,15 @@ class MentionSpanProvider( @@ -112,9 +115,15 @@ class MentionSpanProvider(
}
@Composable
fun rememberMentionSpanProvider(currentUserId: SessionId): MentionSpanProvider {
fun rememberMentionSpanProvider(
currentUserId: SessionId,
permalinkParser: PermalinkParser,
): MentionSpanProvider {
val provider = remember(currentUserId) {
MentionSpanProvider(currentUserId)
MentionSpanProvider(
currentSessionId = currentUserId,
permalinkParser = permalinkParser,
)
}
provider.setup()
return provider
@ -123,7 +132,26 @@ fun rememberMentionSpanProvider(currentUserId: SessionId): MentionSpanProvider { @@ -123,7 +132,26 @@ fun rememberMentionSpanProvider(currentUserId: SessionId): MentionSpanProvider {
@PreviewsDayNight
@Composable
internal fun MentionSpanPreview() {
val provider = rememberMentionSpanProvider(SessionId("@me:matrix.org"))
val provider = rememberMentionSpanProvider(
currentUserId = SessionId("@me:matrix.org"),
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return when (uriString) {
"https://matrix.to/#/@me:matrix.org" -> PermalinkData.UserLink("@me:matrix.org")
"https://matrix.to/#/@other:matrix.org" -> PermalinkData.UserLink("@other:matrix.org")
"https://matrix.to/#/#room:matrix.org" -> PermalinkData.RoomLink(
roomIdOrAlias = "#room:matrix.org",
isRoomAlias = true,
eventId = null,
viaParameters = persistentListOf(),
)
else -> TODO()
}
}
override fun parse(uri: Uri): PermalinkData = TODO()
},
)
ElementPreview {
provider.setup()

23
libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/impl/mentions/MentionSpanProviderTest.kt

@ -18,9 +18,12 @@ package io.element.android.libraries.textcomposer.impl.mentions @@ -18,9 +18,12 @@ package io.element.android.libraries.textcomposer.impl.mentions
import android.graphics.Color
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@ -35,8 +38,10 @@ class MentionSpanProviderTest { @@ -35,8 +38,10 @@ class MentionSpanProviderTest {
private val otherColor = Color.BLUE
private val currentUserId = A_SESSION_ID
private val permalinkParser = FakePermalinkParser()
private val mentionSpanProvider = MentionSpanProvider(
currentSessionId = currentUserId,
permalinkParser = permalinkParser,
currentUserBackgroundColor = myUserColor,
currentUserTextColor = myUserColor,
otherBackgroundColor = otherColor,
@ -45,6 +50,7 @@ class MentionSpanProviderTest { @@ -45,6 +50,7 @@ class MentionSpanProviderTest {
@Test
fun `getting mention span for current user should return a MentionSpan with custom colors`() {
permalinkParser.givenResult(PermalinkData.UserLink(currentUserId.value))
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@me:matrix.org", "https://matrix.to/#/${currentUserId.value}")
assertThat(mentionSpan.backgroundColor).isEqualTo(myUserColor)
assertThat(mentionSpan.textColor).isEqualTo(myUserColor)
@ -52,6 +58,7 @@ class MentionSpanProviderTest { @@ -52,6 +58,7 @@ class MentionSpanProviderTest {
@Test
fun `getting mention span for other user should return a MentionSpan with normal colors`() {
permalinkParser.givenResult(PermalinkData.UserLink("@other:matrix.org"))
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@other:matrix.org", "https://matrix.to/#/@other:matrix.org")
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
assertThat(mentionSpan.textColor).isEqualTo(otherColor)
@ -59,6 +66,14 @@ class MentionSpanProviderTest { @@ -59,6 +66,14 @@ class MentionSpanProviderTest {
@Test
fun `getting mention span for a room should return a MentionSpan with normal colors`() {
permalinkParser.givenResult(
PermalinkData.RoomLink(
roomIdOrAlias = "#room:matrix.org",
isRoomAlias = true,
eventId = null,
viaParameters = persistentListOf(),
)
)
val mentionSpan = mentionSpanProvider.getMentionSpanFor("#room:matrix.org", "https://matrix.to/#/#room:matrix.org")
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
assertThat(mentionSpan.textColor).isEqualTo(otherColor)
@ -66,6 +81,14 @@ class MentionSpanProviderTest { @@ -66,6 +81,14 @@ class MentionSpanProviderTest {
@Test
fun `getting mention span for @room should return a MentionSpan with normal colors`() {
permalinkParser.givenResult(
PermalinkData.RoomLink(
roomIdOrAlias = "#",
isRoomAlias = true,
eventId = null,
viaParameters = persistentListOf(),
)
)
val mentionSpan = mentionSpanProvider.getMentionSpanFor("@room", "#")
assertThat(mentionSpan.backgroundColor).isEqualTo(otherColor)
assertThat(mentionSpan.textColor).isEqualTo(otherColor)

Loading…
Cancel
Save