Merge pull request #2639 from element-hq/feature/bma/hideCallMemberEvents
Hide call member events
This commit is contained in:
@@ -14,16 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
plugins {
|
||||
id("java-library")
|
||||
id("com.android.lint")
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
android {
|
||||
namespace = "io.element.android.appconfig"
|
||||
}
|
||||
|
||||
anvil {
|
||||
@@ -33,4 +30,5 @@ anvil {
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,29 @@
|
||||
|
||||
package io.element.android.appconfig
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
|
||||
object TimelineConfig {
|
||||
const val MAX_READ_RECEIPT_TO_DISPLAY = 3
|
||||
|
||||
/**
|
||||
* Event types that will be filtered out from the timeline (i.e. not displayed).
|
||||
*/
|
||||
val excludedEvents = listOf(
|
||||
StateEventType.CALL_MEMBER,
|
||||
StateEventType.ROOM_ALIASES,
|
||||
StateEventType.ROOM_CANONICAL_ALIAS,
|
||||
StateEventType.ROOM_GUEST_ACCESS,
|
||||
StateEventType.ROOM_HISTORY_VISIBILITY,
|
||||
StateEventType.ROOM_JOIN_RULES,
|
||||
StateEventType.ROOM_PINNED_EVENTS,
|
||||
StateEventType.ROOM_POWER_LEVELS,
|
||||
StateEventType.ROOM_SERVER_ACL,
|
||||
StateEventType.ROOM_TOMBSTONE,
|
||||
StateEventType.SPACE_CHILD,
|
||||
StateEventType.SPACE_PARENT,
|
||||
StateEventType.POLICY_RULE_ROOM,
|
||||
StateEventType.POLICY_RULE_SERVER,
|
||||
StateEventType.POLICY_RULE_USER,
|
||||
)
|
||||
}
|
||||
|
||||
1
changelog.d/2625.bugfix
Normal file
1
changelog.d/2625.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Hide Event org.matrix.msc3401.call.member on the timeline.
|
||||
@@ -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
|
||||
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(
|
||||
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(
|
||||
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 -> {
|
||||
callback?.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(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||
) {
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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(
|
||||
onMessageClicked = onMessageClicked,
|
||||
onMessageLongClicked = onMessageLongClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onReactionClicked = onReactionClicked,
|
||||
onReactionLongClicked = onReactionLongClicked,
|
||||
@@ -570,6 +574,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
||||
onEventClicked = { false },
|
||||
onPreviewAttachments = {},
|
||||
onUserDataClicked = {},
|
||||
onLinkClicked = {},
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
onJoinCallClicked = {},
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
}
|
||||
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(
|
||||
|
||||
return MessageComposerState(
|
||||
richTextEditorState = richTextEditorState,
|
||||
permalinkParser = permalinkParser,
|
||||
isFullScreen = isFullScreen.value,
|
||||
mode = messageComposerContext.composerMode,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
|
||||
@@ -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
|
||||
@Stable
|
||||
data class MessageComposerState(
|
||||
val richTextEditorState: RichTextEditorState,
|
||||
val permalinkParser: PermalinkParser,
|
||||
val isFullScreen: Boolean,
|
||||
val mode: MessageComposerMode,
|
||||
val showAttachmentSourcePicker: Boolean,
|
||||
|
||||
@@ -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(
|
||||
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,
|
||||
|
||||
@@ -109,6 +109,7 @@ internal fun MessageComposerView(
|
||||
modifier = modifier,
|
||||
state = state.richTextEditorState,
|
||||
voiceMessageState = voiceMessageState.voiceMessageState,
|
||||
permalinkParser = state.permalinkParser,
|
||||
subcomposing = subcomposing,
|
||||
onRequestFocus = ::onRequestFocus,
|
||||
onSendMessage = ::sendMessage,
|
||||
|
||||
@@ -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
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
val editorStyle = ElementRichTextEditorStyle.textStyle()
|
||||
val mentionSpanProvider = rememberMentionSpanProvider(currentUserId = currentUserId)
|
||||
val mentionSpanProvider = rememberMentionSpanProvider(
|
||||
currentUserId = currentUserId,
|
||||
permalinkParser = permalinkParser,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
|
||||
@@ -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(
|
||||
onClick = onMessageClicked,
|
||||
onLongClick = onMessageLongClicked,
|
||||
onUserDataClick = onUserDataClicked,
|
||||
onLinkClicked = onLinkClicked,
|
||||
inReplyToClick = ::inReplyToClicked,
|
||||
onReactionClick = onReactionClicked,
|
||||
onReactionLongClick = onReactionLongClicked,
|
||||
@@ -276,6 +278,7 @@ internal fun TimelineViewPreview(
|
||||
onMessageClicked = {},
|
||||
onTimestampClicked = {},
|
||||
onUserDataClicked = {},
|
||||
onLinkClicked = {},
|
||||
onMessageLongClicked = {},
|
||||
onReactionClicked = { _, _ -> },
|
||||
onReactionLongClicked = { _, _ -> },
|
||||
|
||||
@@ -38,6 +38,7 @@ internal fun ATimelineItemEventRow(
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
onLinkClicked = {},
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onReactionLongClick = { _, _ -> },
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
onTimestampClicked = {
|
||||
onTimestampClicked(event)
|
||||
},
|
||||
onMentionClicked = onMentionClicked,
|
||||
onLinkClicked = onLinkClicked,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
@@ -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(
|
||||
modifier: Modifier = Modifier,
|
||||
canShrinkContent: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val timestampLayoutModifier: Modifier
|
||||
val contentModifier: Modifier
|
||||
when {
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
@@ -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(
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
@@ -175,6 +179,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||
onLongClick = {},
|
||||
inReplyToClick = {},
|
||||
onUserDataClick = {},
|
||||
onLinkClicked = {},
|
||||
onTimestampClicked = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onReactionLongClick = { _, _ -> },
|
||||
@@ -200,6 +205,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||
onLongClick = {},
|
||||
inReplyToClick = {},
|
||||
onUserDataClick = {},
|
||||
onLinkClicked = {},
|
||||
onTimestampClicked = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onReactionLongClick = { _, _ -> },
|
||||
|
||||
@@ -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(
|
||||
onClick = { onClick(timelineItem) },
|
||||
onLongClick = { onLongClick(timelineItem) },
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClicked = onLinkClicked,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
@@ -103,6 +105,7 @@ internal fun TimelineItemRow(
|
||||
onLongClick = onLongClick,
|
||||
inReplyToClick = inReplyToClick,
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onTimestampClicked = onTimestampClicked,
|
||||
onReactionClick = onReactionClick,
|
||||
onReactionLongClick = onReactionLongClick,
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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,
|
||||
|
||||
@@ -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(
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -35,6 +35,7 @@ internal fun MessagesViewWithTypingPreview(
|
||||
onEventClicked = { false },
|
||||
onPreviewAttachments = {},
|
||||
onUserDataClicked = {},
|
||||
onLinkClicked = {},
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
onJoinCallClicked = {},
|
||||
|
||||
@@ -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 {
|
||||
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
|
||||
permissionsPresenterFactory = permissionsPresenterFactory,
|
||||
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
permalinkBuilder = FakePermalinkBuilder(),
|
||||
)
|
||||
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
|
||||
this,
|
||||
|
||||
@@ -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
|
||||
onRoomDetailsClicked = onRoomDetailsClicked,
|
||||
onEventClicked = onEventClicked,
|
||||
onUserDataClicked = onUserDataClicked,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onPreviewAttachments = onPreviewAttachments,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
|
||||
@@ -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 {
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(
|
||||
@@ -73,6 +75,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
),
|
||||
matrixClient = matrixClient,
|
||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
|
||||
@@ -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
|
||||
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 {
|
||||
|
||||
@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 {
|
||||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
|
||||
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder()
|
||||
) = MessageComposerPresenter(
|
||||
coroutineScope,
|
||||
room,
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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)
|
||||
|
||||
@@ -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 {
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
roomName = null,
|
||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||
onLinkClicked = EnsureNeverCalledWithParam(),
|
||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||
onMessageLongClicked = EnsureNeverCalledWithParam(),
|
||||
onTimestampClicked = EnsureNeverCalledWithParam(),
|
||||
|
||||
@@ -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 {
|
||||
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
|
||||
featureFlagService = featureFlagService,
|
||||
htmlConverterProvider = FakeHtmlConverterProvider(htmlConverterTransform),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
)
|
||||
|
||||
private fun createStickerContent(
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(inReplyTo.map()?.textContent).isEqualTo("Hello!")
|
||||
assertThat(
|
||||
inReplyTo.map(
|
||||
permalinkParser = FakePermalinkParser()
|
||||
)?.textContent
|
||||
).isEqualTo("Hello!")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,6 +114,10 @@ class InReplyToDetailTest {
|
||||
)
|
||||
)
|
||||
)
|
||||
assertThat(inReplyTo.map()?.textContent).isEqualTo("**Hello!**")
|
||||
assertThat(
|
||||
inReplyTo.map(
|
||||
permalinkParser = FakePermalinkParser()
|
||||
)?.textContent
|
||||
).isEqualTo("**Hello!**")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
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(
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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(
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
return "* $senderDisplayName ${messageType.body}"
|
||||
}
|
||||
is TextMessageType -> {
|
||||
messageType.toPlainText()
|
||||
messageType.toPlainText(permalinkParser)
|
||||
}
|
||||
is VideoMessageType -> {
|
||||
sp.getString(CommonStrings.common_video)
|
||||
|
||||
@@ -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 {
|
||||
sp = AndroidStringProvider(context.resources),
|
||||
roomMembershipContentFormatter = RoomMembershipContentFormatter(fakeMatrixClient, stringProvider),
|
||||
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
|
||||
stateContentFormatter = StateContentFormatter(stringProvider)
|
||||
stateContentFormatter = StateContentFormatter(stringProvider),
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
||||
@@ -17,40 +17,17 @@
|
||||
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].
|
||||
* 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?
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
* 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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
import io.element.android.appconfig.TimelineConfig
|
||||
import io.element.android.libraries.androidutils.file.getSizeOfFiles
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -54,6 +55,7 @@ import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
|
||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber
|
||||
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
|
||||
import io.element.android.libraries.matrix.impl.room.map
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RoomListFactory
|
||||
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
|
||||
@@ -97,7 +99,6 @@ import org.matrix.rustcomponents.sdk.NotificationProcessSetup
|
||||
import org.matrix.rustcomponents.sdk.PowerLevels
|
||||
import org.matrix.rustcomponents.sdk.Room
|
||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||
import org.matrix.rustcomponents.sdk.StateEventType
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter
|
||||
import org.matrix.rustcomponents.sdk.use
|
||||
@@ -229,24 +230,15 @@ class RustMatrixClient(
|
||||
),
|
||||
)
|
||||
|
||||
private val eventFilters = TimelineEventTypeFilter.exclude(
|
||||
listOf(
|
||||
StateEventType.ROOM_ALIASES,
|
||||
StateEventType.ROOM_CANONICAL_ALIAS,
|
||||
StateEventType.ROOM_GUEST_ACCESS,
|
||||
StateEventType.ROOM_HISTORY_VISIBILITY,
|
||||
StateEventType.ROOM_JOIN_RULES,
|
||||
StateEventType.ROOM_PINNED_EVENTS,
|
||||
StateEventType.ROOM_POWER_LEVELS,
|
||||
StateEventType.ROOM_SERVER_ACL,
|
||||
StateEventType.ROOM_TOMBSTONE,
|
||||
StateEventType.SPACE_CHILD,
|
||||
StateEventType.SPACE_PARENT,
|
||||
StateEventType.POLICY_RULE_ROOM,
|
||||
StateEventType.POLICY_RULE_SERVER,
|
||||
StateEventType.POLICY_RULE_USER,
|
||||
).map(FilterTimelineEventType::State)
|
||||
)
|
||||
private val eventFilters = TimelineConfig.excludedEvents
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { listStateEventType ->
|
||||
TimelineEventTypeFilter.exclude(
|
||||
listStateEventType.map { stateEventType ->
|
||||
FilterTimelineEventType.State(stateEventType.map())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override val mediaLoader: MatrixMediaLoader = RustMediaLoader(
|
||||
baseCacheDirectory = baseCacheDirectory,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SUPPORTED_PATHS = listOf(
|
||||
"/#/room/",
|
||||
"/#/user/",
|
||||
"/#/group/"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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
|
||||
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 {
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
* 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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
"&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 {
|
||||
"&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",
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 = { TODO("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")
|
||||
}
|
||||
}
|
||||
@@ -47,4 +47,5 @@ dependencies {
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.robolectric)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
|
||||
@@ -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? {
|
||||
}
|
||||
|
||||
// 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,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
|
||||
* 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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
body = "Hello world"
|
||||
)
|
||||
|
||||
val document = body.toHtmlDocument()
|
||||
val document = body.toHtmlDocument(permalinkParser = FakePermalinkParser())
|
||||
|
||||
assertThat(document).isNull()
|
||||
}
|
||||
@@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
<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 {
|
||||
<br />
|
||||
""".trimIndent()
|
||||
)
|
||||
assertThat(formattedBody.toPlainText()).isNull()
|
||||
assertThat(formattedBody.toPlainText(permalinkParser = FakePermalinkParser())).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -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 {
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
assertThat(messageType.toPlainText()).isEqualTo("This is the fallback text")
|
||||
assertThat(messageType.toPlainText(permalinkParser = FakePermalinkParser())).isEqualTo("This is the fallback text")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
notificationMediaRepoFactory = notificationMediaRepoFactory,
|
||||
context = context,
|
||||
permalinkParser = FakePermalinkParser(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
fun TextComposer(
|
||||
state: RichTextEditorState,
|
||||
voiceMessageState: VoiceMessageState,
|
||||
permalinkParser: PermalinkParser,
|
||||
composerMode: MessageComposerMode,
|
||||
enableTextFormatting: Boolean,
|
||||
enableVoiceMessages: Boolean,
|
||||
@@ -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(
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
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(
|
||||
}
|
||||
|
||||
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(
|
||||
}
|
||||
|
||||
@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 {
|
||||
@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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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 {
|
||||
|
||||
@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)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.samples.minimal
|
||||
|
||||
import android.net.Uri
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData
|
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
||||
|
||||
class OnlyFallbackPermalinkParser : PermalinkParser {
|
||||
override fun parse(uriString: String): PermalinkData {
|
||||
return PermalinkData.FallbackLink(Uri.parse(uriString))
|
||||
}
|
||||
|
||||
override fun parse(uri: Uri): PermalinkData {
|
||||
return PermalinkData.FallbackLink(uri)
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ class RoomListScreen(
|
||||
),
|
||||
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
|
||||
stateContentFormatter = StateContentFormatter(stringProvider),
|
||||
permalinkParser = OnlyFallbackPermalinkParser(),
|
||||
),
|
||||
)
|
||||
private val presenter = RoomListPresenter(
|
||||
|
||||
Reference in New Issue
Block a user