mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Merge branch 'develop' into fix/1612-redaction-reason
This commit is contained in:
+8
-2
@@ -51,6 +51,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.captionOrNull
|
||||
import io.element.android.features.messages.impl.timeline.model.event.htmlCaptionOrNull
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
@@ -396,7 +397,7 @@ class MessagesPresenter(
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
|
||||
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
|
||||
TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState, timelineProtectionState)
|
||||
TimelineItemAction.ReplyInThread -> {
|
||||
@@ -575,10 +576,15 @@ class MessagesPresenter(
|
||||
private suspend fun handleActionEditCaption(
|
||||
targetEvent: TimelineItem.Event,
|
||||
composerState: MessageComposerState,
|
||||
enableTextFormatting: Boolean,
|
||||
) {
|
||||
val composerMode = MessageComposerMode.EditCaption(
|
||||
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||
content = targetEvent.content.captionOrNull().orEmpty(),
|
||||
content = if (enableTextFormatting) {
|
||||
targetEvent.content.htmlCaptionOrNull() ?: targetEvent.content.captionOrNull()
|
||||
} else {
|
||||
targetEvent.content.captionOrNull()
|
||||
}.orEmpty(),
|
||||
)
|
||||
composerState.eventSink(
|
||||
MessageComposerEvent.SetMode(composerMode)
|
||||
|
||||
+14
-1
@@ -115,9 +115,10 @@ class SuggestionsProcessor(
|
||||
.filterUpTo(MAX_BATCH_ITEMS) { member ->
|
||||
isJoinedMemberAndNotSelf(member) && memberMatchesQuery(member, query)
|
||||
}
|
||||
.sortedBy { member -> matchRank(member, query) }
|
||||
.map(ResolvedSuggestion::Member)
|
||||
|
||||
if ("room".contains(query) && canSendRoomMention) {
|
||||
if ("room".startsWith(query, ignoreCase = true) && canSendRoomMention) {
|
||||
listOf(ResolvedSuggestion.AtRoom) + matchingMembers
|
||||
} else {
|
||||
matchingMembers
|
||||
@@ -125,6 +126,18 @@ class SuggestionsProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Members whose display name starts with the query come first, then members whose user id
|
||||
* localpart starts with it, then the remaining matches on any other part of the name or id.
|
||||
*/
|
||||
private fun matchRank(member: RoomMember, query: String): Int {
|
||||
return when {
|
||||
member.displayName?.startsWith(query, ignoreCase = true) == true -> 0
|
||||
member.userId.extractedDisplayName.startsWith(query, ignoreCase = true) -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// We don't want to retrieve thousands of members
|
||||
private const val MAX_BATCH_ITEMS = 100
|
||||
|
||||
+2
-2
@@ -52,9 +52,9 @@ sealed interface TimelineEvent {
|
||||
*/
|
||||
sealed interface TimelineItemPollEvent : TimelineItemEvent
|
||||
|
||||
data class SelectPollAnswer(
|
||||
data class SendPollResponse(
|
||||
val pollStartId: EventId,
|
||||
val answerId: String
|
||||
val answerIds: List<String>
|
||||
) : TimelineItemPollEvent
|
||||
|
||||
data class EndPoll(
|
||||
|
||||
+5
-3
@@ -201,13 +201,15 @@ class TimelinePresenter(
|
||||
newEventState.value = NewEventState.None
|
||||
}
|
||||
}
|
||||
is TimelineEvent.SelectPollAnswer -> sessionCoroutineScope.launch {
|
||||
is TimelineEvent.SendPollResponse -> sessionCoroutineScope.launch {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
sendPollResponseAction.execute(
|
||||
timeline = this,
|
||||
pollStartId = event.pollStartId,
|
||||
answerId = event.answerId
|
||||
)
|
||||
answerIds = event.answerIds,
|
||||
).onFailure { error ->
|
||||
Timber.tag(tag).e(error, "Failed to send poll response")
|
||||
}
|
||||
}
|
||||
}
|
||||
is TimelineEvent.EndPoll -> sessionCoroutineScope.launch {
|
||||
|
||||
+48
-2
@@ -49,7 +49,9 @@ import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
||||
import androidx.compose.ui.semantics.isTraversalGroup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -217,13 +219,23 @@ fun TimelineItemEventRow(
|
||||
inReplyToClick(inReplyToEventId)
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.canBeRepliedTo
|
||||
val accessibilityActions = rememberTimelineItemAccessibilityActions(
|
||||
canReply = canReply,
|
||||
onLongClick = onLongClick,
|
||||
onSwipeToReply = onSwipeToReply,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.semantics { customActions = accessibilityActions }
|
||||
) {
|
||||
if (event.groupPosition.isNew()) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
}
|
||||
val canReply = timelineRoomInfo.userHasPermissionToSendMessage && event.canBeRepliedTo
|
||||
if (canReply) {
|
||||
val state: SwipeableActionsState = rememberSwipeableActionsState()
|
||||
val offset = state.offset.floatValue
|
||||
@@ -319,6 +331,40 @@ fun TimelineItemEventRow(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes the gestures of a timeline item — long press for the action list, swipe for a reply — as TalkBack actions, since neither gesture is reachable
|
||||
* with a screen reader enabled.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberTimelineItemAccessibilityActions(
|
||||
canReply: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onSwipeToReply: () -> Unit,
|
||||
): List<CustomAccessibilityAction> {
|
||||
val messageActionsLabel = stringResource(CommonStrings.common_message_actions)
|
||||
val replyLabel = stringResource(CommonStrings.action_reply)
|
||||
val latestOnLongClick by rememberUpdatedState(onLongClick)
|
||||
val latestOnSwipeToReply by rememberUpdatedState(onSwipeToReply)
|
||||
return remember(canReply, messageActionsLabel, replyLabel) {
|
||||
buildList {
|
||||
add(
|
||||
CustomAccessibilityAction(messageActionsLabel) {
|
||||
latestOnLongClick()
|
||||
true
|
||||
}
|
||||
)
|
||||
if (canReply) {
|
||||
add(
|
||||
CustomAccessibilityAction(replyLabel) {
|
||||
latestOnSwipeToReply()
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThreadSummaryView(
|
||||
threadSummary: ThreadSummary,
|
||||
|
||||
+5
-3
@@ -26,8 +26,8 @@ fun TimelineItemPollView(
|
||||
eventSink: (TimelineEvent.TimelineItemPollEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onSelectAnswer(pollStartId: EventId, answerId: String) {
|
||||
eventSink(TimelineEvent.SelectPollAnswer(pollStartId, answerId))
|
||||
fun onSendPollResponse(pollStartId: EventId, answerIds: List<String>) {
|
||||
eventSink(TimelineEvent.SendPollResponse(pollStartId, answerIds))
|
||||
}
|
||||
|
||||
fun onEndPoll(pollStartId: EventId) {
|
||||
@@ -43,10 +43,12 @@ fun TimelineItemPollView(
|
||||
question = content.question,
|
||||
answerItems = content.answerItems.toImmutableList(),
|
||||
pollKind = content.pollKind,
|
||||
isMultipleSelection = content.maxSelections > 1u,
|
||||
maxSelections = content.maxSelections,
|
||||
isPollEnded = content.isEnded,
|
||||
isPollEditable = content.isEditable,
|
||||
isMine = content.isMine,
|
||||
onSelectAnswer = ::onSelectAnswer,
|
||||
onSendPollResponse = ::onSendPollResponse,
|
||||
onEditPoll = ::onEditPoll,
|
||||
onEndPoll = ::onEndPoll,
|
||||
modifier = modifier,
|
||||
|
||||
+10
-2
@@ -15,7 +15,9 @@ import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -23,6 +25,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentPreviewParam
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
@@ -47,8 +50,13 @@ fun TimelineItemTextView(
|
||||
// The View <-> Compose interop is not working well with Compose UI tests (it loops indefinitely), so we skip it in the UI test mode.
|
||||
if (LocalUiTestMode.current) return
|
||||
|
||||
val emojiOnly = content.formattedBody.toString() == content.body &&
|
||||
content.body.replace(" ", "").containsOnlyEmojis()
|
||||
val isInPreview = LocalInspectionMode.current
|
||||
val emojiOnly = remember(content.body, content.formattedBody, isInPreview) {
|
||||
content.formattedBody.toString() == content.body &&
|
||||
content.body.replace(" ", "").let { body ->
|
||||
if (isInPreview) body == AN_EMOJI_ONLY_TEXT else body.containsOnlyEmojis()
|
||||
}
|
||||
}
|
||||
val textStyle = when {
|
||||
emojiOnly -> ElementTheme.typography.fontHeadingXlRegular
|
||||
else -> ElementTheme.typography.fontBodyLgRegular
|
||||
|
||||
+8
@@ -108,6 +108,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
@@ -132,6 +133,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
@@ -174,6 +176,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mediaSource = messageType.source,
|
||||
@@ -198,6 +201,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
@@ -216,6 +220,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
mediaSource = messageType.source,
|
||||
duration = messageType.info?.duration ?: Duration.ZERO,
|
||||
@@ -235,6 +240,7 @@ class TimelineItemContentMessageFactory(
|
||||
fileSize = messageType.info?.size ?: 0,
|
||||
caption = messageType.caption?.trimEnd(),
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formattedCaption?.body,
|
||||
isEdited = content.isEdited,
|
||||
thumbnailSource = messageType.info?.thumbnailSource,
|
||||
mediaSource = messageType.source,
|
||||
@@ -345,6 +351,7 @@ class TimelineItemContentMessageFactory(
|
||||
body = messageType.body,
|
||||
caption = messageType.body.trimEnd().takeIf { it.isNotEmpty() },
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formatted?.body,
|
||||
isEdited = content.isEdited,
|
||||
items = galleryItems.toImmutableList(),
|
||||
)
|
||||
@@ -402,6 +409,7 @@ class TimelineItemContentMessageFactory(
|
||||
body = messageType.body,
|
||||
caption = messageType.body.trimEnd().takeIf { it.isNotEmpty() },
|
||||
formattedCaption = formattedCaption,
|
||||
htmlCaption = messageType.formatted?.body,
|
||||
isEdited = content.isEdited,
|
||||
attachments = attachments.toImmutableList(),
|
||||
)
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ class TimelineItemContentPollFactory(
|
||||
question = pollContentState.question,
|
||||
answerItems = pollContentState.answerItems,
|
||||
pollKind = pollContentState.pollKind,
|
||||
maxSelections = pollContentState.maxSelections,
|
||||
isEnded = pollContentState.isPollEnded,
|
||||
isEdited = content.isEdited
|
||||
)
|
||||
|
||||
+24
-1
@@ -28,10 +28,12 @@ import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
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.core.UserId
|
||||
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.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
@@ -64,7 +66,7 @@ class TimelineItemEventFactory(
|
||||
val currentSender = currentTimelineItem.event.sender
|
||||
val groupPosition =
|
||||
computeGroupPosition(currentTimelineItem, timelineItems, index)
|
||||
val senderProfile = currentTimelineItem.event.senderProfile
|
||||
val senderProfile = currentTimelineItem.event.senderProfile.orFallbackTo(roomMembers, currentSender)
|
||||
val sentTime = dateFormatter.format(
|
||||
timestamp = currentTimelineItem.event.timestamp,
|
||||
mode = DateFormatterMode.TimeOnly,
|
||||
@@ -138,11 +140,32 @@ class TimelineItemEventFactory(
|
||||
roomMembers: List<RoomMember>,
|
||||
renderReadReceipts: Boolean,
|
||||
): TimelineItem.Event {
|
||||
val senderProfile = receivedMatrixTimelineItem.event.senderProfile.orFallbackTo(roomMembers, timelineItem.senderId)
|
||||
return timelineItem.copy(
|
||||
senderProfile = senderProfile,
|
||||
senderAvatar = timelineItem.senderAvatar.copy(
|
||||
name = senderProfile.getDisambiguatedDisplayName(timelineItem.senderId),
|
||||
url = senderProfile.getAvatarUrl(),
|
||||
),
|
||||
readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The timeline only knows the profiles the SDK has resolved, so a sender it has nothing for is rendered as a raw
|
||||
* user ID. The room member list usually knows that user, so use it rather than showing the ID.
|
||||
*/
|
||||
private fun ProfileDetails.orFallbackTo(roomMembers: List<RoomMember>, senderId: UserId): ProfileDetails {
|
||||
if (this is ProfileDetails.Ready) return this
|
||||
val member = roomMembers.find { it.userId == senderId } ?: return this
|
||||
return ProfileDetails.Ready(
|
||||
displayName = member.displayName,
|
||||
displayNameAmbiguous = member.isNameAmbiguous,
|
||||
avatarUrl = member.avatarUrl,
|
||||
displayedStatus = member.displayedStatus,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
|
||||
if (!config.computeReactions) {
|
||||
return TimelineItemReactions(reactions = persistentListOf())
|
||||
|
||||
+1
-1
@@ -91,7 +91,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||
// Can be grouped
|
||||
is FailedToParseMessageLikeContent,
|
||||
is MessageContent,
|
||||
RedactedContent,
|
||||
is RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
is UnableToDecryptContent,
|
||||
|
||||
+4
-2
@@ -12,6 +12,7 @@ import androidx.annotation.VisibleForTesting
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
@@ -99,7 +100,8 @@ internal const val MIN_REDACTED_RUN_SIZE = 3
|
||||
* Fold runs of [MIN_REDACTED_RUN_SIZE] or more consecutive deleted (redacted) messages into a single
|
||||
* [TimelineItem.GroupedEvents], so a long stretch of "Message removed" tiles shows as one expandable
|
||||
* "N removed messages" line, like element-web. Shorter runs, and anything that is not a redacted event
|
||||
* (day dividers included), are left untouched.
|
||||
* (day dividers included), are left untouched. A deleted message that still heads a thread is left
|
||||
* untouched too, so that its thread summary stays reachable.
|
||||
*
|
||||
* This runs as the final step of [TimelineItemGrouper.group], after the state and membership events
|
||||
* have been grouped. It is deliberately kept out of the normal grouping: a run needs a higher minimum
|
||||
@@ -134,7 +136,7 @@ internal fun List<TimelineItem>.collapseRedactedRuns(groupIds: MutableMap<String
|
||||
}
|
||||
|
||||
for (item in this) {
|
||||
if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent) {
|
||||
if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent && item.threadInfo !is TimelineItemThreadInfo.ThreadRoot) {
|
||||
run.add(item)
|
||||
} else {
|
||||
flushRun()
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ data class TimelineItemAttachmentsContent(
|
||||
val body: String,
|
||||
val caption: String?,
|
||||
val formattedCaption: CharSequence?,
|
||||
val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val attachments: ImmutableList<AttachmentItem>,
|
||||
) : TimelineItemEventContent, TimelineItemEventMutableContent {
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ data class TimelineItemAudioContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val duration: Duration,
|
||||
override val mediaSource: MediaSource,
|
||||
|
||||
+10
@@ -33,6 +33,9 @@ sealed interface TimelineItemEventContentWithAttachment :
|
||||
val fileSize: Long?
|
||||
val caption: String?
|
||||
val formattedCaption: CharSequence?
|
||||
|
||||
/** The caption as it was written, in HTML, so that editing it can start from the formatted text. */
|
||||
val htmlCaption: String?
|
||||
val mediaSource: MediaSource
|
||||
val mimeType: String
|
||||
val formattedFileSize: String
|
||||
@@ -127,6 +130,13 @@ fun TimelineItemEventContent.formattedCaptionOrNull(): CharSequence? = when (thi
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun TimelineItemEventContent.htmlCaptionOrNull(): String? = when (this) {
|
||||
is TimelineItemEventContentWithAttachment -> htmlCaption
|
||||
is TimelineItemGalleryContent -> htmlCaption
|
||||
is TimelineItemAttachmentsContent -> htmlCaption
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun TimelineItemEventContentWithAttachment.duration(): Duration? {
|
||||
return when (this) {
|
||||
is TimelineItemAudioContent -> duration
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ data class TimelineItemFileContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
override val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ data class TimelineItemGalleryContent(
|
||||
val body: String,
|
||||
val caption: String?,
|
||||
val formattedCaption: CharSequence?,
|
||||
val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val items: ImmutableList<GalleryItem>,
|
||||
) : TimelineItemEventContent, TimelineItemEventMutableContent {
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ data class TimelineItemImageContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
override val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
+2
@@ -28,6 +28,7 @@ fun aTimelineItemImageContent(
|
||||
blurhash: String? = A_BLUR_HASH,
|
||||
filename: String = "A picture.jpg",
|
||||
caption: String? = null,
|
||||
htmlCaption: String? = null,
|
||||
width: Int? = null,
|
||||
height: Int? = 300,
|
||||
) = TimelineItemImageContent(
|
||||
@@ -35,6 +36,7 @@ fun aTimelineItemImageContent(
|
||||
fileSize = 4 * 1024 * 1024L,
|
||||
caption = caption,
|
||||
formattedCaption = null,
|
||||
htmlCaption = htmlCaption,
|
||||
isEdited = false,
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ data class TimelineItemPollContent(
|
||||
val question: String,
|
||||
val answerItems: List<PollAnswerItem>,
|
||||
val pollKind: PollKind,
|
||||
val maxSelections: ULong,
|
||||
val isEnded: Boolean,
|
||||
override val isEdited: Boolean,
|
||||
) : TimelineItemEventContent,
|
||||
|
||||
+2
@@ -32,12 +32,14 @@ fun aTimelineItemPollContent(
|
||||
isEditable: Boolean = false,
|
||||
isEnded: Boolean = false,
|
||||
isEdited: Boolean = false,
|
||||
maxSelections: ULong = 1u,
|
||||
): TimelineItemPollContent {
|
||||
return TimelineItemPollContent(
|
||||
eventId = EventId("\$anEventId"),
|
||||
pollKind = PollKind.Disclosed,
|
||||
question = question,
|
||||
answerItems = answerItems,
|
||||
maxSelections = maxSelections,
|
||||
isMine = isMine,
|
||||
isEditable = isEditable,
|
||||
isEnded = isEnded,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ data class TimelineItemStickerContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
override val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ data class TimelineItemVideoContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val duration: Duration,
|
||||
override val mediaSource: MediaSource,
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ data class TimelineItemVoiceContent(
|
||||
override val fileSize: Long?,
|
||||
override val caption: String?,
|
||||
override val formattedCaption: CharSequence?,
|
||||
override val htmlCaption: String? = null,
|
||||
override val isEdited: Boolean,
|
||||
val duration: Duration,
|
||||
override val mediaSource: MediaSource,
|
||||
|
||||
+1
-11
@@ -8,25 +8,15 @@
|
||||
|
||||
package io.element.android.features.messages.impl.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import com.sigpwned.emoji4j.core.Grapheme.Type.EMOJI
|
||||
import com.sigpwned.emoji4j.core.Grapheme.Type.PICTOGRAPHIC
|
||||
import com.sigpwned.emoji4j.core.GraphemeMatchResult
|
||||
import com.sigpwned.emoji4j.core.GraphemeMatcher
|
||||
import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT
|
||||
|
||||
/**
|
||||
* Returns true if the string consists exclusively of "emoji or pictographic graphemes".
|
||||
*/
|
||||
@Composable
|
||||
fun String.containsOnlyEmojis(): Boolean {
|
||||
if (LocalInspectionMode.current) return this == AN_EMOJI_ONLY_TEXT
|
||||
if (isEmpty()) return false
|
||||
return containsOnlyEmojisInternal()
|
||||
}
|
||||
|
||||
internal fun String.containsOnlyEmojisInternal(): Boolean {
|
||||
internal fun String.containsOnlyEmojis(): Boolean {
|
||||
val matcher = GraphemeMatcher(this)
|
||||
var m: GraphemeMatchResult? = null
|
||||
var contiguous = true
|
||||
|
||||
@@ -7,19 +7,37 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Verschlüsselt von einem unbekannten oder gelöschten Gerät."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Verschlüsselt durch ein Gerät, das nicht von seinem Besitzer verifiziert wurde."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Verschlüsselt durch einen nicht verifizierten Nutzer."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Ungültiger MIME-Typ: %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Fehlende Medieninhalte"</string>
|
||||
<string name="error_sending_failure_other">"Fehlerdetails: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Senden von einem nicht verifizierten Gerät"</string>
|
||||
<string name="error_sending_failure_unknown">"Unbekannter Fehler"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Bild horizontal spiegeln"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Horizontal gespiegelt"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Original"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Bild vertikal spiegeln"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Vertikal gespiegelt"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Original"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Drehe das Bild nach links"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="one">"%1$d Grad"</item>
|
||||
<item quantity="other">"%1$d Grad"</item>
|
||||
</plurals>
|
||||
<string name="screen_image_edition_title">"Foto bearbeiten"</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Bildunterschriften sind für Nutzer älterer Apps möglicherweise nicht sichtbar."</string>
|
||||
<string name="screen_media_upload_preview_change_video_quality_prompt">"Tippe, um die Qualität des Video-Uploads zu ändern"</string>
|
||||
<string name="screen_media_upload_preview_error_could_not_be_uploaded">"Die Datei konnte nicht hochgeladen werden."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_sending">"Das Hochladen der Medien ist fehlgeschlagen. Bitte versuche es erneut."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_message">"Die maximal zulässige Dateigröße beträgt %1$s."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"Die Datei ist zu groß zum Hochladen."</string>
|
||||
<string name="screen_media_upload_preview_error_too_large_title">"Die Datei ist zu groß zum Hochladen"</string>
|
||||
<string name="screen_media_upload_preview_item_count">"%1$d von %2$d"</string>
|
||||
<string name="screen_media_upload_preview_optimize_image_quality_title">"Optimiere die Bildqualität"</string>
|
||||
<string name="screen_media_upload_preview_processing">"Verarbeitung läuft …"</string>
|
||||
<string name="screen_media_upload_preview_title">"Medien hinzufügen"</string>
|
||||
<string name="screen_report_content_block_user">"Nutzer blockieren"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Nutzers ausblenden möchtest"</string>
|
||||
<string name="screen_report_content_explanation">"Diese Nachricht wird dem Administrator deines Homeservers gemeldet. Dieser kann allerdings keine verschlüsselten Nachrichten lesen."</string>
|
||||
<string name="screen_report_content_explanation">"Diese Nachricht wird an deinen Kontoanbieter gemeldet. Verschlüsselte Nachrichten kann dieser nicht lesen."</string>
|
||||
<string name="screen_report_content_hint">"Grund für die Meldung dieses Inhalts"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Foto aufnehmen"</string>
|
||||
@@ -29,7 +47,7 @@
|
||||
<string name="screen_room_attachment_source_location">"Standort teilen"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Umfrage"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Textformatierung"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist derzeit nicht verfügbar"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist derzeit nicht verfügbar."</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"Der Nachrichtenverlauf ist nicht verfügbar. Verifiziere dieses Gerät, um deinen Nachrichtenverlauf zu sehen."</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Möchtest du sie wieder einladen?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"Du bist allein in diesem Chat"</string>
|
||||
@@ -57,6 +75,10 @@
|
||||
<string name="screen_room_timeline_reactions_show_more">"Mehr anzeigen"</string>
|
||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"Zusammenfassung der Reaktionen anzeigen"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Neu"</string>
|
||||
<plurals name="screen_room_timeline_redacted_messages">
|
||||
<item quantity="one">"%1$d entfernte Nachricht"</item>
|
||||
<item quantity="other">"%1$d entfernte Nachrichten"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d Änderung im Chat"</item>
|
||||
<item quantity="other">"%1$d Änderungen im Chat"</item>
|
||||
@@ -67,7 +89,7 @@
|
||||
<string name="screen_room_timeline_upgraded_room_message">"Dieser Chat ist eine Fortsetzung eines anderen Chats"</string>
|
||||
<plurals name="screen_room_typing_many_members">
|
||||
<item quantity="one">"%1$s, %2$s und %3$d weitere Person"</item>
|
||||
<item quantity="other">"%1$s, %2$s und %3$d weitere Person"</item>
|
||||
<item quantity="other">"%1$s, %2$s und %3$d weitere Personen"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_typing_notification">
|
||||
<item quantity="one">"%1$s schreibt…"</item>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<string name="screen_media_upload_preview_processing">"Επεξεργασία…"</string>
|
||||
<string name="screen_report_content_block_user">"Αποκλεισμός χρήστη"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Επέλεξε εάν θες να αποκρύψεις όλα τα τρέχοντα και μελλοντικά μηνύματα από αυτόν τον χρήστη"</string>
|
||||
<string name="screen_report_content_explanation">"Αυτό το μήνυμα θα αναφερθεί στον διαχειριστή του οικιακού διακομιστή σας. Δεν θα μπορεί να διαβάσει κρυπτογραφημένα μηνύματα."</string>
|
||||
<string name="screen_report_content_explanation">"Αυτό το μήνυμα θα αναφερθεί στον πάροχο του λογαριασμού σας. Δεν θα μπορεί να διαβάσει κανένα κρυπτογραφημένο μήνυμα."</string>
|
||||
<string name="screen_report_content_hint">"Λόγος αναφοράς αυτού του περιεχομένου"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Κάμερα"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Τράβηξε φωτογραφία"</string>
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Krüptitud tundmatu või kustutatud seadme poolt."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Krüptitud seadme poolt, mida tema omanik pole verifitseerinud."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Krüptitud verifitseerimata kasutaja poolt."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Vigane MIME-tüüp: %s"</string>
|
||||
<string name="error_sending_failure_other">"Vea üksikasjad: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Saadetakse verifitseerimata seadmest"</string>
|
||||
<string name="error_sending_failure_unknown">"Tundmatu viga"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Pööra pilti ümber rõhttelje"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Pilt on pööratud ümber rõhttelje"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Pildi algne variant"</string>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Chiffré par un appareil inconnu ou supprimé."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Chiffré par un appareil non vérifié par son propriétaire."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Chiffré par un utilisateur non vérifié."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Type MIME invalide : %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Contenu multimédia manquant"</string>
|
||||
<string name="error_sending_failure_other">"Détails de l’erreur : %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Envoi depuis un appareil non vérifié"</string>
|
||||
<string name="error_sending_failure_unknown">"Erreur inconnue"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Retourner l’image horizontalement"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Retournée horizontalement"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Šifrirano nepoznatim ili izbrisanim uređajem."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Šifrirano uređajem koji vlasnik nije potvrdio."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Šifrirao neprovjereni korisnik."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Nevažeći MIME tip: %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Nedostaje medijski sadržaj"</string>
|
||||
<string name="error_sending_failure_other">"Detalji pogreške: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Slanje s neprovjerenog uređaja"</string>
|
||||
<string name="error_sending_failure_unknown">"Nepoznata pogreška"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Okreni sliku vodoravno"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Okrenuto vodoravno"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Izvornik"</string>
|
||||
|
||||
@@ -7,6 +7,17 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Dienkripsi oleh perangkat yang tidak dikenal atau dihapus."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Dienkripsi oleh perangkat yang tidak diverifikasi oleh pemiliknya."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Dienkripsi oleh pengguna yang tidak terverifikasi."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Tipe MIME tidak valid: %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Konten media tidak ditemukan"</string>
|
||||
<string name="error_sending_failure_other">"Detail kesalahan: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Mengirim dari perangkat yang belum terverifikasi"</string>
|
||||
<string name="error_sending_failure_unknown">"Kesalahan tidak diketahui"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Balikkan gambar secara horizontal"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Dibalik secara horizontal"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Asli"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically">"Balikkan gambar secara vertikal"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_flipped">"Dibalik secara vertikal"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_vertically_state_original">"Asli"</string>
|
||||
<string name="screen_image_edition_a11y_rotate_to_the_left">"Putar gambar ke kiri"</string>
|
||||
<plurals name="screen_image_edition_a11y_rotation_state">
|
||||
<item quantity="other">"%1$d derajat"</item>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Cifrato da un dispositivo sconosciuto o eliminato."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Cifrato da un dispositivo non verificato dal proprietario."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Cifrato da un utente non verificato."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Tipo MIME non valido: %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Contenuto multimediale mancante"</string>
|
||||
<string name="error_sending_failure_other">"Dettagli dell\'errore: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Invio da dispositivo non verificato"</string>
|
||||
<string name="error_sending_failure_unknown">"Errore sconosciuto"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Capovolgi l\'immagine orizzontalmente"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Capovolto orizzontalmente"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Originale"</string>
|
||||
@@ -32,7 +37,7 @@
|
||||
<string name="screen_media_upload_preview_title">"Aggiungi contenuti multimediali"</string>
|
||||
<string name="screen_report_content_block_user">"Blocca utente"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Seleziona se vuoi nascondere tutti i messaggi attuali e futuri di questo utente"</string>
|
||||
<string name="screen_report_content_explanation">"Questo messaggio verrà segnalato all\'amministratore dell\'homeserver. Questi non sarà in grado di leggere i messaggi cifrati."</string>
|
||||
<string name="screen_report_content_explanation">"Questo messaggio verrà segnalato al gestore del tuo account. Quest’ultimo non potrà leggere alcun messaggio cifrato."</string>
|
||||
<string name="screen_report_content_hint">"Motivo della segnalazione di questo contenuto"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Fotocamera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Scatta foto"</string>
|
||||
@@ -70,6 +75,10 @@
|
||||
<string name="screen_room_timeline_reactions_show_more">"Mostra di più"</string>
|
||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"Mostra il riepilogo delle reazioni"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"Nuovo"</string>
|
||||
<plurals name="screen_room_timeline_redacted_messages">
|
||||
<item quantity="one">"%1$d messaggio rimosso"</item>
|
||||
<item quantity="other">"%1$d messaggi rimossi"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d modifica alla stanza"</item>
|
||||
<item quantity="other">"%1$d modifiche alla stanza"</item>
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
<string name="screen_room_timeline_reactions_show_more">"さらに表示"</string>
|
||||
<string name="screen_room_timeline_reactions_show_reactions_summary">"リアクションのまとめを表示"</string>
|
||||
<string name="screen_room_timeline_read_marker_title">"新着"</string>
|
||||
<plurals name="screen_room_timeline_redacted_messages">
|
||||
<item quantity="other">"%1$d 件の削除されたメッセージ"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_state_changes">
|
||||
<item quantity="other">"%1$d 個のルーム更新点"</item>
|
||||
</plurals>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Zaszyfrowana przez nieznane lub usunięte urządzenie."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Zaszyfrowana przez urządzenie niezweryfikowane przez jego właściciela."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Zaszyfrowana przez niezweryfikowanego użytkownika."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Nieprawidłowy typ MIME: %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Brak treści multimedialnych"</string>
|
||||
<string name="error_sending_failure_other">"Szczegóły błędu: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Wysyłanie z niezweryfikowanego urządzenia"</string>
|
||||
<string name="error_sending_failure_unknown">"Nieznany błąd"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Odwróć obraz w poziomie"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Odwrócono w poziomie"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Oryginalny"</string>
|
||||
@@ -33,7 +38,7 @@
|
||||
<string name="screen_media_upload_preview_title">"Dodaj media"</string>
|
||||
<string name="screen_report_content_block_user">"Zablokuj użytkownika"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Zaznacz, jeśli chcesz ukryć wszystkie bieżące i przyszłe wiadomości od tego użytkownika."</string>
|
||||
<string name="screen_report_content_explanation">"Ta wiadomość zostanie zgłoszona do administratora Twojego serwera domowego. Nie będzie mógł on przeczytać żadnych zaszyfrowanych wiadomości."</string>
|
||||
<string name="screen_report_content_explanation">"Wiadomość zostanie zgłoszona do Twojego dostawcy konta. Nie będzie mógł on przeczytać żadnych zaszyfrowanych wiadomości."</string>
|
||||
<string name="screen_report_content_hint">"Powód zgłoszenia treści"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Zrób zdjęcie"</string>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"Зашифровано невідомим або видаленим пристроєм."</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"Зашифровано пристроєм, який не звірено його власником."</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"Зашифровано не звіреним користувачем."</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"Недійсний тип MIME: %s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"Відсутній медіавміст"</string>
|
||||
<string name="error_sending_failure_other">"Подробиці помилки: %s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"Надсилання з незвіреного пристрою"</string>
|
||||
<string name="error_sending_failure_unknown">"Невідома помилка"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"Повернути зображення по горизонталі"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"Перевернуто по горизонталі"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"Оригінал"</string>
|
||||
@@ -33,7 +38,7 @@
|
||||
<string name="screen_media_upload_preview_title">"Додати медіа"</string>
|
||||
<string name="screen_report_content_block_user">"Заблокувати користувача"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Перевірте, чи хочете ви приховати всі поточні та майбутні повідомлення від цього користувача"</string>
|
||||
<string name="screen_report_content_explanation">"Це повідомлення буде надіслано адміністраторам вашого домашнього сервера. Вони не зможуть прочитати зашифровані повідомлення."</string>
|
||||
<string name="screen_report_content_explanation">"Це повідомлення буде передано постачальнику вашого облікового запису. Він не зможе прочитати зашифрованих повідомлень."</string>
|
||||
<string name="screen_report_content_hint">"Причина скарги на цей вміст"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Камера"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Зробити фото"</string>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<string name="crypto_event_authenticity_unknown_device">"由未知或已删除的设备加密。"</string>
|
||||
<string name="crypto_event_authenticity_unsigned_device">"由未经其所有者验证的设备加密。"</string>
|
||||
<string name="crypto_event_authenticity_unverified_identity">"由未经验证的用户加密。"</string>
|
||||
<string name="error_sending_failure_invalid_mime_type">"无效的 MIME 类型:%s"</string>
|
||||
<string name="error_sending_failure_missing_media_content">"媒体内容丢失"</string>
|
||||
<string name="error_sending_failure_other">"错误详细信息:%s"</string>
|
||||
<string name="error_sending_failure_sending_from_unverified_device">"由未经验证的设备发送"</string>
|
||||
<string name="error_sending_failure_unknown">"未知错误"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally">"水平翻转图像"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_flipped">"水平翻转"</string>
|
||||
<string name="screen_image_edition_a11y_flip_image_horizontally_state_original">"原始"</string>
|
||||
@@ -31,7 +36,7 @@
|
||||
<string name="screen_media_upload_preview_title">"添加媒体"</string>
|
||||
<string name="screen_report_content_block_user">"屏蔽用户"</string>
|
||||
<string name="screen_report_content_block_user_hint">"请确认是否要隐藏该用户当前和未来的所有消息"</string>
|
||||
<string name="screen_report_content_explanation">"此消息将举报给服务器管理员。他们无法读取任何加密消息。"</string>
|
||||
<string name="screen_report_content_explanation">"此消息将举报给账户提供者。他们无法读取任何加密消息。"</string>
|
||||
<string name="screen_report_content_hint">"举报此内容的理由"</string>
|
||||
<string name="screen_room_attachment_source_camera">"相机"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"拍摄照片"</string>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<string name="screen_media_upload_preview_title">"Add media"</string>
|
||||
<string name="screen_report_content_block_user">"Block user"</string>
|
||||
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
|
||||
<string name="screen_report_content_explanation">"This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."</string>
|
||||
<string name="screen_report_content_explanation">"This message will be reported to your account provider. They will not be able to read any encrypted messages."</string>
|
||||
<string name="screen_report_content_hint">"Reason for reporting this content"</string>
|
||||
<string name="screen_room_attachment_source_camera">"Camera"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Take photo"</string>
|
||||
|
||||
+27
@@ -1065,6 +1065,33 @@ class MessagesPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action edit caption starts from the formatted caption`() = runTest {
|
||||
val messageEvent = aMessageEvent(
|
||||
content = aTimelineItemImageContent(
|
||||
caption = "Hello world",
|
||||
htmlCaption = "<b>Hello</b> world",
|
||||
)
|
||||
)
|
||||
val composerRecorder = EventsRecorder<MessageComposerEvent>()
|
||||
val presenter = createMessagesPresenter(
|
||||
messageComposerPresenter = { aMessageComposerState(eventSink = composerRecorder, showTextFormatting = true) },
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.EditCaption, messageEvent))
|
||||
awaitItem()
|
||||
composerRecorder.assertSingle(
|
||||
MessageComposerEvent.SetMode(
|
||||
composerMode = MessageComposerMode.EditCaption(
|
||||
eventOrTransactionId = AN_EVENT_ID.toEventOrTransactionId(),
|
||||
content = "<b>Hello</b> world",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action add caption`() = runTest {
|
||||
val composerRecorder = EventsRecorder<MessageComposerEvent>()
|
||||
|
||||
+42
@@ -383,6 +383,48 @@ class SuggestionsProcessorTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processing Mention suggestion returns the members matching the start of the query first`() = runTest {
|
||||
val matchingOnAnyPart = aRoomMember(userId = UserId("@carol:server.org"), displayName = "Not Alice")
|
||||
val matchingOnUserIdStart = aRoomMember(userId = UserId("@alicia:server.org"), displayName = null)
|
||||
val matchingOnDisplayNameStart = aRoomMember(userId = UserId("@zoe:server.org"), displayName = "Alice")
|
||||
val result = suggestionsProcessor.process(
|
||||
suggestion = aMentionSuggestion("ali"),
|
||||
roomMembersState = RoomMembersState.Ready(
|
||||
persistentListOf(matchingOnAnyPart, matchingOnUserIdStart, matchingOnDisplayNameStart)
|
||||
),
|
||||
roomAliasSuggestions = emptyList(),
|
||||
currentUserId = A_USER_ID_2,
|
||||
canSendRoomMention = { false },
|
||||
isInThread = false,
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
ResolvedSuggestion.Member(matchingOnDisplayNameStart),
|
||||
ResolvedSuggestion.Member(matchingOnUserIdStart),
|
||||
ResolvedSuggestion.Member(matchingOnAnyPart),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processing Mention suggestion returns room ignoring the case of the query`() = runTest {
|
||||
val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = "alice")
|
||||
val result = suggestionsProcessor.process(
|
||||
suggestion = aMentionSuggestion("RO"),
|
||||
roomMembersState = RoomMembersState.Ready(persistentListOf(aRoomMember)),
|
||||
roomAliasSuggestions = emptyList(),
|
||||
currentUserId = A_USER_ID_2,
|
||||
canSendRoomMention = { true },
|
||||
isInThread = false,
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
ResolvedSuggestion.AtRoom,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `processing Mention suggestion with return matching display name but not room if not allowed`() = runTest {
|
||||
val aRoomMember = aRoomMember(userId = UserId("@alice:server.org"), displayName = "ro")
|
||||
|
||||
+4
-4
@@ -51,7 +51,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
@@ -69,6 +68,7 @@ import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
@@ -880,7 +880,7 @@ class TimelinePresenterTest {
|
||||
(0 until 3).map { index ->
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("redacted_$index"),
|
||||
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = RedactedContent),
|
||||
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = aRedactedContent()),
|
||||
)
|
||||
}
|
||||
),
|
||||
@@ -951,14 +951,14 @@ class TimelinePresenterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - PollAnswerSelected event`() = runTest {
|
||||
fun `present - SendPollResponse event`() = runTest {
|
||||
val sendPollResponseAction = FakeSendPollResponseAction()
|
||||
val presenter = createTimelinePresenter(
|
||||
sendPollResponseAction = sendPollResponseAction,
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink.invoke(TimelineEvent.SelectPollAnswer(AN_EVENT_ID, "anAnswerId"))
|
||||
initialState.eventSink.invoke(TimelineEvent.SendPollResponse(AN_EVENT_ID, listOf("anAnswerId")))
|
||||
}
|
||||
delay(1)
|
||||
sendPollResponseAction.verifyExecutionCount(1)
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class TimelineItemPollViewTest : RobolectricTest() {
|
||||
matcher = hasText(answer.text),
|
||||
useUnmergedTree = true,
|
||||
).performClick()
|
||||
eventsRecorder.assertSingle(TimelineEvent.SelectPollAnswer(content.eventId!!, answer.id))
|
||||
eventsRecorder.assertSingle(TimelineEvent.SendPollResponse(content.eventId!!, listOf(answer.id)))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+150
@@ -12,12 +12,21 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -84,6 +93,42 @@ class TimelineItemsFactoryTest {
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `redacted message keeps its thread info`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
timestamp = 0L,
|
||||
content = aRedactedContent(
|
||||
threadInfo = EventThreadInfo.ThreadResponse(A_THREAD_ID),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
var threadInfo: TimelineItemThreadInfo? = null
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = emptyList(),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
threadInfo = awaitItem()
|
||||
.filterIsInstance<TimelineItem.Event>()
|
||||
.firstOrNull()
|
||||
?.threadInfo
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
assertThat(threadInfo).isEqualTo(TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a key verification request is not rendered`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
@@ -128,6 +173,111 @@ class TimelineItemsFactoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a deleted message keeps the summary of the thread it heads`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
content = aRedactedContent(
|
||||
threadInfo = EventThreadInfo.ThreadRoot(
|
||||
summary = ThreadSummary(latestEvent = AsyncData.Uninitialized, numberOfReplies = 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = emptyList(),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
val threadInfo = awaitItem()
|
||||
.filterIsInstance<TimelineItem.Event>()
|
||||
.single()
|
||||
.threadInfo
|
||||
assertThat(threadInfo).isInstanceOf(TimelineItemThreadInfo.ThreadRoot::class.java)
|
||||
assertThat((threadInfo as TimelineItemThreadInfo.ThreadRoot).summary.numberOfReplies).isEqualTo(3)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a sender the timeline knows nothing about is named from the room member list`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
senderProfile = ProfileDetails.Unavailable,
|
||||
content = aMessageContent(body = "A regular message"),
|
||||
),
|
||||
),
|
||||
)
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = listOf(aRoomMember(userId = A_USER_ID, displayName = "Alice")),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
val event = awaitItem().filterIsInstance<TimelineItem.Event>().single()
|
||||
assertThat(event.senderProfile).isEqualTo(
|
||||
ProfileDetails.Ready(
|
||||
displayName = "Alice",
|
||||
displayNameAmbiguous = false,
|
||||
avatarUrl = null,
|
||||
displayedStatus = null,
|
||||
)
|
||||
)
|
||||
assertThat(event.senderAvatar.name).isEqualTo("Alice")
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a sender the room member list knows nothing about keeps the profile the timeline gave`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
senderProfile = ProfileDetails.Unavailable,
|
||||
content = aMessageContent(body = "A regular message"),
|
||||
),
|
||||
),
|
||||
)
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = listOf(aRoomMember(userId = A_USER_ID_2, displayName = "Bob")),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
val event = awaitItem().filterIsInstance<TimelineItem.Event>().single()
|
||||
assertThat(event.senderProfile).isEqualTo(ProfileDetails.Unavailable)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TestScope.groupPositionsOf(timestamps: List<Long>): List<TimelineItemGroupPosition> {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
|
||||
+21
@@ -229,6 +229,25 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() {
|
||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("body"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test create TextMessageType with a formatted body containing only an image falls back to the body`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
val result = sut.create(
|
||||
content = createMessageContent(
|
||||
type = TextMessageType(
|
||||
body = "\uD83D\uDE1C",
|
||||
formatted = FormattedBody(MessageFormat.HTML, "<img src=\"mxc://matrix.org/anImage\" alt=\"\uD83D\uDE1C\" />")
|
||||
)
|
||||
),
|
||||
senderId = A_USER_ID,
|
||||
senderProfile = aProfileDetails(),
|
||||
eventId = AN_EVENT_ID,
|
||||
)
|
||||
assertThat((result as TimelineItemTextContent).formattedBody).isEqualTo(SpannedString("\uD83D\uDE1C"))
|
||||
assertThat(result.htmlDocument).isNull()
|
||||
assertThat(result.plainText).isEqualTo("\uD83D\uDE1C")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test create VideoMessageType`() = runTest {
|
||||
val sut = createTimelineItemContentMessageFactory()
|
||||
@@ -297,6 +316,7 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() {
|
||||
fileSize = 555L,
|
||||
caption = "body.mp4 caption",
|
||||
formattedCaption = SpannedString("formatted"),
|
||||
htmlCaption = "formatted",
|
||||
isEdited = true,
|
||||
duration = 1.minutes,
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
@@ -539,6 +559,7 @@ class TimelineItemContentMessageFactoryTest : RobolectricTest() {
|
||||
fileSize = 888L,
|
||||
caption = "body.jpg caption",
|
||||
formattedCaption = SpannedString("formatted"),
|
||||
htmlCaption = "formatted",
|
||||
isEdited = true,
|
||||
mediaSource = MediaSource(url = "url", json = null),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
|
||||
+19
@@ -15,11 +15,14 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
@@ -190,6 +193,22 @@ class TimelineItemGrouperTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a deleted message that still heads a thread is left out of the removed messages group`() {
|
||||
val threadRoot = aGroupableItem.copy(
|
||||
id = UniqueId("r1"),
|
||||
content = TimelineItemRedactedContent,
|
||||
threadInfo = TimelineItemThreadInfo.ThreadRoot(
|
||||
summary = ThreadSummary(latestEvent = AsyncData.Uninitialized, numberOfReplies = 2),
|
||||
latestEventText = null,
|
||||
),
|
||||
)
|
||||
val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent)
|
||||
val r2 = aGroupableItem.copy(id = UniqueId("r2"), content = TimelineItemRedactedContent)
|
||||
val result = sut.group(listOf(r0, threadRoot, r2))
|
||||
assertThat(result).isEqualTo(listOf(r0, threadRoot, r2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a run of fewer than three redacted events is left untouched`() {
|
||||
val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent)
|
||||
|
||||
+15
-15
@@ -16,23 +16,23 @@ class EmojiTest {
|
||||
@Test
|
||||
fun validEmojis() {
|
||||
// Simple single/multiple single-codepoint emojis per string
|
||||
assertTrue("👍".containsOnlyEmojisInternal())
|
||||
assertTrue("😀".containsOnlyEmojisInternal())
|
||||
assertTrue("🙂🙁".containsOnlyEmojisInternal())
|
||||
assertTrue("👁❤️🍝".containsOnlyEmojisInternal()) // 👁 is a pictographic
|
||||
assertTrue("👨👩👦1️⃣🚀👳🏾♂️🪩".containsOnlyEmojisInternal())
|
||||
assertTrue("🌍🌎🌏".containsOnlyEmojisInternal())
|
||||
assertTrue("👍".containsOnlyEmojis())
|
||||
assertTrue("😀".containsOnlyEmojis())
|
||||
assertTrue("🙂🙁".containsOnlyEmojis())
|
||||
assertTrue("👁❤️🍝".containsOnlyEmojis()) // 👁 is a pictographic
|
||||
assertTrue("👨👩👦1️⃣🚀👳🏾♂️🪩".containsOnlyEmojis())
|
||||
assertTrue("🌍🌎🌏".containsOnlyEmojis())
|
||||
|
||||
// Awkward multi-codepoint graphemes
|
||||
assertTrue("🧑🧑🧒🧒".containsOnlyEmojisInternal())
|
||||
assertTrue("🏴☠".containsOnlyEmojisInternal())
|
||||
assertTrue("👩🏿🔧".containsOnlyEmojisInternal())
|
||||
assertTrue("🧑🧑🧒🧒".containsOnlyEmojis())
|
||||
assertTrue("🏴☠".containsOnlyEmojis())
|
||||
assertTrue("👩🏿🔧".containsOnlyEmojis())
|
||||
|
||||
Assert.assertFalse("".containsOnlyEmojisInternal())
|
||||
Assert.assertFalse(" ".containsOnlyEmojisInternal())
|
||||
Assert.assertFalse("🙂 🙁".containsOnlyEmojisInternal())
|
||||
Assert.assertFalse(" 🙂 🙁 ".containsOnlyEmojisInternal())
|
||||
Assert.assertFalse("Hello".containsOnlyEmojisInternal())
|
||||
Assert.assertFalse("Hello 👋".containsOnlyEmojisInternal())
|
||||
Assert.assertFalse("".containsOnlyEmojis())
|
||||
Assert.assertFalse(" ".containsOnlyEmojis())
|
||||
Assert.assertFalse("🙂 🙁".containsOnlyEmojis())
|
||||
Assert.assertFalse(" 🙂 🙁 ".containsOnlyEmojis())
|
||||
Assert.assertFalse("Hello".containsOnlyEmojis())
|
||||
Assert.assertFalse("Hello 👋".containsOnlyEmojis())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,11 +16,11 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.core.FakeSendHandle
|
||||
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
|
||||
import io.element.android.libraries.mediaplayer.api.MediaPlayer
|
||||
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
@@ -88,7 +88,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||
sender = A_USER_ID,
|
||||
senderProfile = ProfileDetails.Unavailable,
|
||||
timestamp = 9442,
|
||||
content = RedactedContent,
|
||||
content = aRedactedContent(),
|
||||
origin = null,
|
||||
timelineItemDebugInfoProvider = {
|
||||
TimelineItemDebugInfo(
|
||||
|
||||
Reference in New Issue
Block a user