Merge branch 'develop' of https://github.com/vector-im/element-x-android into feature/dla/emojibase_integration
This commit is contained in:
commit
59d7947cc0
2118 changed files with 11048 additions and 4029 deletions
|
|
@ -24,7 +24,7 @@ sealed interface MessagesEvents {
|
|||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
|
||||
data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||
object Dismiss : MessagesEvents
|
||||
data object Dismiss : MessagesEvents
|
||||
}
|
||||
|
||||
enum class InviteDialogAction {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Messages : NavTarget
|
||||
data object Messages : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class MediaViewer(
|
||||
|
|
@ -100,7 +100,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object SendLocation : NavTarget
|
||||
data object SendLocation : NavTarget
|
||||
}
|
||||
|
||||
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
|
|
@ -277,6 +278,7 @@ class MessagesPresenter @AssistedInject constructor(
|
|||
is TimelineItemLocationContent -> AttachmentThumbnailInfo(
|
||||
type = AttachmentThumbnailType.Location,
|
||||
)
|
||||
is TimelineItemPollContent, // TODO Polls: handle reply to
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemRedactedContent,
|
||||
is TimelineItemStateContent,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ 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.core.RoomId
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||
override val values: Sequence<MessagesState>
|
||||
|
|
@ -71,6 +72,7 @@ fun aMessagesState() = MessagesState(
|
|||
selectedEventId = null,
|
||||
emojiProvider = Async.Uninitialized,
|
||||
eventSink = {},
|
||||
selectedEmoji = persistentSetOf(),
|
||||
),
|
||||
reactionSummaryState = ReactionSummaryState(
|
||||
target = null,
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -53,6 +52,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView
|
|||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||
|
|
@ -78,6 +78,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
|
|
@ -100,7 +101,11 @@ fun MessagesView(
|
|||
) {
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
|
||||
AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments)
|
||||
AttachmentStateView(
|
||||
state = state.composerState.attachmentsState,
|
||||
onPreviewAttachments = onPreviewAttachments,
|
||||
onCancel = { state.composerState.eventSink(MessageComposerEvents.CancelSendAttachment) },
|
||||
)
|
||||
|
||||
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
|
||||
|
||||
|
|
@ -135,8 +140,7 @@ fun MessagesView(
|
|||
}
|
||||
|
||||
fun onMoreReactionsClicked(event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -189,7 +193,7 @@ fun MessagesView(
|
|||
onActionSelected = ::onActionSelected,
|
||||
onCustomReactionClicked = { event ->
|
||||
if (event.eventId == null) return@ActionListView
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId))
|
||||
state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event))
|
||||
},
|
||||
onEmojiReactionClicked = ::onEmojiReactionClicked,
|
||||
)
|
||||
|
|
@ -231,7 +235,8 @@ private fun ReinviteDialog(state: MessagesState) {
|
|||
@Composable
|
||||
private fun AttachmentStateView(
|
||||
state: AttachmentsState,
|
||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit
|
||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
AttachmentsState.None -> Unit
|
||||
|
|
@ -244,7 +249,9 @@ private fun AttachmentStateView(
|
|||
is AttachmentsState.Sending.Uploading -> ProgressDialogType.Determinate(state.progress)
|
||||
is AttachmentsState.Sending.Processing -> ProgressDialogType.Indeterminate
|
||||
},
|
||||
text = stringResource(id = CommonStrings.common_sending)
|
||||
text = stringResource(id = CommonStrings.common_sending),
|
||||
isCancellable = true,
|
||||
onDismissRequest = onCancel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ package io.element.android.features.messages.impl.actionlist
|
|||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
sealed interface ActionListEvents {
|
||||
object Clear : ActionListEvents
|
||||
data object Clear : ActionListEvents
|
||||
data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ data class ActionListState(
|
|||
val eventSink: (ActionListEvents) -> Unit,
|
||||
) {
|
||||
sealed interface Target {
|
||||
object None : Target
|
||||
data object None : Target
|
||||
data class Loading(val event: TimelineItem.Event) : Target
|
||||
data class Success(
|
||||
val event: TimelineItem.Event,
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
|
|
@ -73,7 +74,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.text.toSp
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
|
||||
import io.element.android.libraries.designsystem.theme.components.hide
|
||||
|
|
@ -176,7 +177,7 @@ private fun SheetContent(
|
|||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(14.dp))
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
if (state.displayEmojiReactions) {
|
||||
|
|
@ -187,7 +188,7 @@ private fun SheetContent(
|
|||
onCustomReactionClicked = onCustomReactionClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
items(
|
||||
|
|
@ -236,6 +237,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
|||
val textContent = remember(event.content) { formatter.format(event) }
|
||||
|
||||
when (event.content) {
|
||||
is TimelineItemPollContent, // TODO Polls: handle summary
|
||||
is TimelineItemTextBasedContent,
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemEncryptedContent,
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ sealed class TimelineItemAction(
|
|||
@DrawableRes val icon: Int,
|
||||
val destructive: Boolean = false
|
||||
) {
|
||||
object Forward : TimelineItemAction(CommonStrings.action_forward, VectorIcons.Forward)
|
||||
object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy)
|
||||
object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true)
|
||||
object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply)
|
||||
object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
|
||||
object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
|
||||
object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
|
||||
data object Forward : TimelineItemAction(CommonStrings.action_forward, VectorIcons.Forward)
|
||||
data object Copy : TimelineItemAction(CommonStrings.action_copy, VectorIcons.Copy)
|
||||
data object Redact : TimelineItemAction(CommonStrings.action_remove, VectorIcons.Delete, destructive = true)
|
||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, VectorIcons.Reply)
|
||||
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
|
||||
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
|
||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ import androidx.compose.runtime.Immutable
|
|||
|
||||
@Immutable
|
||||
sealed interface AttachmentsPreviewEvents {
|
||||
object SendAttachment : AttachmentsPreviewEvents
|
||||
object ClearSendState : AttachmentsPreviewEvents
|
||||
data object SendAttachment : AttachmentsPreviewEvents
|
||||
data object ClearSendState : AttachmentsPreviewEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,12 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||
@Assisted private val attachment: Attachment,
|
||||
|
|
@ -50,10 +54,18 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
mutableStateOf<SendActionState>(SendActionState.Idle)
|
||||
}
|
||||
|
||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||
when (attachmentsPreviewEvents) {
|
||||
AttachmentsPreviewEvents.SendAttachment -> coroutineScope.sendAttachment(attachment, sendActionState)
|
||||
AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = SendActionState.Idle
|
||||
AttachmentsPreviewEvents.SendAttachment -> ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(attachment, sendActionState)
|
||||
AttachmentsPreviewEvents.ClearSendState -> {
|
||||
ongoingSendAttachmentJob.value?.let {
|
||||
it.cancel()
|
||||
ongoingSendAttachmentJob.value = null
|
||||
}
|
||||
sendActionState.value = SendActionState.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +84,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
is Attachment.Media -> {
|
||||
sendMedia(
|
||||
mediaAttachment = attachment,
|
||||
sendActionState = sendActionState
|
||||
sendActionState = sendActionState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -81,10 +93,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
private suspend fun sendMedia(
|
||||
mediaAttachment: Attachment.Media,
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
) {
|
||||
) = runCatching {
|
||||
val context = coroutineContext
|
||||
val progressCallback = object : ProgressCallback {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
if (context.isActive) {
|
||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
sendActionState.value = SendActionState.Sending.Processing
|
||||
|
|
@ -93,13 +108,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||
compressIfPossible = mediaAttachment.compressIfPossible,
|
||||
progressCallback = progressCallback
|
||||
).fold(
|
||||
onSuccess = {
|
||||
sendActionState.value = SendActionState.Done
|
||||
},
|
||||
onFailure = {
|
||||
sendActionState.value = SendActionState.Failure(it)
|
||||
).getOrThrow()
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
sendActionState.value = SendActionState.Done
|
||||
},
|
||||
onFailure = { error ->
|
||||
if (error is CancellationException) {
|
||||
throw error
|
||||
} else {
|
||||
sendActionState.value = SendActionState.Failure(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,13 +25,13 @@ data class AttachmentsPreviewState(
|
|||
)
|
||||
|
||||
sealed interface SendActionState {
|
||||
object Idle : SendActionState
|
||||
data object Idle : SendActionState
|
||||
sealed interface Sending : SendActionState {
|
||||
object Processing : Sending
|
||||
data object Processing : Sending
|
||||
data class Uploading(val progress: Float) : Sending
|
||||
}
|
||||
|
||||
data class Failure(val error: Throwable) : SendActionState
|
||||
object Done : SendActionState
|
||||
data object Done : SendActionState
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ private fun AttachmentSendStateView(
|
|||
is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress)
|
||||
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
|
||||
},
|
||||
text = stringResource(id = CommonStrings.common_sending)
|
||||
text = stringResource(id = CommonStrings.common_sending),
|
||||
isCancellable = true,
|
||||
onDismissRequest = onDismissClicked,
|
||||
)
|
||||
}
|
||||
is SendActionState.Failure -> {
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
|||
sealed interface ForwardMessagesEvents {
|
||||
data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents
|
||||
// TODO remove to restore multi-selection
|
||||
object RemoveSelectedRoom : ForwardMessagesEvents
|
||||
object ToggleSearchActive : ForwardMessagesEvents
|
||||
data object RemoveSelectedRoom : ForwardMessagesEvents
|
||||
data object ToggleSearchActive : ForwardMessagesEvents
|
||||
data class UpdateQuery(val query: String) : ForwardMessagesEvents
|
||||
object ForwardEvent : ForwardMessagesEvents
|
||||
object ClearError : ForwardMessagesEvents
|
||||
data object ForwardEvent : ForwardMessagesEvents
|
||||
data object ClearError : ForwardMessagesEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogD
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.RadioButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||
|
|
@ -161,7 +161,7 @@ fun ForwardMessagesView(
|
|||
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
|
||||
}
|
||||
)
|
||||
Divider(modifier = Modifier.fillMaxWidth())
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ fun ForwardMessagesView(
|
|||
state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary))
|
||||
}
|
||||
)
|
||||
Divider(modifier = Modifier.fillMaxWidth())
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
package io.element.android.features.messages.impl.media.viewer
|
||||
|
||||
sealed interface MediaViewerEvents {
|
||||
object SaveOnDisk: MediaViewerEvents
|
||||
object Share: MediaViewerEvents
|
||||
object OpenWith: MediaViewerEvents
|
||||
object RetryLoading : MediaViewerEvents
|
||||
object ClearLoadingError : MediaViewerEvents
|
||||
data object SaveOnDisk: MediaViewerEvents
|
||||
data object Share: MediaViewerEvents
|
||||
data object OpenWith: MediaViewerEvents
|
||||
data object RetryLoading : MediaViewerEvents
|
||||
data object ClearLoadingError : MediaViewerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ import androidx.compose.material.icons.filled.OpenInNew
|
|||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -64,6 +61,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarHost
|
||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
|
|
@ -99,15 +97,7 @@ fun MediaViewerView(
|
|||
eventSink = state.eventSink
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
SnackbarHost(snackbarHostState) { data ->
|
||||
Snackbar(
|
||||
snackbarData = data,
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -21,19 +21,20 @@ import io.element.android.libraries.textcomposer.MessageComposerMode
|
|||
|
||||
@Immutable
|
||||
sealed interface MessageComposerEvents {
|
||||
object ToggleFullScreenState : MessageComposerEvents
|
||||
data object ToggleFullScreenState : MessageComposerEvents
|
||||
data class FocusChanged(val hasFocus: Boolean) : MessageComposerEvents
|
||||
data class SendMessage(val message: String) : MessageComposerEvents
|
||||
object CloseSpecialMode : MessageComposerEvents
|
||||
data object CloseSpecialMode : MessageComposerEvents
|
||||
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
|
||||
data class UpdateText(val text: String) : MessageComposerEvents
|
||||
object AddAttachment : MessageComposerEvents
|
||||
object DismissAttachmentMenu : MessageComposerEvents
|
||||
data object AddAttachment : MessageComposerEvents
|
||||
data object DismissAttachmentMenu : MessageComposerEvents
|
||||
sealed interface PickAttachmentSource : MessageComposerEvents {
|
||||
object FromGallery : PickAttachmentSource
|
||||
object FromFiles : PickAttachmentSource
|
||||
object PhotoFromCamera : PickAttachmentSource
|
||||
object VideoFromCamera : PickAttachmentSource
|
||||
object Location : PickAttachmentSource
|
||||
data object FromGallery : PickAttachmentSource
|
||||
data object FromFiles : PickAttachmentSource
|
||||
data object PhotoFromCamera : PickAttachmentSource
|
||||
data object VideoFromCamera : PickAttachmentSource
|
||||
data object Location : PickAttachmentSource
|
||||
}
|
||||
data object CancelSendAttachment : MessageComposerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,13 @@ import io.element.android.libraries.mediaupload.api.MediaSender
|
|||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
|
|
@ -100,6 +104,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
val text: MutableState<String> = rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -112,7 +117,12 @@ class MessageComposerPresenter @Inject constructor(
|
|||
|
||||
LaunchedEffect(attachmentsState.value) {
|
||||
when (val attachmentStateValue = attachmentsState.value) {
|
||||
is AttachmentsState.Sending.Processing -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState)
|
||||
is AttachmentsState.Sending.Processing -> {
|
||||
ongoingSendAttachmentJob.value = localCoroutineScope.sendAttachment(
|
||||
attachmentStateValue.attachments.first(),
|
||||
attachmentsState,
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -169,6 +179,12 @@ class MessageComposerPresenter @Inject constructor(
|
|||
showAttachmentSourcePicker = false
|
||||
// Navigation to the location picker screen is done at the view layer
|
||||
}
|
||||
is MessageComposerEvents.CancelSendAttachment -> {
|
||||
ongoingSendAttachmentJob.value?.let {
|
||||
it.cancel()
|
||||
ongoingSendAttachmentJob.value == null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,13 +228,13 @@ class MessageComposerPresenter @Inject constructor(
|
|||
private fun CoroutineScope.sendAttachment(
|
||||
attachment: Attachment,
|
||||
attachmentState: MutableState<AttachmentsState>,
|
||||
) = launch {
|
||||
when (attachment) {
|
||||
is Attachment.Media -> {
|
||||
) = when (attachment) {
|
||||
is Attachment.Media -> {
|
||||
launch {
|
||||
sendMedia(
|
||||
uri = attachment.localMedia.uri,
|
||||
mimeType = attachment.localMedia.info.mimeType,
|
||||
attachmentState = attachmentState
|
||||
attachmentState = attachmentState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -259,20 +275,27 @@ class MessageComposerPresenter @Inject constructor(
|
|||
uri: Uri,
|
||||
mimeType: String,
|
||||
attachmentState: MutableState<AttachmentsState>,
|
||||
) {
|
||||
) = runCatching {
|
||||
val context = coroutineContext
|
||||
val progressCallback = object : ProgressCallback {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
if (context.isActive) {
|
||||
attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback)
|
||||
.onSuccess {
|
||||
attachmentState.value = AttachmentsState.None
|
||||
}
|
||||
.onFailure {
|
||||
val snackbarMessage = SnackbarMessage(sendAttachmentError(it))
|
||||
snackbarDispatcher.post(snackbarMessage)
|
||||
attachmentState.value = AttachmentsState.None
|
||||
}
|
||||
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback).getOrThrow()
|
||||
}
|
||||
.onSuccess {
|
||||
attachmentState.value = AttachmentsState.None
|
||||
}
|
||||
.onFailure { cause ->
|
||||
attachmentState.value = AttachmentsState.None
|
||||
if (cause is CancellationException) {
|
||||
throw cause
|
||||
} else {
|
||||
val snackbarMessage = SnackbarMessage(sendAttachmentError(cause))
|
||||
snackbarDispatcher.post(snackbarMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ data class MessageComposerState(
|
|||
|
||||
@Immutable
|
||||
sealed interface AttachmentsState {
|
||||
object None : AttachmentsState
|
||||
data object None : AttachmentsState
|
||||
data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState
|
||||
sealed interface Sending : AttachmentsState {
|
||||
data class Processing(val attachments: ImmutableList<Attachment>) : Sending
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ fun MessageComposerView(
|
|||
state.eventSink(MessageComposerEvents.FocusChanged(hasFocus))
|
||||
}
|
||||
|
||||
Box {
|
||||
Box(modifier = modifier) {
|
||||
AttachmentsBottomSheet(
|
||||
state = state,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
|
|
@ -69,8 +69,7 @@ fun MessageComposerView(
|
|||
onAddAttachment = ::onAddAttachment,
|
||||
onFocusChanged = ::onFocusChanged,
|
||||
composerCanSendMessage = state.isSendButtonVisible,
|
||||
composerText = state.text,
|
||||
modifier = modifier
|
||||
composerText = state.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.report
|
|||
|
||||
sealed interface ReportMessageEvents {
|
||||
data class UpdateReason(val reason: String) : ReportMessageEvents
|
||||
object ToggleBlockUser : ReportMessageEvents
|
||||
object Report : ReportMessageEvents
|
||||
object ClearError : ReportMessageEvents
|
||||
data object ToggleBlockUser : ReportMessageEvents
|
||||
data object Report : ReportMessageEvents
|
||||
data object ClearError : ReportMessageEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.timeline
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface TimelineEvents {
|
||||
object LoadMore : TimelineEvents
|
||||
data object LoadMore : TimelineEvents
|
||||
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
|
||||
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ internal fun aTimelineItemEvent(
|
|||
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
|
||||
transactionId: TransactionId? = null,
|
||||
isMine: Boolean = false,
|
||||
senderDisplayName: String = "Sender",
|
||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||
sendState: LocalEventSendState = LocalEventSendState.Sent(eventId),
|
||||
|
|
@ -123,7 +124,7 @@ internal fun aTimelineItemEvent(
|
|||
reactionsState = timelineItemReactions,
|
||||
sentTime = "12:34",
|
||||
isMine = isMine,
|
||||
senderDisplayName = "Sender",
|
||||
senderDisplayName = senderDisplayName,
|
||||
groupPosition = groupPosition,
|
||||
localSendState = sendState,
|
||||
inReplyTo = inReplyTo,
|
||||
|
|
@ -141,11 +142,13 @@ fun aTimelineItemReactions(
|
|||
reactions = buildList {
|
||||
repeat(count) { index ->
|
||||
val key = emojis[index % emojis.size]
|
||||
add(anAggregatedReaction(
|
||||
key = key,
|
||||
count = index + 1,
|
||||
isHighlighted = isHighlighted
|
||||
))
|
||||
add(
|
||||
anAggregatedReaction(
|
||||
key = key,
|
||||
count = index + 1,
|
||||
isHighlighted = isHighlighted
|
||||
)
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.widthIn
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -152,7 +153,7 @@ private fun ContentToPreview(state: BubbleState) {
|
|||
) {
|
||||
MessageEventBubble(
|
||||
state = state,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
// Render the state as a text to better understand the previews
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.widthIn
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -81,13 +82,13 @@ private fun ContentToPreview() {
|
|||
Column {
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = false,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
|
||||
}
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = true,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import androidx.compose.material.icons.filled.Error
|
|||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -63,7 +64,7 @@ fun TimelineEventTimestampView(
|
|||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = MutableInteractionSource()
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -68,9 +69,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
|
|||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
|
|
@ -340,6 +343,8 @@ private fun MessageSenderInformation(
|
|||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = sender,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
|
|
@ -355,11 +360,15 @@ private fun MessageEventBubbleContent(
|
|||
onMessageLongClick: () -> Unit,
|
||||
inReplyToClick: () -> Unit,
|
||||
onTimestampClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones
|
||||
) {
|
||||
val isMediaItem = event.content is TimelineItemImageContent
|
||||
|| event.content is TimelineItemVideoContent
|
||||
|| event.content is TimelineItemLocationContent
|
||||
val timestampPosition = when (event.content) {
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemVideoContent,
|
||||
is TimelineItemLocationContent -> TimestampPosition.Overlay
|
||||
is TimelineItemPollContent -> TimestampPosition.Below
|
||||
else -> TimestampPosition.Default
|
||||
}
|
||||
val replyToDetails = event.inReplyTo as? InReplyTo.Ready
|
||||
|
||||
// Long clicks are not not automatically propagated from a `clickable`
|
||||
|
|
@ -382,96 +391,97 @@ private fun MessageEventBubbleContent(
|
|||
|
||||
@Composable
|
||||
fun ContentAndTimestampView(
|
||||
overlayTimestamp: Boolean,
|
||||
timestampPosition: TimestampPosition,
|
||||
modifier: Modifier = Modifier,
|
||||
contentModifier: Modifier = Modifier,
|
||||
timestampModifier: Modifier = Modifier,
|
||||
) {
|
||||
if (overlayTimestamp) {
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||
.background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
when (timestampPosition) {
|
||||
TimestampPosition.Overlay ->
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||
.background(ElementTheme.colors.bgSubtleSecondary, RoundedCornerShape(10.0.dp))
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
|
||||
)
|
||||
}
|
||||
TimestampPosition.Aligned ->
|
||||
Box(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
TimestampPosition.Below ->
|
||||
Column(modifier) {
|
||||
ContentView(modifier = contentModifier)
|
||||
TimelineEventTimestampView(
|
||||
event = event,
|
||||
onClick = onTimestampClicked,
|
||||
onLongClick = ::onTimestampLongClick,
|
||||
modifier = timestampModifier
|
||||
.align(Alignment.End)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Used only for media items, with no reply to metadata. It displays the contents with no paddings. */
|
||||
@Composable
|
||||
fun SimpleMediaItemLayout(modifier: Modifier = Modifier) {
|
||||
ContentAndTimestampView(overlayTimestamp = true, modifier = modifier)
|
||||
}
|
||||
|
||||
/** Used for every other type of message, groups the different components in a Column with some space between them. */
|
||||
/** Groups the different components in a Column with some space between them. */
|
||||
@Composable
|
||||
fun CommonLayout(
|
||||
inReplyToDetails: InReplyTo.Ready?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var modifierWithPadding: Modifier = Modifier
|
||||
var contentModifier: Modifier = Modifier
|
||||
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
|
||||
if (inReplyToDetails != null) {
|
||||
val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
|
||||
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
|
||||
val text = textForInReplyTo(inReplyToDetails)
|
||||
ReplyToContent(
|
||||
senderName = senderName,
|
||||
text = text,
|
||||
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, start = 8.dp, end = 8.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable(enabled = true, onClick = inReplyToClick),
|
||||
)
|
||||
}
|
||||
val modifierWithPadding = if (isMediaItem) {
|
||||
Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
val contentModifier = if (isMediaItem) {
|
||||
Modifier.clip(RoundedCornerShape(12.dp))
|
||||
} else {
|
||||
if (inReplyToDetails != null) {
|
||||
Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
||||
} else {
|
||||
Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
when {
|
||||
inReplyToDetails != null -> {
|
||||
val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value
|
||||
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
|
||||
val text = textForInReplyTo(inReplyToDetails)
|
||||
ReplyToContent(
|
||||
senderName = senderName,
|
||||
text = text,
|
||||
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, start = 8.dp, end = 8.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable(enabled = true, onClick = inReplyToClick),
|
||||
)
|
||||
if (timestampPosition == TimestampPosition.Overlay) {
|
||||
modifierWithPadding = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||
contentModifier = Modifier.clip(RoundedCornerShape(12.dp))
|
||||
} else {
|
||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 0.dp, bottom = 8.dp)
|
||||
}
|
||||
}
|
||||
timestampPosition != TimestampPosition.Overlay -> {
|
||||
contentModifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp)
|
||||
}
|
||||
}
|
||||
|
||||
ContentAndTimestampView(
|
||||
overlayTimestamp = isMediaItem,
|
||||
timestampPosition = timestampPosition,
|
||||
contentModifier = contentModifier,
|
||||
modifier = modifierWithPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isMediaItem && replyToDetails == null) {
|
||||
SimpleMediaItemLayout()
|
||||
} else {
|
||||
CommonLayout(inReplyToDetails = replyToDetails, modifier = modifier)
|
||||
}
|
||||
CommonLayout(inReplyToDetails = replyToDetails, modifier = bubbleModifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -786,3 +796,45 @@ private fun ContentWithManyReactionsToPreview() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: no need for light/dark variant for this preview
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(
|
||||
senderDisplayName = "a long sender display name to test single line and ellipsis at the end of the line",
|
||||
),
|
||||
isHighlighted = false,
|
||||
canReply = true,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onReactionLongClick = { _, _ -> },
|
||||
onMoreReactionsClick = {},
|
||||
onSwipeToReply = {},
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
// Note: no need for light/dark variant for this preview, we only look at the timestamp position
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
|
||||
TimelineItemEventRow(
|
||||
event = aTimelineItemEvent(content = aTimelineItemPollContent()),
|
||||
isHighlighted = false,
|
||||
canReply = true,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onUserDataClick = {},
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onReactionLongClick = { _, _ -> },
|
||||
onMoreReactionsClick = {},
|
||||
onSwipeToReply = {},
|
||||
onTimestampClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.features.messages.impl.timeline.components
|
||||
|
||||
enum class TimestampPosition {
|
||||
/**
|
||||
* Timestamp should overlay the timeline event content (eg. image).
|
||||
*/
|
||||
Overlay,
|
||||
|
||||
/**
|
||||
* Timestamp should be aligned with the timeline event content if this is possible (eg. text).
|
||||
*/
|
||||
Aligned,
|
||||
|
||||
/**
|
||||
* Timestamp should always be rendered below the timeline event content (eg. poll).
|
||||
*/
|
||||
Below;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Default timestamp position for timeline event contents.
|
||||
*/
|
||||
val Default: TimestampPosition = Aligned
|
||||
}
|
||||
}
|
||||
|
|
@ -57,8 +57,9 @@ fun CustomReactionBottomSheet(
|
|||
) {
|
||||
EmojiPicker(
|
||||
onEmojiSelected = ::onEmojiSelectedDismiss,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
emojiProvider = emojiProvider,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
selectedEmojis = state.selectedEmoji,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
|
||||
sealed interface CustomReactionEvents {
|
||||
data class ShowCustomReactionSheet(val eventId: EventId) : CustomReactionEvents
|
||||
data class ShowCustomReactionSheet(val event: TimelineItem.Event?) : CustomReactionEvents
|
||||
object DismissCustomReactionSheet : CustomReactionEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,20 +29,23 @@ import io.element.android.libraries.architecture.Async
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import kotlinx.coroutines.launch
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import javax.inject.Inject
|
||||
|
||||
class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionState> {
|
||||
|
||||
@Composable
|
||||
override fun present(): CustomReactionState {
|
||||
var selectedEventId by remember { mutableStateOf<EventId?>(null) }
|
||||
var selectedEvent by remember { mutableStateOf<TimelineItem.Event?>(null) }
|
||||
var emojiState: Async<EmojibaseStore> by remember {
|
||||
mutableStateOf(Async.Uninitialized)
|
||||
}
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
fun handleShowCustomReactionSheet(eventId: EventId) {
|
||||
selectedEventId = eventId
|
||||
fun handleShowCustomReactionSheet(event: TimelineItem.Event) {
|
||||
selectedEvent = event
|
||||
emojiState = Async.Loading()
|
||||
localCoroutineScope.launch {
|
||||
emojiState = Async.Success(EmojibaseDatasource().load(context))
|
||||
|
|
@ -50,17 +53,17 @@ class CustomReactionPresenter @Inject constructor() : Presenter<CustomReactionSt
|
|||
}
|
||||
|
||||
fun handleDismissCustomReactionSheet() {
|
||||
selectedEventId = null
|
||||
selectedEvent = null
|
||||
emojiState = Async.Uninitialized
|
||||
}
|
||||
|
||||
fun handleEvents(event: CustomReactionEvents) {
|
||||
when (event) {
|
||||
is CustomReactionEvents.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.eventId)
|
||||
is CustomReactionEvents.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event)
|
||||
is CustomReactionEvents.DismissCustomReactionSheet -> handleDismissCustomReactionSheet()
|
||||
}
|
||||
}
|
||||
|
||||
return CustomReactionState(selectedEventId = selectedEventId, emojiProvider = emojiState, eventSink = ::handleEvents)
|
||||
val selectedEmoji = selectedEvent?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet()
|
||||
return CustomReactionState(selectedEventId = selectedEvent?.eventId, emojiProvider = emojiState, selectedEmoji = selectedEmoji, eventSink = ::handleEvents)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ package io.element.android.features.messages.impl.timeline.components.customreac
|
|||
import io.element.android.emojibasebindings.EmojibaseStore
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
|
||||
data class CustomReactionState(
|
||||
val selectedEventId: EventId?,
|
||||
val emojiProvider: Async<EmojibaseStore>,
|
||||
val selectedEmoji: ImmutableSet<String>,
|
||||
val eventSink: (CustomReactionEvents) -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package io.element.android.features.messages.impl.timeline.components.customreaction
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -31,6 +32,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
|
|
@ -41,6 +43,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.emojibasebindings.Emoji
|
||||
|
|
@ -52,18 +55,21 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
|||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun EmojiPicker(
|
||||
onEmojiSelected: (Emoji) -> Unit,
|
||||
emojiProvider: EmojibaseStore,
|
||||
modifier: Modifier = Modifier,
|
||||
emojiProvider: EmojibaseStore,
|
||||
selectedEmojis: ImmutableSet<String>,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val categories = remember { emojiProvider.categories }
|
||||
val pagerState = rememberPagerState()
|
||||
val pagerState = rememberPagerState(pageCount = { emojiProvider.categories.size })
|
||||
Column(modifier) {
|
||||
TabRow(
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
|
|
@ -95,12 +101,20 @@ fun EmojiPicker(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
columns = GridCells.Adaptive(minSize = 40.dp),
|
||||
contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
|
||||
items(emojis, key = { it.unicode }) { item ->
|
||||
val backgroundColor = if (selectedEmojis.contains(item.unicode)) {
|
||||
ElementTheme.colors.bgActionPrimaryRest
|
||||
} else {
|
||||
Color.Transparent
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(backgroundColor, CircleShape)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = { onEmojiSelected(item) },
|
||||
|
|
@ -136,7 +150,8 @@ internal fun EmojiPickerDarkPreview() {
|
|||
private fun ContentToPreview() {
|
||||
EmojiPicker(
|
||||
onEmojiSelected = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
emojiProvider = EmojibaseDatasource().load(LocalContext.current),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
selectedEmojis = persistentSetOf("😀", "😄", "😃")
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
|
|
@ -90,5 +91,10 @@ fun TimelineItemEventContentView(
|
|||
content = content,
|
||||
modifier = modifier
|
||||
)
|
||||
is TimelineItemPollContent -> TimelineItemPollView(
|
||||
content = content,
|
||||
onAnswerSelected = {},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.features.messages.impl.timeline.components.event
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider
|
||||
import io.element.android.features.poll.api.PollContentView
|
||||
import io.element.android.libraries.designsystem.preview.DayNightPreviews
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun TimelineItemPollView(
|
||||
content: TimelineItemPollContent,
|
||||
onAnswerSelected: (PollAnswer) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PollContentView(
|
||||
question = content.question,
|
||||
answerItems = content.answerItems.toImmutableList(),
|
||||
pollKind = content.pollKind,
|
||||
isPollEnded = content.isEnded,
|
||||
onAnswerSelected = onAnswerSelected,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) =
|
||||
ElementPreview {
|
||||
TimelineItemPollView(
|
||||
content = content,
|
||||
onAnswerSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -16,10 +16,6 @@
|
|||
|
||||
package io.element.android.features.messages.impl.timeline.components.event
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify.PHONE_NUMBERS
|
||||
import android.text.util.Linkify.WEB_URLS
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -28,20 +24,16 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.theme.LinkColor
|
||||
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||
|
||||
@Composable
|
||||
|
|
@ -69,15 +61,11 @@ fun TimelineItemTextView(
|
|||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
val linkStyle = SpanStyle(
|
||||
color = LinkColor,
|
||||
)
|
||||
val styledText = remember(content.body) {
|
||||
content.body.linkify(linkStyle) + extraPadding.getStr(16.sp).toAnnotatedString()
|
||||
val textWithPadding = remember(content.body) {
|
||||
content.body + extraPadding.getStr(16.sp).toAnnotatedString()
|
||||
}
|
||||
ClickableLinkText(
|
||||
text = styledText,
|
||||
linkAnnotationTag = "URL",
|
||||
text = textWithPadding,
|
||||
onClick = onTextClicked,
|
||||
onLongClick = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
|
|
@ -86,31 +74,6 @@ fun TimelineItemTextView(
|
|||
}
|
||||
}
|
||||
|
||||
private fun String.linkify(
|
||||
linkStyle: SpanStyle,
|
||||
) = buildAnnotatedString {
|
||||
append(this@linkify)
|
||||
val spannable = SpannableString(this@linkify)
|
||||
LinkifyCompat.addLinks(spannable, WEB_URLS or PHONE_NUMBERS)
|
||||
|
||||
val spans = spannable.getSpans(0, spannable.length, URLSpan::class.java)
|
||||
for (span in spans) {
|
||||
val start = spannable.getSpanStart(span)
|
||||
val end = spannable.getSpanEnd(span)
|
||||
addStyle(
|
||||
start = start,
|
||||
end = end,
|
||||
style = linkStyle,
|
||||
)
|
||||
addStringAnnotation(
|
||||
tag = "URL",
|
||||
annotation = span.url,
|
||||
start = start,
|
||||
end = end
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun TimelineItemTextViewLightPreview(@PreviewParameter(TimelineItemTextBasedContentProvider::class) content: TimelineItemTextBasedContent) =
|
||||
|
|
@ -125,7 +88,7 @@ internal fun TimelineItemTextViewDarkPreview(@PreviewParameter(TimelineItemTextB
|
|||
fun ContentToPreview(content: TimelineItemTextBasedContent) {
|
||||
TimelineItemTextView(
|
||||
content = content,
|
||||
interactionSource = MutableInteractionSource(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
extraPadding = ExtraPadding(nbChars = 8),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,5 +50,7 @@ open class DocumentProvider : PreviewParameterProvider<Document> {
|
|||
// "<pre>pre</pre>",
|
||||
"<mx-reply><blockquote><a href=\\\"https://matrix.to/#/!roomId/\$eventId?via=matrix.org\\\">In reply to</a> " +
|
||||
"<a href=\\\"https://matrix.to/#/@alice:matrix.org\\\">@alice:matrix.org</a><br>original message</blockquote></mx-reply>reply",
|
||||
"<ol><li>Testing <a href='#'>link</a> item.</li><li>And <a href='#'>another</a> item.</li></ol>",
|
||||
"<ul><li>Testing <a href='#'>link</a> item.</li><li>And <a href='#'>another</a> item.</li></ul>",
|
||||
).map { Jsoup.parse(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalLayoutApi::class)
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.components.html
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -29,6 +34,8 @@ import androidx.compose.material3.ColorScheme
|
|||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
|
@ -46,7 +53,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
|
|
@ -95,18 +101,15 @@ private fun HtmlBody(
|
|||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) = FlowRow(
|
||||
mainAxisSpacing = 2.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.Start),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.Top),
|
||||
) {
|
||||
var sameRow = true
|
||||
while (sameRow && nodes.hasNext()) {
|
||||
when (val node = nodes.next()) {
|
||||
is TextNode -> {
|
||||
if (!node.isBlank) {
|
||||
Text(
|
||||
text = node.text(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
ClickableLinkText(text = node.text(), interactionSource = interactionSource)
|
||||
}
|
||||
}
|
||||
is Element -> {
|
||||
|
|
@ -399,22 +402,14 @@ private fun HtmlOrderedList(
|
|||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) {
|
||||
var number = 1
|
||||
val delimiter = "."
|
||||
HtmlListItems(
|
||||
list = orderedList,
|
||||
marker = { index -> "$index$delimiter" },
|
||||
modifier = modifier,
|
||||
onTextClicked = onTextClicked, onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
val text = buildAnnotatedString {
|
||||
append("${number++}$delimiter ${it.text()}")
|
||||
}
|
||||
HtmlText(
|
||||
text = text, onClick = onTextClicked,
|
||||
onLongClick = onTextLongClicked, interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -428,42 +423,52 @@ private fun HtmlUnorderedList(
|
|||
val marker = "・"
|
||||
HtmlListItems(
|
||||
list = unorderedList,
|
||||
marker = { marker },
|
||||
modifier = modifier,
|
||||
onTextClicked = onTextClicked, onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
val text = buildAnnotatedString {
|
||||
append("$marker ${it.text()}")
|
||||
}
|
||||
HtmlText(
|
||||
text = text, onClick = onTextClicked,
|
||||
onLongClick = onTextLongClicked, interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HtmlListItems(
|
||||
list: Element,
|
||||
marker: (Int) -> String,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier = Modifier,
|
||||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
content: @Composable (node: TextNode) -> Unit = {}
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
for (node in list.children()) {
|
||||
for (innerNode in node.childNodes()) {
|
||||
when (innerNode) {
|
||||
is TextNode -> {
|
||||
if (!innerNode.isBlank) content(innerNode)
|
||||
for ((index, node) in list.children().withIndex()) {
|
||||
val areAllChildrenInline = node.childNodes().all { it is TextNode || it is Element && it.isInline() }
|
||||
if (areAllChildrenInline) {
|
||||
val text = buildAnnotatedString {
|
||||
append("${marker(index + 1)} ")
|
||||
appendInlineChildrenElements(node.childNodes(), MaterialTheme.colorScheme)
|
||||
}
|
||||
HtmlText(text = text, interactionSource = remember { MutableInteractionSource() })
|
||||
} else {
|
||||
for (innerNode in node.childNodes()) {
|
||||
when (innerNode) {
|
||||
is TextNode -> {
|
||||
if (!innerNode.isBlank) {
|
||||
val text = buildAnnotatedString {
|
||||
append("${marker(index + 1)} ")
|
||||
}
|
||||
HtmlText(
|
||||
text = text, onClick = onTextClicked,
|
||||
onLongClick = onTextLongClicked, interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
is Element -> HtmlBlock(
|
||||
element = innerNode,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
onTextClicked = onTextClicked, onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
is Element -> HtmlBlock(
|
||||
element = innerNode,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
onTextClicked = onTextClicked, onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -576,8 +581,7 @@ private fun HtmlText(
|
|||
) {
|
||||
val inlineContentMap = persistentMapOf<String, InlineTextContent>()
|
||||
ClickableLinkText(
|
||||
text = text,
|
||||
linkAnnotationTag = "URL",
|
||||
annotatedString = text,
|
||||
style = style,
|
||||
modifier = modifier,
|
||||
inlineContent = inlineContentMap,
|
||||
|
|
@ -599,5 +603,5 @@ internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class)
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(document: Document) {
|
||||
HtmlDocument(document, MutableInteractionSource())
|
||||
HtmlDocument(document, remember { MutableInteractionSource() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ import io.element.android.features.messages.impl.timeline.model.AggregatedReacti
|
|||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface ReactionSummaryEvents {
|
||||
object Clear : ReactionSummaryEvents
|
||||
data object Clear : ReactionSummaryEvents
|
||||
data class ShowReactionSummary(val eventId: EventId, val reactions: List<AggregatedReaction>, val selectedKey: String) : ReactionSummaryEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ private fun SheetContent(
|
|||
summary.reactions.indexOfFirst { it.key == selectedReactionKey }
|
||||
}
|
||||
}
|
||||
val pagerState = rememberPagerState(initialPage = selectedReactionIndex)
|
||||
val pagerState = rememberPagerState(initialPage = selectedReactionIndex, pageCount = { summary.reactions.size })
|
||||
val reactionListState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
|
|
@ -123,7 +123,8 @@ private fun SheetContent(
|
|||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
LazyRow(state = reactionListState,
|
||||
LazyRow(
|
||||
state = reactionListState,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(start = 12.dp, end = 12.dp, bottom = 12.dp)
|
||||
) {
|
||||
|
|
@ -140,7 +141,7 @@ private fun SheetContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
HorizontalPager(state = pagerState, pageCount = summary.reactions.size) { page ->
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
LazyColumn(modifier = Modifier.fillMaxHeight()) {
|
||||
items(summary.reactions[page].senders) { sender ->
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|||
|
||||
sealed interface RetrySendMenuEvents {
|
||||
data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents
|
||||
object RetrySend : RetrySendMenuEvents
|
||||
object RemoveFailed : RetrySendMenuEvents
|
||||
object Dismiss: RetrySendMenuEvents
|
||||
data object RetrySend : RetrySendMenuEvents
|
||||
data object RemoveFailed : RetrySendMenuEvents
|
||||
data object Dismiss: RetrySendMenuEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class TimelineItemsFactory @Inject constructor(
|
|||
this.timelineItems.emit(result)
|
||||
}
|
||||
|
||||
private fun buildAndCacheItem(
|
||||
private suspend fun buildAndCacheItem(
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
index: Int
|
||||
): TimelineItem? {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
|
|
@ -35,6 +36,7 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
private val messageFactory: TimelineItemContentMessageFactory,
|
||||
private val redactedMessageFactory: TimelineItemContentRedactedFactory,
|
||||
private val stickerFactory: TimelineItemContentStickerFactory,
|
||||
private val pollFactory: TimelineItemContentPollFactory,
|
||||
private val utdFactory: TimelineItemContentUTDFactory,
|
||||
private val roomMembershipFactory: TimelineItemContentRoomMembershipFactory,
|
||||
private val profileChangeFactory: TimelineItemContentProfileChangeFactory,
|
||||
|
|
@ -43,7 +45,7 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
private val failedToParseStateFactory: TimelineItemContentFailedToParseStateFactory
|
||||
) {
|
||||
|
||||
fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
suspend fun create(eventTimelineItem: EventTimelineItem): TimelineItemEventContent {
|
||||
return when (val itemContent = eventTimelineItem.content) {
|
||||
is FailedToParseMessageLikeContent -> failedToParseMessageFactory.create(itemContent)
|
||||
is FailedToParseStateContent -> failedToParseStateFactory.create(itemContent)
|
||||
|
|
@ -53,6 +55,7 @@ class TimelineItemContentFactory @Inject constructor(
|
|||
is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem)
|
||||
is StateContent -> stateFactory.create(eventTimelineItem)
|
||||
is StickerContent -> stickerFactory.create(itemContent)
|
||||
is PollContent -> pollFactory.create(itemContent)
|
||||
is UnableToDecryptContent -> utdFactory.create(itemContent)
|
||||
is UnknownContent -> TimelineItemUnknownContent
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.features.messages.impl.timeline.factories.event
|
||||
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.poll.isDisclosed
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineItemContentPollFactory @Inject constructor(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) {
|
||||
|
||||
suspend fun create(content: PollContent): TimelineItemEventContent {
|
||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
||||
|
||||
// Todo Move this computation to the matrix rust sdk
|
||||
val totalVoteCount = content.votes.flatMap { it.value }.size
|
||||
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
|
||||
val isEndedPoll = content.endTime != null
|
||||
val winnerIds = if (!isEndedPoll) {
|
||||
emptyList()
|
||||
} else {
|
||||
content.answers
|
||||
.map { answer -> answer.id }
|
||||
.groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count
|
||||
.maxByOrNull { (votes, _) -> votes } // Keep max voted answers
|
||||
?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted
|
||||
?.value
|
||||
.orEmpty()
|
||||
}
|
||||
val answerItems = content.answers.map { answer ->
|
||||
val answerVoteCount = content.votes[answer.id]?.size ?: 0
|
||||
val isSelected = answer.id in myVotes
|
||||
val isWinner = answer.id in winnerIds
|
||||
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
|
||||
PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = isSelected,
|
||||
isEnabled = !isEndedPoll,
|
||||
isWinner = isWinner,
|
||||
isDisclosed = content.kind.isDisclosed || isEndedPoll,
|
||||
votesCount = answerVoteCount,
|
||||
percentage = percentage,
|
||||
)
|
||||
}
|
||||
|
||||
return TimelineItemPollContent(
|
||||
question = content.question,
|
||||
answerItems = answerItems,
|
||||
votes = content.votes,
|
||||
pollKind = content.kind,
|
||||
isEnded = isEndedPoll,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ class TimelineItemEventFactory @Inject constructor(
|
|||
private val matrixClient: MatrixClient,
|
||||
) {
|
||||
|
||||
fun create(
|
||||
suspend fun create(
|
||||
currentTimelineItem: MatrixTimelineItem.Event,
|
||||
index: Int,
|
||||
timelineItems: List<MatrixTimelineItem>,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
|
||||
|
|
@ -33,6 +34,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||
|
|
@ -55,6 +57,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean {
|
|||
is TimelineItemVideoContent,
|
||||
is TimelineItemAudioContent,
|
||||
is TimelineItemLocationContent,
|
||||
is TimelineItemPollContent,
|
||||
TimelineItemRedactedContent,
|
||||
TimelineItemUnknownContent -> false
|
||||
is TimelineItemProfileChangeContent,
|
||||
|
|
@ -74,6 +77,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
|||
is MessageContent,
|
||||
RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
is UnableToDecryptContent -> true
|
||||
is FailedToParseStateContent,
|
||||
is ProfileChangeContent,
|
||||
|
|
|
|||
|
|
@ -40,22 +40,22 @@ sealed interface TimelineItemGroupPosition {
|
|||
/**
|
||||
* The event is part of a group of events from the same sender and is the first sent Event.
|
||||
*/
|
||||
object First : TimelineItemGroupPosition
|
||||
data object First : TimelineItemGroupPosition
|
||||
|
||||
/**
|
||||
* The event is part of a group of events from the same sender and is neither the first nor the last sent Event.
|
||||
*/
|
||||
object Middle : TimelineItemGroupPosition
|
||||
data object Middle : TimelineItemGroupPosition
|
||||
|
||||
/**
|
||||
* The event is part of a group of events from the same sender and is the last sent Event.
|
||||
*/
|
||||
object Last : TimelineItemGroupPosition
|
||||
data object Last : TimelineItemGroupPosition
|
||||
|
||||
/**
|
||||
* The event is not part of a group of events. Sender of previous event is different, and sender of next event is different.
|
||||
*/
|
||||
object None : TimelineItemGroupPosition
|
||||
data object None : TimelineItemGroupPosition
|
||||
|
||||
/**
|
||||
* Return true if the previous sender of the event is a different sender.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ fun TimelineItemEventContent.canReact(): Boolean =
|
|||
is TimelineItemFileContent,
|
||||
is TimelineItemImageContent,
|
||||
is TimelineItemLocationContent,
|
||||
is TimelineItemPollContent,
|
||||
is TimelineItemVideoContent -> true
|
||||
is TimelineItemStateContent,
|
||||
is TimelineItemRedactedContent,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
data class TimelineItemPollContent(
|
||||
val question: String,
|
||||
val answerItems: List<PollAnswerItem>,
|
||||
val votes: Map<String, List<UserId>>,
|
||||
val pollKind: PollKind,
|
||||
val isEnded: Boolean,
|
||||
) : TimelineItemEventContent {
|
||||
override val type: String = "TimelineItemPollContent"
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.features.messages.impl.timeline.model.event
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.poll.api.aPollAnswerItemList
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineItemPollContent> {
|
||||
override val values: Sequence<TimelineItemPollContent>
|
||||
get() = sequenceOf(
|
||||
aTimelineItemPollContent(),
|
||||
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
||||
)
|
||||
}
|
||||
|
||||
fun aTimelineItemPollContent(): TimelineItemPollContent {
|
||||
return TimelineItemPollContent(
|
||||
pollKind = PollKind.Disclosed,
|
||||
question = "What type of food should we have at the party?",
|
||||
answerItems = aPollAnswerItemList(),
|
||||
isEnded = false,
|
||||
votes = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
|
|
@ -47,6 +48,7 @@ class MessageSummaryFormatterImpl @Inject constructor(
|
|||
is TimelineItemLocationContent -> context.getString(CommonStrings.common_shared_location)
|
||||
is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt)
|
||||
is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed)
|
||||
is TimelineItemPollContent, // Todo Polls: handle summary
|
||||
is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event)
|
||||
is TimelineItemImageContent -> context.getString(CommonStrings.common_image)
|
||||
is TimelineItemVideoContent -> context.getString(CommonStrings.common_video)
|
||||
|
|
|
|||
|
|
@ -5,19 +5,43 @@
|
|||
<item quantity="few">"%1$d změny místnosti"</item>
|
||||
<item quantity="other">"%1$d změn místnosti"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_more_reactions">
|
||||
<item quantity="one">"%1$d další"</item>
|
||||
<item quantity="few">"%1$d další"</item>
|
||||
<item quantity="other">"%1$d dalších"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Fotoaparát"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Vyfotit"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Natočit video"</string>
|
||||
<string name="screen_room_attachment_source_files">"Příloha"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Knihovna fotografií a videí"</string>
|
||||
<string name="screen_room_attachment_source_location">"Poloha"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Hlasování"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Historie zpráv je momentálně v této místnosti nedostupná"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodařilo se načíst údaje o uživateli"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Chtěli byste je pozvat zpět?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"V tomto chatu jste sami"</string>
|
||||
<string name="screen_room_message_copied">"Zpráva zkopírována"</string>
|
||||
<string name="screen_room_no_permission_to_post">"Nemáte oprávnění zveřejňovat příspěvky v této místnosti"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Povolit vlastní nastavení"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Zapnutím této funkce přepíšete výchozí nastavení"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Upozornit mě v tomto chatu na"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Můžete změnit ve vašem %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"globální nastavení"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Výchozí nastavení"</string>
|
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Odebrat vlastní nastavení"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Při načítání nastavení oznámení došlo k chybě."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Nastavení režimu se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Všechny zprávy"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Pouze zmínky a klíčová slova"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"V této místnosti mě upozornit na"</string>
|
||||
<string name="screen_room_reactions_show_less">"Zobrazit méně"</string>
|
||||
<string name="screen_room_reactions_show_more">"Zobrazit více"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Odeslat znovu"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Vaši zprávu se nepodařilo odeslat"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Přidat emoji"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Zobrazit méně"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Nahrání média se nezdařilo, zkuste to prosím znovu."</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Odstranit"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -10,16 +10,32 @@
|
|||
<string name="screen_room_attachment_source_files">"Anhang"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Foto- & Video-Bibliothek"</string>
|
||||
<string name="screen_room_attachment_source_location">"Standort"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Umfrage"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Der Nachrichtenverlauf ist in diesem Raum derzeit nicht verfügbar"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Benutzerdetails konnten nicht abgerufen werden"</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>
|
||||
<string name="screen_room_message_copied">"Nachricht kopiert"</string>
|
||||
<string name="screen_room_no_permission_to_post">"Du bist keine Berechtigung, um in diesem Raum zu posten"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Benutzerdefinierte Einstellung zulassen"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Das Aktivieren dieser Option wird die Standardeinstellungen überschreiben."</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Benachrichtige mich in diesem Chat für"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Du kannst es in deinem %1$s ändern."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Globale Einstellungen"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Standardeinstellung"</string>
|
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Benutzerdefinierte Einstellung entfernen"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Beim Laden der Benachrichtigungseinstellungen ist ein Fehler aufgetreten."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Wiederherstellung des Standardmodus fehlgeschlagen. Bitte versuche es erneut."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Fehler beim Einstellen des Modus. Bitte versuche es erneut."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Alle Nachrichten"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Nur Erwähnungen und Schlüsselwörter"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"In diesem Raum, benachrichtige mich für"</string>
|
||||
<string name="screen_room_reactions_show_less">"Weniger anzeigen"</string>
|
||||
<string name="screen_room_reactions_show_more">"Mehr anzeigen"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Erneut senden"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Ihre Nachricht konnte nicht gesendet werden"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Emoji hinzufügen"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Weniger anzeigen"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuche es erneut."</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Entfernen"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="one">"%1$d изменение в комнате"</item>
|
||||
<item quantity="few">"%1$d изменения в комнате"</item>
|
||||
<item quantity="many">"%1$d изменений в комнате"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_more_reactions">
|
||||
<item quantity="one">"И ещё %1$d"</item>
|
||||
<item quantity="few">"И ещё %1$d"</item>
|
||||
<item quantity="many">"И ещё %1$d"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"Камера"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"Сделать фото"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"Записать видео"</string>
|
||||
<string name="screen_room_attachment_source_files">"Вложение"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Фото и видео"</string>
|
||||
<string name="screen_room_attachment_source_location">"Местоположение"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Опрос"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"В настоящее время история сообщений недоступна в этой комнате"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не удалось получить данные о пользователе"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Хотите пригласить их снова?"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"Вы одни в этой комнате"</string>
|
||||
<string name="screen_room_message_copied">"Сообщение скопировано"</string>
|
||||
<string name="screen_room_no_permission_to_post">"У вас нет разрешения публиковать сообщения в этой комнате"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom">"Разрешить пользовательские настройки"</string>
|
||||
<string name="screen_room_notification_settings_allow_custom_footnote">"Включение этого параметра отменяет настройки по умолчанию"</string>
|
||||
<string name="screen_room_notification_settings_custom_settings_title">"Уведомить меня в этом чате"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote">"Вы можете изменить его в своем %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"Основные Настройки"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Настройка по умолчанию"</string>
|
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Удалить пользовательскую настройку"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Произошла ошибка при загрузке настроек уведомлений."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Не удалось восстановить режим по умолчанию, попробуйте еще раз."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Не удалось настроить режим, попробуйте еще раз."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Все сообщения"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Только упоминания и ключевые слова"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"В этой комнате уведомить меня о"</string>
|
||||
<string name="screen_room_reactions_show_less">"Показать меньше"</string>
|
||||
<string name="screen_room_reactions_show_more">"Показать больше"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Отправить снова"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"Не удалось отправить ваше сообщение"</string>
|
||||
<string name="screen_room_timeline_add_reaction">"Добавить эмодзи"</string>
|
||||
<string name="screen_room_timeline_less_reactions">"Показать меньше"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте еще раз."</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"Удалить"</string>
|
||||
</resources>
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<string name="screen_room_attachment_source_files">"Príloha"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Knižnica fotografií a videí"</string>
|
||||
<string name="screen_room_attachment_source_location">"Poloha"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Anketa"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"História správ v tejto miestnosti nie je momentálne k dispozícii"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nepodarilo sa získať údaje o používateľovi"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Chceli by ste ich pozvať späť?"</string>
|
||||
|
|
@ -28,11 +29,13 @@
|
|||
<string name="screen_room_notification_settings_default_setting_footnote">"Môžete to zmeniť vo svojich %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"všeobecných nastaveniach"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Predvolené nastavenie"</string>
|
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Odstrániť vlastné nastavenie"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"Pri načítavaní nastavení oznámení došlo k chybe."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Nepodarilo sa nastaviť režim, skúste to prosím znova."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"Všetky správy"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Iba zmienky a kľúčové slová"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"V tejto miestnosti ma upozorniť na"</string>
|
||||
<string name="screen_room_reactions_show_less">"Zobraziť menej"</string>
|
||||
<string name="screen_room_reactions_show_more">"Zobraziť viac"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Odoslať znova"</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<plurals name="room_timeline_state_changes">
|
||||
<item quantity="other">"%1$d 個聊天室變更"</item>
|
||||
</plurals>
|
||||
<plurals name="screen_room_timeline_more_reactions">
|
||||
<item quantity="other">"還有 %1$d 個"</item>
|
||||
</plurals>
|
||||
<string name="screen_room_attachment_source_camera">"照相機"</string>
|
||||
<string name="screen_room_attachment_source_camera_photo">"拍照"</string>
|
||||
<string name="screen_room_attachment_source_camera_video">"錄影"</string>
|
||||
<string name="screen_room_attachment_source_files">"附件"</string>
|
||||
<string name="screen_room_attachment_source_location">"位置"</string>
|
||||
<string name="screen_room_attachment_source_poll">"投票"</string>
|
||||
<string name="screen_room_invite_again_alert_title">"此聊天室只有您一個人"</string>
|
||||
<string name="screen_room_message_copied">"訊息已複製"</string>
|
||||
<string name="screen_room_no_permission_to_post">"您沒有權限在此聊天室傳送訊息"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"全域設定"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"預設"</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"無法重設為預設模式,請再試一次。"</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"無法設定模式,請再試一次。"</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"所有訊息"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"只限提及與關鍵字"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"重傳"</string>
|
||||
<string name="screen_room_retry_send_menu_title">"無法傳送您的訊息"</string>
|
||||
<string name="screen_room_retry_send_menu_remove_action">"移除"</string>
|
||||
</resources>
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="screen_room_attachment_source_files">"Attachment"</string>
|
||||
<string name="screen_room_attachment_source_gallery">"Photo & Video Library"</string>
|
||||
<string name="screen_room_attachment_source_location">"Location"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Poll"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Message history is currently unavailable in this room"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Would you like to invite them back?"</string>
|
||||
|
|
@ -25,11 +26,13 @@
|
|||
<string name="screen_room_notification_settings_default_setting_footnote">"You can change it in your %1$s."</string>
|
||||
<string name="screen_room_notification_settings_default_setting_footnote_content_link">"global settings"</string>
|
||||
<string name="screen_room_notification_settings_default_setting_title">"Default setting"</string>
|
||||
<string name="screen_room_notification_settings_edit_remove_setting">"Remove custom setting"</string>
|
||||
<string name="screen_room_notification_settings_error_loading_settings">"An error occurred while loading notification settings."</string>
|
||||
<string name="screen_room_notification_settings_error_restoring_default">"Failed restoring the default mode, please try again."</string>
|
||||
<string name="screen_room_notification_settings_error_setting_mode">"Failed setting the mode, please try again."</string>
|
||||
<string name="screen_room_notification_settings_mode_all_messages">"All messages"</string>
|
||||
<string name="screen_room_notification_settings_mode_mentions_and_keywords">"Mentions and Keywords only"</string>
|
||||
<string name="screen_room_notification_settings_room_custom_settings_title">"In this room, notify me for"</string>
|
||||
<string name="screen_room_reactions_show_less">"Show less"</string>
|
||||
<string name="screen_room_reactions_show_more">"Show more"</string>
|
||||
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.messages.fixtures.aMessageEvent
|
||||
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.InviteDialogAction
|
||||
|
|
@ -69,6 +68,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
|||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.consumeItemsUntilTimeout
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
|
|
|
|||
|
|
@ -94,6 +94,21 @@ class AttachmentsPreviewPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - dismissing the progress dialog stops media upload`() = runTest {
|
||||
val presenter = anAttachmentsPreviewPresenter()
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
private fun anAttachmentsPreviewPresenter(
|
||||
localMedia: LocalMedia = aLocalMedia(
|
||||
uri = mockMediaUrl,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
|
|||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseStateFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentMessageFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentProfileChangeFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRedactedFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentRoomMembershipFactory
|
||||
|
|
@ -35,6 +36,7 @@ import io.element.android.features.messages.impl.timeline.util.FileExtensionExtr
|
|||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||
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.tests.testutils.testCoroutineDispatchers
|
||||
|
|
@ -42,6 +44,7 @@ import kotlinx.coroutines.test.TestScope
|
|||
|
||||
internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||
val timelineEventFormatter = aTimelineEventFormatter()
|
||||
val matrixClient = FakeMatrixClient()
|
||||
return TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
eventItemFactory = TimelineItemEventFactory(
|
||||
|
|
@ -49,14 +52,15 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
|||
messageFactory = TimelineItemContentMessageFactory(FakeFileSizeFormatter(), FileExtensionExtractorWithoutValidation()),
|
||||
redactedMessageFactory = TimelineItemContentRedactedFactory(),
|
||||
stickerFactory = TimelineItemContentStickerFactory(),
|
||||
pollFactory = TimelineItemContentPollFactory(matrixClient, FakeFeatureFlagService()),
|
||||
utdFactory = TimelineItemContentUTDFactory(),
|
||||
roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter),
|
||||
profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter),
|
||||
stateFactory = TimelineItemContentStateFactory(timelineEventFormatter),
|
||||
failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(),
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory()
|
||||
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
|
||||
),
|
||||
matrixClient = FakeMatrixClient(),
|
||||
matrixClient = matrixClient,
|
||||
),
|
||||
virtualItemFactory = TimelineItemVirtualFactory(
|
||||
daySeparatorFactory = TimelineItemDaySeparatorFactory(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
|
|
@ -55,6 +54,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender
|
|||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -500,6 +500,23 @@ class MessageComposerPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - CancelSendAttachment stops media upload`() = runTest {
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
||||
val sendingState = awaitItem()
|
||||
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending.Processing::class.java)
|
||||
sendingState.eventSink(MessageComposerEvents.CancelSendAttachment)
|
||||
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.None)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) {
|
||||
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
||||
skipItems(skipCount)
|
||||
|
|
@ -534,5 +551,6 @@ fun anEditMode(
|
|||
message: String = A_MESSAGE,
|
||||
transactionId: TransactionId? = null,
|
||||
) = MessageComposerMode.Edit(eventId, message, transactionId)
|
||||
|
||||
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE)
|
||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
|
|
@ -38,11 +40,27 @@ class CustomReactionPresenterTests {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedEventId).isNull()
|
||||
|
||||
initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(AN_EVENT_ID))
|
||||
initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(aTimelineItemEvent(eventId = AN_EVENT_ID)))
|
||||
assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID)
|
||||
|
||||
initialState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
|
||||
assertThat(awaitItem().selectedEventId).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle selected emojis`() = runTest {
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.selectedEventId).isNull()
|
||||
val reactions = aTimelineItemReactions(count = 1, isHighlighted = true)
|
||||
val key = reactions.reactions.first().key
|
||||
initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions)))
|
||||
val stateWithSelectedEmojis = awaitItem()
|
||||
assertThat(stateWithSelectedEmojis.selectedEventId).isEqualTo(AN_EVENT_ID)
|
||||
assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue