[Message actions] New UI for replies (#545)
* Add 'reply to' UI to the message composer. * Move the `BlurHashAsyncImage` to `:libraries:designsystem` as it is now used in several modules. * Create reusable `AttachmentThumbnail` and associated data classes and enums, it's now added to `:libraries:matrixui`. * Re-use `AttachmentThumbnail` in a `ActionListView` and `TextComposer`. * Add 'inReplyTo' models and UI. * Add min size for images * Create a separate layout for media items with no reply to info. Also, separate `Timeline__Row` components from `TimelineView`, as it was getting too large. * Added `EqualWidthColumn` to use inside message bubbles. Also fixed some modifiers for media items replying to other messages. * Disable `inReplyToClicked`. * Remove unused resources and libraries. * Remove any traces of `BlurHashAsyncImage` in `:features:messages`, since it was moved to the design system. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
43e4e1627d
commit
25c32cb1e8
56 changed files with 1253 additions and 1008 deletions
|
|
@ -33,16 +33,26 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
|
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.TimelineItemRedactedContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -55,6 +65,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
private val actionListPresenter: ActionListPresenter,
|
private val actionListPresenter: ActionListPresenter,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -145,7 +156,38 @@ class MessagesPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
private fun handleActionReply(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
|
||||||
if (targetEvent.eventId == null) return
|
if (targetEvent.eventId == null) return
|
||||||
val composerMode = MessageComposerMode.Reply(targetEvent.safeSenderName, targetEvent.eventId, "")
|
val textContent = messageSummaryFormatter.format(targetEvent)
|
||||||
|
val attachmentThumbnailInfo = when (targetEvent.content) {
|
||||||
|
is TimelineItemImageContent -> AttachmentThumbnailInfo(
|
||||||
|
mediaSource = targetEvent.content.mediaSource,
|
||||||
|
textContent = targetEvent.content.body,
|
||||||
|
type = AttachmentThumbnailType.Image,
|
||||||
|
blurHash = targetEvent.content.blurhash,
|
||||||
|
)
|
||||||
|
is TimelineItemVideoContent -> AttachmentThumbnailInfo(
|
||||||
|
mediaSource = targetEvent.content.thumbnailSource,
|
||||||
|
textContent = targetEvent.content.body,
|
||||||
|
type = AttachmentThumbnailType.Video,
|
||||||
|
blurHash = targetEvent.content.blurHash,
|
||||||
|
)
|
||||||
|
is TimelineItemFileContent -> AttachmentThumbnailInfo(
|
||||||
|
mediaSource = targetEvent.content.thumbnailSource,
|
||||||
|
textContent = targetEvent.content.body,
|
||||||
|
type = AttachmentThumbnailType.File,
|
||||||
|
blurHash = null,
|
||||||
|
)
|
||||||
|
is TimelineItemTextBasedContent,
|
||||||
|
is TimelineItemRedactedContent,
|
||||||
|
is TimelineItemStateContent,
|
||||||
|
is TimelineItemEncryptedContent,
|
||||||
|
is TimelineItemUnknownContent -> null
|
||||||
|
}
|
||||||
|
val composerMode = MessageComposerMode.Reply(
|
||||||
|
senderName = targetEvent.safeSenderName,
|
||||||
|
eventId = targetEvent.eventId,
|
||||||
|
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||||
|
defaultContent = textContent,
|
||||||
|
)
|
||||||
composerState.eventSink(
|
composerState.eventSink(
|
||||||
MessageComposerEvents.SetMode(composerMode)
|
MessageComposerEvents.SetMode(composerMode)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.actionlist
|
package io.element.android.features.messages.impl.actionlist
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -36,19 +35,16 @@ import androidx.compose.material.ListItem
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.AddReaction
|
import androidx.compose.material.icons.outlined.AddReaction
|
||||||
import androidx.compose.material.icons.outlined.Attachment
|
|
||||||
import androidx.compose.material.icons.outlined.VideoCameraBack
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -56,7 +52,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
|
|
@ -67,6 +62,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatterImpl
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -75,8 +71,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Divider
|
import io.element.android.libraries.designsystem.theme.components.Divider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.ModalBottomSheet
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -189,70 +186,56 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
|
||||||
Text(body, style = contentStyle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(body, style = contentStyle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val formatter = remember(context) { MessageSummaryFormatterImpl(context) }
|
||||||
|
val textContent = remember(event.content) { formatter.format(event) }
|
||||||
|
|
||||||
when (event.content) {
|
when (event.content) {
|
||||||
is TimelineItemTextBasedContent -> content = { ContentForBody(event.content.body) }
|
is TimelineItemTextBasedContent,
|
||||||
is TimelineItemStateContent -> content = { ContentForBody(event.content.body) }
|
is TimelineItemStateContent,
|
||||||
is TimelineItemProfileChangeContent -> content = { ContentForBody(event.content.body) }
|
is TimelineItemProfileChangeContent,
|
||||||
is TimelineItemEncryptedContent -> content = { ContentForBody(stringResource(StringR.string.common_unable_to_decrypt)) }
|
is TimelineItemEncryptedContent,
|
||||||
is TimelineItemRedactedContent -> content = { ContentForBody(stringResource(StringR.string.common_message_removed)) }
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemUnknownContent -> content = { ContentForBody(stringResource(StringR.string.common_unsupported_event)) }
|
is TimelineItemUnknownContent -> content = { ContentForBody(textContent) }
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
icon = {
|
icon = {
|
||||||
val mediaRequestData = MediaRequestData(
|
AttachmentThumbnail(
|
||||||
source = event.content.mediaSource,
|
|
||||||
kind = MediaRequestData.Kind.Thumbnail(32),
|
|
||||||
)
|
|
||||||
BlurHashAsyncImage(
|
|
||||||
model = mediaRequestData,
|
|
||||||
blurHash = event.content.blurhash,
|
|
||||||
contentDescription = stringResource(StringR.string.common_image),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = imageModifier,
|
modifier = imageModifier,
|
||||||
|
info = AttachmentThumbnailInfo(
|
||||||
|
mediaSource = event.content.mediaSource,
|
||||||
|
textContent = textContent,
|
||||||
|
type = AttachmentThumbnailType.File,
|
||||||
|
blurHash = event.content.blurhash,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.body) }
|
||||||
}
|
}
|
||||||
is TimelineItemVideoContent -> {
|
is TimelineItemVideoContent -> {
|
||||||
icon = {
|
icon = {
|
||||||
val thumbnailSource = event.content.thumbnailSource
|
AttachmentThumbnail(
|
||||||
if (thumbnailSource != null) {
|
modifier = imageModifier,
|
||||||
val mediaRequestData = MediaRequestData(
|
info = AttachmentThumbnailInfo(
|
||||||
source = event.content.thumbnailSource,
|
mediaSource = event.content.thumbnailSource,
|
||||||
kind = MediaRequestData.Kind.Thumbnail(32),
|
textContent = textContent,
|
||||||
)
|
type = AttachmentThumbnailType.Video,
|
||||||
BlurHashAsyncImage(
|
|
||||||
model = mediaRequestData,
|
|
||||||
blurHash = event.content.blurHash,
|
blurHash = event.content.blurHash,
|
||||||
contentDescription = stringResource(StringR.string.common_video),
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = imageModifier,
|
|
||||||
)
|
)
|
||||||
} else {
|
)
|
||||||
Box(
|
|
||||||
modifier = imageModifier.background(MaterialTheme.colorScheme.surface),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.VideoCameraBack,
|
|
||||||
contentDescription = stringResource(StringR.string.common_video),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.body) }
|
||||||
}
|
}
|
||||||
is TimelineItemFileContent -> {
|
is TimelineItemFileContent -> {
|
||||||
icon = {
|
icon = {
|
||||||
Box(
|
AttachmentThumbnail(
|
||||||
modifier = imageModifier.background(MaterialTheme.colorScheme.surface),
|
modifier = imageModifier,
|
||||||
contentAlignment = Alignment.Center
|
info = AttachmentThumbnailInfo(
|
||||||
) {
|
mediaSource = null,
|
||||||
Icon(
|
textContent = textContent,
|
||||||
imageVector = Icons.Outlined.Attachment,
|
type = AttachmentThumbnailType.File,
|
||||||
contentDescription = stringResource(StringR.string.common_file),
|
blurHash = null
|
||||||
modifier = Modifier.rotate(-45f)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
content = { ContentForBody(event.content.body) }
|
content = { ContentForBody(event.content.body) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
@ -96,6 +97,7 @@ internal fun aTimelineItemEvent(
|
||||||
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
content: TimelineItemEventContent = aTimelineItemTextContent(),
|
||||||
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
sendState: EventSendState = EventSendState.Sent(eventId),
|
sendState: EventSendState = EventSendState.Sent(eventId),
|
||||||
|
inReplyTo: InReplyTo? = null,
|
||||||
): TimelineItem.Event {
|
): TimelineItem.Event {
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = eventId.value,
|
id = eventId.value,
|
||||||
|
|
@ -113,5 +115,6 @@ internal fun aTimelineItemEvent(
|
||||||
senderDisplayName = "sender",
|
senderDisplayName = "sender",
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
sendState = sendState,
|
sendState = sendState,
|
||||||
|
inReplyTo = inReplyTo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,30 +17,16 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowDownward
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -55,39 +41,24 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.LastBaseline
|
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.R
|
||||||
import io.element.android.features.messages.impl.timeline.components.MessageEventBubble
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemEventRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemStateEventRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemVirtualRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
|
||||||
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.LocalColors
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
@ -101,12 +72,16 @@ fun TimelineView(
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
onMessageClicked: (TimelineItem.Event) -> Unit = {},
|
||||||
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onReachedLoadMore() {
|
fun onReachedLoadMore() {
|
||||||
state.eventSink(TimelineEvents.LoadMore)
|
state.eventSink(TimelineEvents.LoadMore)
|
||||||
}
|
}
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
fun inReplyToClicked(eventId: EventId) {
|
||||||
|
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
@ -124,6 +99,7 @@ fun TimelineView(
|
||||||
onClick = onMessageClicked,
|
onClick = onMessageClicked,
|
||||||
onLongClick = onMessageLongClicked,
|
onLongClick = onMessageLongClicked,
|
||||||
onUserDataClick = onUserDataClicked,
|
onUserDataClick = onUserDataClicked,
|
||||||
|
inReplyToClick = ::inReplyToClicked,
|
||||||
)
|
)
|
||||||
if (index == state.timelineItems.lastIndex) {
|
if (index == state.timelineItems.lastIndex) {
|
||||||
onReachedLoadMore()
|
onReachedLoadMore()
|
||||||
|
|
@ -146,6 +122,7 @@ fun TimelineItemRow(
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
inReplyToClick: (EventId) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
when (timelineItem) {
|
when (timelineItem) {
|
||||||
|
|
@ -179,6 +156,7 @@ fun TimelineItemRow(
|
||||||
onClick = ::onClick,
|
onClick = ::onClick,
|
||||||
onLongClick = ::onLongClick,
|
onLongClick = ::onLongClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +187,7 @@ fun TimelineItemRow(
|
||||||
highlightedItem = highlightedItem,
|
highlightedItem = highlightedItem,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -219,208 +198,6 @@ fun TimelineItemRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TimelineItemVirtualRow(
|
|
||||||
virtual: TimelineItem.Virtual,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
when (virtual.model) {
|
|
||||||
is TimelineItemLoadingModel -> TimelineLoadingMoreIndicator(modifier)
|
|
||||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TimelineItemEventRow(
|
|
||||||
event: TimelineItem.Event,
|
|
||||||
isHighlighted: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
onUserDataClick: (UserId) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
|
|
||||||
fun onUserDataClicked() {
|
|
||||||
onUserDataClick(event.senderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
|
||||||
Pair(Alignment.CenterEnd, Alignment.End)
|
|
||||||
} else {
|
|
||||||
Pair(Alignment.CenterStart, Alignment.Start)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight(),
|
|
||||||
contentAlignment = parentAlignment
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
if (!event.isMine) {
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
Column(horizontalAlignment = contentAlignment) {
|
|
||||||
if (event.showSenderInformation) {
|
|
||||||
MessageSenderInformation(
|
|
||||||
event.safeSenderName,
|
|
||||||
event.senderAvatar,
|
|
||||||
Modifier
|
|
||||||
.zIndex(1f)
|
|
||||||
.offset(y = 12.dp)
|
|
||||||
.clickable(onClick = ::onUserDataClicked)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val bubbleState = BubbleState(
|
|
||||||
groupPosition = event.groupPosition,
|
|
||||||
isMine = event.isMine,
|
|
||||||
isHighlighted = isHighlighted,
|
|
||||||
)
|
|
||||||
MessageEventBubble(
|
|
||||||
state = bubbleState,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(-1f)
|
|
||||||
.widthIn(max = 320.dp)
|
|
||||||
) {
|
|
||||||
MessageEventBubbleContent(
|
|
||||||
event = event,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onMessageClick = onClick,
|
|
||||||
onMessageLongClick = onLongClick
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TimelineItemReactionsView(
|
|
||||||
reactionsState = event.reactionsState,
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(1f)
|
|
||||||
.offset(x = if (event.isMine) 0.dp else 20.dp, y = -(4.dp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (event.isMine) {
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.groupPosition.isNew()) {
|
|
||||||
Spacer(modifier = modifier.height(8.dp))
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = modifier.height(2.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TimelineItemStateEventRow(
|
|
||||||
event: TimelineItem.Event,
|
|
||||||
isHighlighted: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
MessageStateEventContainer(
|
|
||||||
isHighlighted = isHighlighted,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(-1f)
|
|
||||||
.widthIn(max = 320.dp)
|
|
||||||
) {
|
|
||||||
TimelineItemEventContentView(
|
|
||||||
content = event.content,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
modifier = Modifier.defaultTimelineContentPadding()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MessageEventBubbleContent(
|
|
||||||
event: TimelineItem.Event,
|
|
||||||
interactionSource: MutableInteractionSource,
|
|
||||||
onMessageClick: () -> Unit,
|
|
||||||
onMessageLongClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val showTimestampWithOverlay = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ContentView(
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
TimelineItemEventContentView(
|
|
||||||
content = event.content,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onMessageClick,
|
|
||||||
onLongClick = onMessageLongClick,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showTimestampWithOverlay) {
|
|
||||||
Box(modifier.wrapContentSize()) {
|
|
||||||
ContentView()
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
|
||||||
.background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp))
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
) {
|
|
||||||
TimelineEventTimestampView(
|
|
||||||
event = event,
|
|
||||||
onClick = onMessageClick,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column {
|
|
||||||
ContentView(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp))
|
|
||||||
TimelineEventTimestampView(
|
|
||||||
event = event,
|
|
||||||
onClick = onMessageClick,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.End)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MessageSenderInformation(
|
|
||||||
sender: String,
|
|
||||||
senderAvatar: AvatarData?,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Row(modifier = modifier) {
|
|
||||||
if (senderAvatar != null) {
|
|
||||||
Avatar(senderAvatar)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = sender,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
modifier = Modifier
|
|
||||||
.alignBy(LastBaseline)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BoxScope.TimelineScrollHelper(
|
internal fun BoxScope.TimelineScrollHelper(
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,361 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.clip
|
||||||
|
import androidx.compose.ui.layout.LastBaseline
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
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.TimelineItemVideoContent
|
||||||
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.theme.LocalColors
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemEventRow(
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
isHighlighted: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
|
fun onUserDataClicked() {
|
||||||
|
onUserDataClick(event.senderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inReplayToClicked() {
|
||||||
|
val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return
|
||||||
|
inReplyToClick(inReplyToEventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (parentAlignment, contentAlignment) = if (event.isMine) {
|
||||||
|
Pair(Alignment.CenterEnd, Alignment.End)
|
||||||
|
} else {
|
||||||
|
Pair(Alignment.CenterStart, Alignment.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
contentAlignment = parentAlignment
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
if (!event.isMine) {
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
Column(horizontalAlignment = contentAlignment) {
|
||||||
|
if (event.showSenderInformation) {
|
||||||
|
MessageSenderInformation(
|
||||||
|
event.safeSenderName,
|
||||||
|
event.senderAvatar,
|
||||||
|
Modifier
|
||||||
|
.zIndex(1f)
|
||||||
|
.offset(y = 12.dp)
|
||||||
|
.clickable(onClick = ::onUserDataClicked)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val bubbleState = BubbleState(
|
||||||
|
groupPosition = event.groupPosition,
|
||||||
|
isMine = event.isMine,
|
||||||
|
isHighlighted = isHighlighted,
|
||||||
|
)
|
||||||
|
MessageEventBubble(
|
||||||
|
state = bubbleState,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(-1f)
|
||||||
|
.widthIn(max = 320.dp)
|
||||||
|
) {
|
||||||
|
MessageEventBubbleContent(
|
||||||
|
event = event,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onMessageClick = onClick,
|
||||||
|
onMessageLongClick = onLongClick,
|
||||||
|
inReplyToClick = ::inReplayToClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TimelineItemReactionsView(
|
||||||
|
reactionsState = event.reactionsState,
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(1f)
|
||||||
|
.offset(x = if (event.isMine) 0.dp else 20.dp, y = -(4.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (event.isMine) {
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.groupPosition.isNew()) {
|
||||||
|
Spacer(modifier = modifier.height(8.dp))
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = modifier.height(2.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageSenderInformation(
|
||||||
|
sender: String,
|
||||||
|
senderAvatar: AvatarData?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(modifier = modifier) {
|
||||||
|
if (senderAvatar != null) {
|
||||||
|
Avatar(senderAvatar)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = sender,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.alignBy(LastBaseline)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageEventBubbleContent(
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
onMessageClick: () -> Unit,
|
||||||
|
onMessageLongClick: () -> Unit,
|
||||||
|
inReplyToClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val isMediaItem = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent
|
||||||
|
val replyToDetails = event.inReplyTo as? InReplyTo.Ready
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContentView(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
TimelineItemEventContentView(
|
||||||
|
content = event.content,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onMessageClick,
|
||||||
|
onLongClick = onMessageLongClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ContentAndTimestampView(
|
||||||
|
overlayTimestamp: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentModifier: Modifier = Modifier,
|
||||||
|
timestampModifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (overlayTimestamp) {
|
||||||
|
Box(modifier) {
|
||||||
|
ContentView(modifier = contentModifier)
|
||||||
|
TimelineEventTimestampView(
|
||||||
|
event = event,
|
||||||
|
onClick = onMessageClick,
|
||||||
|
modifier = timestampModifier
|
||||||
|
.padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding
|
||||||
|
.background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp))
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(horizontal = 4.dp, vertical = 2.dp) // Inner padding
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(modifier) {
|
||||||
|
ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp))
|
||||||
|
TimelineEventTimestampView(
|
||||||
|
event = event,
|
||||||
|
onClick = onMessageClick,
|
||||||
|
modifier = timestampModifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 2.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. */
|
||||||
|
@Composable
|
||||||
|
fun CommonLayout(
|
||||||
|
inReplyToDetails: InReplyTo.Ready?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
EqualWidthColumn(modifier = modifier, spacing = 8.dp) {
|
||||||
|
if (inReplyToDetails != null) {
|
||||||
|
val senderName = event.senderDisplayName ?: event.senderId.value
|
||||||
|
val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails)
|
||||||
|
ReplyToContent(
|
||||||
|
senderName = senderName,
|
||||||
|
text = inReplyToDetails.content.body,
|
||||||
|
attachmentThumbnailInfo = attachmentThumbnailInfo,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp, start = 8.dp, end = 8.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 {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentAndTimestampView(
|
||||||
|
overlayTimestamp = isMediaItem,
|
||||||
|
contentModifier = contentModifier,
|
||||||
|
modifier = modifierWithPadding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMediaItem && replyToDetails == null) {
|
||||||
|
SimpleMediaItemLayout()
|
||||||
|
} else {
|
||||||
|
CommonLayout(inReplyToDetails = replyToDetails, modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReplyToContent(
|
||||||
|
senderName: String,
|
||||||
|
text: String?,
|
||||||
|
attachmentThumbnailInfo: AttachmentThumbnailInfo?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val paddings = if (attachmentThumbnailInfo != null) {
|
||||||
|
PaddingValues(start = 4.dp, end = 12.dp, top = 4.dp, bottom = 4.dp)
|
||||||
|
} else {
|
||||||
|
PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 4.dp)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(paddings)
|
||||||
|
) {
|
||||||
|
if (attachmentThumbnailInfo != null) {
|
||||||
|
AttachmentThumbnail(
|
||||||
|
info = attachmentThumbnailInfo,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Text(
|
||||||
|
senderName,
|
||||||
|
style = ElementTextStyles.Regular.caption2.copy(fontWeight = FontWeight.Medium),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text.orEmpty(),
|
||||||
|
style = ElementTextStyles.Regular.caption1,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
color = LocalColors.current.placeholder,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) =
|
||||||
|
when (val type = inReplyTo.content.type) {
|
||||||
|
is ImageMessageType -> AttachmentThumbnailInfo(
|
||||||
|
mediaSource = type.info?.thumbnailSource,
|
||||||
|
textContent = inReplyTo.content.body,
|
||||||
|
type = AttachmentThumbnailType.Image,
|
||||||
|
blurHash = type.info?.blurhash,
|
||||||
|
)
|
||||||
|
is VideoMessageType -> AttachmentThumbnailInfo(
|
||||||
|
mediaSource = type.info?.thumbnailSource,
|
||||||
|
textContent = inReplyTo.content.body,
|
||||||
|
type = AttachmentThumbnailType.Video,
|
||||||
|
blurHash = type.info?.blurhash,
|
||||||
|
)
|
||||||
|
is FileMessageType -> AttachmentThumbnailInfo(
|
||||||
|
mediaSource = type.info?.thumbnailSource,
|
||||||
|
textContent = inReplyTo.content.body,
|
||||||
|
type = AttachmentThumbnailType.File,
|
||||||
|
blurHash = null,
|
||||||
|
)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemStateEventRow(
|
||||||
|
event: TimelineItem.Event,
|
||||||
|
isHighlighted: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
MessageStateEventContainer(
|
||||||
|
isHighlighted = isHighlighted,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.zIndex(-1f)
|
||||||
|
.widthIn(max = 320.dp)
|
||||||
|
) {
|
||||||
|
TimelineItemEventContentView(
|
||||||
|
content = event.content,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = Modifier.defaultTimelineContentPadding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemVirtualRow(
|
||||||
|
virtual: TimelineItem.Virtual,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
when (virtual.model) {
|
||||||
|
is TimelineItemLoadingModel -> TimelineLoadingMoreIndicator(modifier)
|
||||||
|
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,20 +22,23 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
|
||||||
|
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemImageView(
|
fun TimelineItemImageView(
|
||||||
content: TimelineItemImageContent,
|
content: TimelineItemImageContent,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
// TODO place this value somewhere else?
|
||||||
|
val minHeight = max(100, content.height ?: 0)
|
||||||
TimelineItemAspectRatioBox(
|
TimelineItemAspectRatioBox(
|
||||||
height = content.height,
|
height = minHeight,
|
||||||
aspectRatio = content.aspectRatio,
|
aspectRatio = content.aspectRatio,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,9 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
||||||
|
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||||
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||||
sendState = currentTimelineItem.event.localSendState ?: EventSendState.NotSentYet,
|
sendState = currentTimelineItem.event.localSendState ?: EventSendState.NotSentYet,
|
||||||
|
inReplyTo = currentTimelineItem.event.inReplyTo(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -59,6 +60,7 @@ sealed interface TimelineItem {
|
||||||
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
val groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
|
||||||
val reactionsState: TimelineItemReactions,
|
val reactionsState: TimelineItemReactions,
|
||||||
val sendState: EventSendState,
|
val sendState: EventSendState,
|
||||||
|
val inReplyTo: InReplyTo?,
|
||||||
) : TimelineItem {
|
) : TimelineItem {
|
||||||
|
|
||||||
val showSenderInformation = groupPosition.isNew() && !isMine
|
val showSenderInformation = groupPosition.isNew() && !isMine
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.utils.messagesummary
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
||||||
|
interface MessageSummaryFormatter {
|
||||||
|
fun format(event: TimelineItem.Event): String
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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.utils.messagesummary
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
|
||||||
|
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.TimelineItemProfileChangeContent
|
||||||
|
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
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(RoomScope::class)
|
||||||
|
class MessageSummaryFormatterImpl @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : MessageSummaryFormatter {
|
||||||
|
override fun format(event: TimelineItem.Event): String {
|
||||||
|
return when (event.content) {
|
||||||
|
is TimelineItemTextBasedContent -> event.content.body
|
||||||
|
is TimelineItemStateContent -> event.content.body
|
||||||
|
is TimelineItemProfileChangeContent -> event.content.body
|
||||||
|
is TimelineItemEncryptedContent -> context.getString(R.string.common_unable_to_decrypt)
|
||||||
|
is TimelineItemRedactedContent -> context.getString(R.string.common_message_removed)
|
||||||
|
is TimelineItemUnknownContent -> context.getString(R.string.common_unsupported_event)
|
||||||
|
is TimelineItemImageContent -> context.getString(R.string.common_image)
|
||||||
|
is TimelineItemVideoContent -> context.getString(R.string.common_video)
|
||||||
|
is TimelineItemFileContent -> context.getString(R.string.common_file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,13 +29,21 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
|
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.TimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatterImpl
|
||||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
|
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
|
|
@ -105,6 +113,105 @@ class MessagesPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action reply to an event with no id does nothing`() = runTest {
|
||||||
|
val presenter = createMessagePresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
|
||||||
|
skipItems(1)
|
||||||
|
// Otherwise we would have some extra items here
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action reply to an image media message`() = runTest {
|
||||||
|
val presenter = createMessagePresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val mediaMessage = aMessageEvent(
|
||||||
|
content = TimelineItemImageContent(
|
||||||
|
body = "image.jpg",
|
||||||
|
mediaSource = MediaSource(AN_AVATAR_URL),
|
||||||
|
mimeType = MimeTypes.Jpeg,
|
||||||
|
blurhash = null,
|
||||||
|
width = 20,
|
||||||
|
height = 20,
|
||||||
|
aspectRatio = 1.0f,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
|
||||||
|
skipItems(1)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
|
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||||
|
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action reply to a video media message`() = runTest {
|
||||||
|
val presenter = createMessagePresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val mediaMessage = aMessageEvent(
|
||||||
|
content = TimelineItemVideoContent(
|
||||||
|
body = "video.mp4",
|
||||||
|
duration = 10L,
|
||||||
|
videoSource = MediaSource(AN_AVATAR_URL),
|
||||||
|
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
||||||
|
mimeType = MimeTypes.Mp4,
|
||||||
|
blurHash = null,
|
||||||
|
width = 20,
|
||||||
|
height = 20,
|
||||||
|
aspectRatio = 1.0f,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
|
||||||
|
skipItems(1)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
|
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||||
|
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - handle action reply to a file media message`() = runTest {
|
||||||
|
val presenter = createMessagePresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val mediaMessage = aMessageEvent(
|
||||||
|
content = TimelineItemFileContent(
|
||||||
|
body = "video.mp4",
|
||||||
|
fileSource = MediaSource(AN_AVATAR_URL),
|
||||||
|
thumbnailSource = MediaSource(AN_AVATAR_URL),
|
||||||
|
formattedFileSize = "10 MB",
|
||||||
|
mimeType = MimeTypes.Pdf,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, mediaMessage))
|
||||||
|
skipItems(1)
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java)
|
||||||
|
val replyMode = finalState.composerState.mode as MessageComposerMode.Reply
|
||||||
|
assertThat(replyMode.attachmentThumbnailInfo).isNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action edit`() = runTest {
|
fun `present - handle action edit`() = runTest {
|
||||||
val presenter = createMessagePresenter()
|
val presenter = createMessagePresenter()
|
||||||
|
|
@ -197,6 +304,7 @@ class MessagesPresenterTest {
|
||||||
actionListPresenter = actionListPresenter,
|
actionListPresenter = actionListPresenter,
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
|
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,5 +245,6 @@ private fun aMessageEvent(
|
||||||
sentTime = "",
|
sentTime = "",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
reactionsState = TimelineItemReactions(persistentListOf()),
|
reactionsState = TimelineItemReactions(persistentListOf()),
|
||||||
sendState = EventSendState.Sent(AN_EVENT_ID)
|
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||||
|
inReplyTo = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemReac
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
|
@ -29,11 +31,13 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
internal fun aMessageEvent(
|
internal fun aMessageEvent(
|
||||||
|
eventId: EventId? = AN_EVENT_ID,
|
||||||
isMine: Boolean = true,
|
isMine: Boolean = true,
|
||||||
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
|
||||||
|
inReplyTo: InReplyTo? = null,
|
||||||
) = TimelineItem.Event(
|
) = TimelineItem.Event(
|
||||||
id = AN_EVENT_ID.value,
|
id = eventId?.value.orEmpty(),
|
||||||
eventId = AN_EVENT_ID,
|
eventId = eventId,
|
||||||
senderId = A_USER_ID,
|
senderId = A_USER_ID,
|
||||||
senderDisplayName = A_USER_NAME,
|
senderDisplayName = A_USER_NAME,
|
||||||
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME),
|
||||||
|
|
@ -41,5 +45,6 @@ internal fun aMessageEvent(
|
||||||
sentTime = "",
|
sentTime = "",
|
||||||
isMine = isMine,
|
isMine = isMine,
|
||||||
reactionsState = TimelineItemReactions(persistentListOf()),
|
reactionsState = TimelineItemReactions(persistentListOf()),
|
||||||
sendState = EventSendState.Sent(AN_EVENT_ID)
|
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||||
|
inReplyTo = inReplyTo,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -483,5 +483,5 @@ class MessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
|
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
|
||||||
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, AN_EVENT_ID, A_MESSAGE)
|
fun aReplyMode() = MessageComposerMode.Reply(A_USER_NAME, null, AN_EVENT_ID, A_MESSAGE)
|
||||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ class TimelineItemGrouperTest {
|
||||||
senderDisplayName = "",
|
senderDisplayName = "",
|
||||||
content = TimelineItemStateEventContent(body = "a state event"),
|
content = TimelineItemStateEventContent(body = "a state event"),
|
||||||
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList()),
|
reactionsState = TimelineItemReactions(emptyList<AggregatedReaction>().toImmutableList()),
|
||||||
sendState = EventSendState.Sent(AN_EVENT_ID)
|
sendState = EventSendState.Sent(AN_EVENT_ID),
|
||||||
|
inReplyTo = null,
|
||||||
)
|
)
|
||||||
private val aNonGroupableItem = aMessageEvent()
|
private val aNonGroupableItem = aMessageEvent()
|
||||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.utils.messagesummary
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
||||||
|
|
||||||
|
class FakeMessageSummaryFormatter : MessageSummaryFormatter {
|
||||||
|
|
||||||
|
private var result = "A message"
|
||||||
|
|
||||||
|
override fun format(event: TimelineItem.Event): String = result
|
||||||
|
|
||||||
|
fun givenMessageResult(value: String) {
|
||||||
|
result = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ android {
|
||||||
// Should not be there, but this is a POC
|
// Should not be there, but this is a POC
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.accompanist.systemui)
|
implementation(libs.accompanist.systemui)
|
||||||
|
implementation(libs.vanniktech.blurhash)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components.blurhash
|
package io.element.android.libraries.designsystem.components
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to create a column where all children have the same width.
|
||||||
|
* It will first measure all children, get the largest width and re-measure all children with this width as the minWidth.
|
||||||
|
*
|
||||||
|
* *Note*: If all children already have the same width, it skips the 2nd measuring and acts like a normal Column.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun EqualWidthColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
spacing: Dp = 0.dp,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||||
|
val measurables = subcompose(0, content).map { it.measure(constraints) }
|
||||||
|
val maxWidth = measurables.maxOf { it.width }
|
||||||
|
val newConstraints = constraints.copy(minWidth = maxWidth)
|
||||||
|
val newMeasurables = if (measurables.all { it.width == maxWidth }) {
|
||||||
|
// Skip re-measuring if all children have the same width
|
||||||
|
measurables
|
||||||
|
} else {
|
||||||
|
// Re-measure with the largest width as the minWidth to have all children constrained to the same width
|
||||||
|
subcompose(1, content).map { it.measure(newConstraints) }
|
||||||
|
}
|
||||||
|
val totalHeight = (newMeasurables.sumOf { it.height } + spacing.toPx() * (newMeasurables.size - 1)).roundToInt()
|
||||||
|
layout(maxWidth, totalHeight) {
|
||||||
|
var yPosition = 0
|
||||||
|
newMeasurables.forEach { measurable ->
|
||||||
|
measurable.placeRelative(0, yPosition)
|
||||||
|
yPosition += measurable.height + spacing.roundToPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,11 +28,25 @@ sealed interface EventContent
|
||||||
|
|
||||||
data class MessageContent(
|
data class MessageContent(
|
||||||
val body: String,
|
val body: String,
|
||||||
val inReplyTo: EventId?,
|
val inReplyTo: InReplyTo?,
|
||||||
val isEdited: Boolean,
|
val isEdited: Boolean,
|
||||||
val type: MessageType?
|
val type: MessageType?
|
||||||
) : EventContent
|
) : EventContent
|
||||||
|
|
||||||
|
|
||||||
|
sealed interface InReplyTo {
|
||||||
|
data class NotLoaded(val eventId: EventId) : InReplyTo
|
||||||
|
data class Ready(
|
||||||
|
val eventId: EventId,
|
||||||
|
val content: MessageContent,
|
||||||
|
val senderId: UserId,
|
||||||
|
val senderDisplayName: String?,
|
||||||
|
val senderAvatarUrl: String?,
|
||||||
|
) : InReplyTo
|
||||||
|
|
||||||
|
object Error : InReplyTo
|
||||||
|
}
|
||||||
|
|
||||||
object RedactedContent : EventContent
|
object RedactedContent : EventContent
|
||||||
|
|
||||||
data class StickerContent(
|
data class StickerContent(
|
||||||
|
|
|
||||||
|
|
@ -32,4 +32,12 @@ data class EventTimelineItem(
|
||||||
val senderProfile: ProfileTimelineDetails,
|
val senderProfile: ProfileTimelineDetails,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val content: EventContent
|
val content: EventContent
|
||||||
)
|
) {
|
||||||
|
fun inReplyTo(): InReplyTo? {
|
||||||
|
return (content as? MessageContent)?.inReplyTo
|
||||||
|
}
|
||||||
|
fun hasNotLoadedInReplyTo(): Boolean {
|
||||||
|
val details = inReplyTo()
|
||||||
|
return details is InReplyTo.NotLoaded || details is InReplyTo.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,19 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.timeline
|
package io.element.android.libraries.matrix.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.rustcomponents.sdk.Room
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class MatrixTimelineItemMapper(
|
class MatrixTimelineItemMapper(
|
||||||
|
private val room: Room,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
||||||
private val eventTimelineItemMapper: EventTimelineItemMapper= EventTimelineItemMapper(),
|
private val eventTimelineItemMapper: EventTimelineItemMapper= EventTimelineItemMapper(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -30,6 +37,12 @@ class MatrixTimelineItemMapper(
|
||||||
val asEvent = it.asEvent()
|
val asEvent = it.asEvent()
|
||||||
if (asEvent != null) {
|
if (asEvent != null) {
|
||||||
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
|
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
|
||||||
|
|
||||||
|
|
||||||
|
if (eventTimelineItem.hasNotLoadedInReplyTo() && eventTimelineItem.eventId != null) {
|
||||||
|
fetchDetailsForEvent(eventTimelineItem.eventId!!)
|
||||||
|
}
|
||||||
|
|
||||||
return MatrixTimelineItem.Event(eventTimelineItem)
|
return MatrixTimelineItem.Event(eventTimelineItem)
|
||||||
}
|
}
|
||||||
val asVirtual = it.asVirtual()
|
val asVirtual = it.asVirtual()
|
||||||
|
|
@ -39,4 +52,13 @@ class MatrixTimelineItemMapper(
|
||||||
}
|
}
|
||||||
return MatrixTimelineItem.Other
|
return MatrixTimelineItem.Other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fetchDetailsForEvent(eventId: EventId) = coroutineScope.launch {
|
||||||
|
runCatching {
|
||||||
|
room.fetchDetailsForEvent(eventId.value)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ class RustMatrixTimeline(
|
||||||
)
|
)
|
||||||
|
|
||||||
private val timelineItemFactory = MatrixTimelineItemMapper(
|
private val timelineItemFactory = MatrixTimelineItemMapper(
|
||||||
|
room = innerRoom,
|
||||||
|
coroutineScope = coroutineScope,
|
||||||
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
||||||
eventTimelineItemMapper = EventTimelineItemMapper(
|
eventTimelineItemMapper = EventTimelineItemMapper(
|
||||||
contentMapper = TimelineEventContentMapper(
|
contentMapper = TimelineEventContentMapper(
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@
|
||||||
package io.element.android.libraries.matrix.impl.timeline.item.event
|
package io.element.android.libraries.matrix.impl.timeline.item.event
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageFormat
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
|
||||||
|
|
@ -31,6 +33,8 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
||||||
import io.element.android.libraries.matrix.impl.media.map
|
import io.element.android.libraries.matrix.impl.media.map
|
||||||
import org.matrix.rustcomponents.sdk.Message
|
import org.matrix.rustcomponents.sdk.Message
|
||||||
import org.matrix.rustcomponents.sdk.MessageType
|
import org.matrix.rustcomponents.sdk.MessageType
|
||||||
|
import org.matrix.rustcomponents.sdk.ProfileDetails
|
||||||
|
import org.matrix.rustcomponents.sdk.RepliedToEventDetails
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
|
import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody
|
||||||
import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
|
import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat
|
||||||
|
|
@ -66,9 +70,26 @@ class EventMessageMapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId)
|
||||||
|
val inReplyToEvent: InReplyTo? = (it.inReplyTo()?.event)?.use { details ->
|
||||||
|
when (details) {
|
||||||
|
is RepliedToEventDetails.Ready -> {
|
||||||
|
val senderProfile = details.senderProfile as? ProfileDetails.Ready
|
||||||
|
InReplyTo.Ready(
|
||||||
|
eventId = inReplyToId!!,
|
||||||
|
content = map(details.message),
|
||||||
|
senderId = UserId(details.sender),
|
||||||
|
senderDisplayName = senderProfile?.displayName,
|
||||||
|
senderAvatarUrl = senderProfile?.avatarUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RepliedToEventDetails.Error -> InReplyTo.Error
|
||||||
|
is RepliedToEventDetails.Pending, is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(inReplyToId!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
MessageContent(
|
MessageContent(
|
||||||
body = it.body(),
|
body = it.body(),
|
||||||
inReplyTo = it.inReplyTo()?.eventId?.let(::EventId),
|
inReplyTo = inReplyToEvent,
|
||||||
isEdited = it.isEdited(),
|
isEdited = it.isEdited(),
|
||||||
type = type
|
type = type
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.ui.components
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Attachment
|
||||||
|
import androidx.compose.material.icons.outlined.VideoCameraBack
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import io.element.android.libraries.designsystem.components.BlurHashAsyncImage
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AttachmentThumbnail(
|
||||||
|
info: AttachmentThumbnailInfo,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
thumbnailSize: Long = 32L,
|
||||||
|
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
if (info.mediaSource != null) {
|
||||||
|
val mediaRequestData = MediaRequestData(
|
||||||
|
source = info.mediaSource,
|
||||||
|
kind = MediaRequestData.Kind.Thumbnail(thumbnailSize),
|
||||||
|
)
|
||||||
|
BlurHashAsyncImage(
|
||||||
|
model = mediaRequestData,
|
||||||
|
blurHash = info.blurHash,
|
||||||
|
contentDescription = info.textContent,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.background(backgroundColor),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
when (info.type) {
|
||||||
|
AttachmentThumbnailType.Video -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.VideoCameraBack,
|
||||||
|
contentDescription = info.textContent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AttachmentThumbnailType.File -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Attachment,
|
||||||
|
contentDescription = info.textContent,
|
||||||
|
modifier = Modifier.rotate(-45f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
enum class AttachmentThumbnailType: Parcelable {
|
||||||
|
Image, Video, File
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class AttachmentThumbnailInfo(
|
||||||
|
val mediaSource: MediaSource?,
|
||||||
|
val textContent: String?,
|
||||||
|
val type: AttachmentThumbnailType?,
|
||||||
|
val blurHash: String?,
|
||||||
|
): Parcelable
|
||||||
|
|
@ -22,9 +22,6 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.textcomposer"
|
namespace = "io.element.android.libraries.textcomposer"
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -33,9 +30,7 @@ dependencies {
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(libs.wysiwyg)
|
|
||||||
implementation(libs.androidx.constraintlayout)
|
|
||||||
implementation(libs.androidx.material)
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<manifest>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.textcomposer
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
sealed interface MessageComposerMode : Parcelable {
|
sealed interface MessageComposerMode : Parcelable {
|
||||||
|
|
@ -38,6 +39,7 @@ sealed interface MessageComposerMode : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Reply(
|
class Reply(
|
||||||
val senderName: String,
|
val senderName: String,
|
||||||
|
val attachmentThumbnailInfo: AttachmentThumbnailInfo?,
|
||||||
override val eventId: EventId,
|
override val eventId: EventId,
|
||||||
override val defaultContent: CharSequence
|
override val defaultContent: CharSequence
|
||||||
) : Special(eventId, defaultContent)
|
) : Special(eventId, defaultContent)
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,10 @@ import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
|
@ -73,6 +75,10 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
|
||||||
|
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -180,21 +186,99 @@ private fun ComposerModeView(
|
||||||
) {
|
) {
|
||||||
when (composerMode) {
|
when (composerMode) {
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
|
EditingModeView(onResetComposerMode = onResetComposerMode, modifier = modifier)
|
||||||
modifier = modifier
|
}
|
||||||
.fillMaxWidth()
|
is MessageComposerMode.Reply -> {
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
ReplyToModeView(
|
||||||
Icon(
|
modifier = modifier.padding(8.dp),
|
||||||
resourceId = VectorIcons.Edit,
|
senderName = composerMode.senderName,
|
||||||
contentDescription = stringResource(R.string.editing),
|
text = composerMode.defaultContent.toString(),
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
attachmentThumbnailInfo = composerMode.attachmentThumbnailInfo,
|
||||||
modifier = Modifier.size(16.dp),
|
onResetComposerMode = onResetComposerMode,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditingModeView(
|
||||||
|
onResetComposerMode: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)) {
|
||||||
|
Icon(
|
||||||
|
resourceId = VectorIcons.Edit,
|
||||||
|
contentDescription = stringResource(R.string.editing),
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.editing),
|
||||||
|
style = ElementTextStyles.Regular.caption2,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(StringR.string.action_close),
|
||||||
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.clickable(
|
||||||
|
enabled = true,
|
||||||
|
onClick = onResetComposerMode,
|
||||||
|
interactionSource = MutableInteractionSource(),
|
||||||
|
indication = rememberRipple(bounded = false)
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReplyToModeView(
|
||||||
|
senderName: String,
|
||||||
|
text: String?,
|
||||||
|
attachmentThumbnailInfo: AttachmentThumbnailInfo?,
|
||||||
|
onResetComposerMode: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val paddings = if (attachmentThumbnailInfo != null) {
|
||||||
|
PaddingValues(start = 4.dp, end = 12.dp, top = 4.dp, bottom = 4.dp)
|
||||||
|
} else {
|
||||||
|
PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 4.dp)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier
|
||||||
|
.clip(RoundedCornerShape(13.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.padding(paddings)
|
||||||
|
) {
|
||||||
|
if (attachmentThumbnailInfo != null) {
|
||||||
|
AttachmentThumbnail(
|
||||||
|
info = attachmentThumbnailInfo,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(RoundedCornerShape(9.dp))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.SpaceEvenly) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.editing),
|
senderName,
|
||||||
style = ElementTextStyles.Regular.caption2,
|
style = ElementTextStyles.Regular.caption2.copy(fontWeight = FontWeight.Medium),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -209,11 +293,19 @@ private fun ComposerModeView(
|
||||||
interactionSource = MutableInteractionSource(),
|
interactionSource = MutableInteractionSource(),
|
||||||
indication = rememberRipple(bounded = false)
|
indication = rememberRipple(bounded = false)
|
||||||
),
|
),
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = text.orEmpty(),
|
||||||
|
style = ElementTextStyles.Regular.caption1,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
color = LocalColors.current.placeholder,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,14 +381,30 @@ private fun BoxScope.SendButton(
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextComposerLightPreview() = ElementPreviewLight { ContentToPreview() }
|
internal fun TextComposerSimpleLightPreview() = ElementPreviewLight { SimpleContentToPreview() }
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextComposerDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
internal fun TextComposerSimpleDarkPreview() = ElementPreviewDark { SimpleContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TextComposerEditLightPreview() = ElementPreviewLight { EditContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TextComposerEditDarkPreview() = ElementPreviewDark { EditContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TextComposerReplyLightPreview() = ElementPreviewLight { ReplyContentToPreview() }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
internal fun TextComposerReplyDarkPreview() = ElementPreviewDark { ReplyContentToPreview() }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview() {
|
private fun SimpleContentToPreview() {
|
||||||
Column {
|
Column {
|
||||||
TextComposer(
|
TextComposer(
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
|
|
@ -322,10 +430,89 @@ private fun ContentToPreview() {
|
||||||
composerCanSendMessage = true,
|
composerCanSendMessage = true,
|
||||||
composerText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
composerText = "A message\nWith several lines\nTo preview larger textfields and long lines with overflow",
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditContentToPreview() {
|
||||||
|
TextComposer(
|
||||||
|
onSendMessage = {},
|
||||||
|
onComposerTextChange = {},
|
||||||
|
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text"),
|
||||||
|
onResetComposerMode = {},
|
||||||
|
composerCanSendMessage = true,
|
||||||
|
composerText = "A message",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReplyContentToPreview() {
|
||||||
|
Column {
|
||||||
TextComposer(
|
TextComposer(
|
||||||
onSendMessage = {},
|
onSendMessage = {},
|
||||||
onComposerTextChange = {},
|
onComposerTextChange = {},
|
||||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text"),
|
composerMode = MessageComposerMode.Reply(
|
||||||
|
senderName = "Alice",
|
||||||
|
eventId = EventId("$1234"),
|
||||||
|
attachmentThumbnailInfo = null,
|
||||||
|
defaultContent = "A message\n" +
|
||||||
|
"With several lines\n" +
|
||||||
|
"To preview larger textfields and long lines with overflow"
|
||||||
|
),
|
||||||
|
onResetComposerMode = {},
|
||||||
|
composerCanSendMessage = true,
|
||||||
|
composerText = "A message",
|
||||||
|
)
|
||||||
|
TextComposer(
|
||||||
|
onSendMessage = {},
|
||||||
|
onComposerTextChange = {},
|
||||||
|
composerMode = MessageComposerMode.Reply(
|
||||||
|
senderName = "Alice",
|
||||||
|
eventId = EventId("$1234"),
|
||||||
|
attachmentThumbnailInfo = AttachmentThumbnailInfo(
|
||||||
|
mediaSource = MediaSource("https://domain.com/image.jpg"),
|
||||||
|
textContent = "image.jpg",
|
||||||
|
type = AttachmentThumbnailType.Image,
|
||||||
|
blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||||
|
),
|
||||||
|
defaultContent = "image.jpg"
|
||||||
|
),
|
||||||
|
onResetComposerMode = {},
|
||||||
|
composerCanSendMessage = true,
|
||||||
|
composerText = "A message",
|
||||||
|
)
|
||||||
|
TextComposer(
|
||||||
|
onSendMessage = {},
|
||||||
|
onComposerTextChange = {},
|
||||||
|
composerMode = MessageComposerMode.Reply(
|
||||||
|
senderName = "Alice",
|
||||||
|
eventId = EventId("$1234"),
|
||||||
|
attachmentThumbnailInfo = AttachmentThumbnailInfo(
|
||||||
|
mediaSource = MediaSource("https://domain.com/video.mp4"),
|
||||||
|
textContent = "video.mp4",
|
||||||
|
type = AttachmentThumbnailType.Video,
|
||||||
|
blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||||
|
),
|
||||||
|
defaultContent = "video.mp4"
|
||||||
|
),
|
||||||
|
onResetComposerMode = {},
|
||||||
|
composerCanSendMessage = true,
|
||||||
|
composerText = "A message",
|
||||||
|
)
|
||||||
|
TextComposer(
|
||||||
|
onSendMessage = {},
|
||||||
|
onComposerTextChange = {},
|
||||||
|
composerMode = MessageComposerMode.Reply(
|
||||||
|
senderName = "Alice",
|
||||||
|
eventId = EventId("$1234"),
|
||||||
|
attachmentThumbnailInfo = AttachmentThumbnailInfo(
|
||||||
|
mediaSource = null,
|
||||||
|
textContent = "logs.txt",
|
||||||
|
type = AttachmentThumbnailType.File,
|
||||||
|
blurHash = null,
|
||||||
|
),
|
||||||
|
defaultContent = "logs.txt"
|
||||||
|
),
|
||||||
onResetComposerMode = {},
|
onResetComposerMode = {},
|
||||||
composerCanSendMessage = true,
|
composerCanSendMessage = true,
|
||||||
composerText = "A message",
|
composerText = "A message",
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:state_hovered="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="?attr/vctr_rich_text_editor_menu_button_background" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item android:state_selected="true">
|
|
||||||
<shape android:shape="rectangle">
|
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="?attr/vctr_rich_text_editor_menu_button_background" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<ripple android:color="?attr/vctr_rich_text_editor_menu_button_background" />
|
|
||||||
</item>
|
|
||||||
</selector>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="?vctr_content_quinary" />
|
|
||||||
<corners android:radius="4dp" />
|
|
||||||
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="44dp"
|
|
||||||
android:height="44dp"
|
|
||||||
android:viewportWidth="44"
|
|
||||||
android:viewportHeight="44">
|
|
||||||
<path
|
|
||||||
android:pathData="M16,14.5C16,13.672 16.672,13 17.5,13H22.288C25.139,13 27.25,15.466 27.25,18.25C27.25,19.38 26.902,20.458 26.298,21.34C27.765,22.268 28.75,23.882 28.75,25.75C28.75,28.689 26.311,31 23.393,31H17.5C16.672,31 16,30.328 16,29.5V14.5ZM19,16V20.5H22.288C23.261,20.5 24.25,19.608 24.25,18.25C24.25,16.892 23.261,16 22.288,16H19ZM19,23.5V28H23.393C24.735,28 25.75,26.953 25.75,25.75C25.75,24.547 24.735,23.5 23.393,23.5H19Z"
|
|
||||||
android:fillColor="#8D97A5"
|
|
||||||
android:fillType="evenOdd" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="20dp"
|
|
||||||
android:height="20dp"
|
|
||||||
android:viewportWidth="20"
|
|
||||||
android:viewportHeight="20">
|
|
||||||
<path
|
|
||||||
android:fillColor="#C1C6CD"
|
|
||||||
android:pathData="M10.708,10Q10.438,10 10.219,9.781Q10,9.562 10,9.292V4.542Q10,4.354 10.146,4.219Q10.292,4.083 10.458,4.083Q10.646,4.083 10.781,4.219Q10.917,4.354 10.917,4.542V8.438L16.375,3Q16.5,2.854 16.688,2.854Q16.875,2.854 17,3Q17.146,3.125 17.146,3.312Q17.146,3.5 17,3.625L11.562,9.083H15.458Q15.646,9.083 15.781,9.229Q15.917,9.375 15.917,9.542Q15.917,9.729 15.781,9.865Q15.646,10 15.458,10ZM3,17Q2.854,16.875 2.854,16.688Q2.854,16.5 3,16.375L8.438,10.917H4.542Q4.354,10.917 4.219,10.771Q4.083,10.625 4.083,10.458Q4.083,10.271 4.219,10.135Q4.354,10 4.542,10H9.292Q9.562,10 9.781,10.219Q10,10.438 10,10.708V15.458Q10,15.646 9.854,15.781Q9.708,15.917 9.542,15.917Q9.354,15.917 9.219,15.781Q9.083,15.646 9.083,15.458V11.562L3.625,17Q3.5,17.146 3.312,17.146Q3.125,17.146 3,17Z" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="48dp"
|
|
||||||
android:height="48dp"
|
|
||||||
android:viewportWidth="48"
|
|
||||||
android:viewportHeight="48">
|
|
||||||
<path
|
|
||||||
android:pathData="M17.125,31.5C16.944,31.5 16.795,31.441 16.677,31.323C16.559,31.205 16.5,31.056 16.5,30.875V25.875C16.5,25.694 16.559,25.545 16.677,25.427C16.795,25.309 16.944,25.25 17.125,25.25C17.306,25.25 17.455,25.309 17.573,25.427C17.691,25.545 17.75,25.694 17.75,25.875V29.375L29.375,17.75H25.875C25.694,17.75 25.545,17.691 25.427,17.573C25.309,17.455 25.25,17.306 25.25,17.125C25.25,16.944 25.309,16.795 25.427,16.677C25.545,16.559 25.694,16.5 25.875,16.5H30.875C31.056,16.5 31.205,16.559 31.323,16.677C31.441,16.795 31.5,16.944 31.5,17.125V22.125C31.5,22.306 31.441,22.455 31.323,22.573C31.205,22.691 31.056,22.75 30.875,22.75C30.694,22.75 30.545,22.691 30.427,22.573C30.309,22.455 30.25,22.306 30.25,22.125V18.625L18.625,30.25H22.125C22.306,30.25 22.455,30.309 22.573,30.427C22.691,30.545 22.75,30.694 22.75,30.875C22.75,31.056 22.691,31.205 22.573,31.323C22.455,31.441 22.306,31.5 22.125,31.5H17.125Z"
|
|
||||||
android:fillColor="#C1C6CD" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="44dp"
|
|
||||||
android:height="44dp"
|
|
||||||
android:viewportWidth="44"
|
|
||||||
android:viewportHeight="44">
|
|
||||||
<path
|
|
||||||
android:pathData="M22.619,14.999L19.747,29.005H17.2C16.758,29.005 16.4,29.363 16.4,29.805C16.4,30.247 16.758,30.605 17.2,30.605H20.389C20.397,30.605 20.405,30.605 20.412,30.605H23.6C24.042,30.605 24.4,30.247 24.4,29.805C24.4,29.363 24.042,29.005 23.6,29.005H21.381L24.253,14.999H26.8C27.242,14.999 27.6,14.64 27.6,14.199C27.6,13.757 27.242,13.399 26.8,13.399H23.615C23.604,13.398 23.594,13.398 23.583,13.399H20.4C19.958,13.399 19.6,13.757 19.6,14.199C19.6,14.64 19.958,14.999 20.4,14.999H22.619Z"
|
|
||||||
android:fillColor="#8D97A5"
|
|
||||||
android:fillType="evenOdd" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="12dp"
|
|
||||||
android:height="12dp"
|
|
||||||
android:viewportWidth="12"
|
|
||||||
android:viewportHeight="12">
|
|
||||||
<path
|
|
||||||
android:pathData="M10.403,2.53C10.696,2.237 10.696,1.763 10.403,1.47C10.111,1.177 9.636,1.177 9.343,1.47L5.946,4.867L2.549,1.47C2.256,1.177 1.781,1.177 1.488,1.47C1.195,1.763 1.195,2.237 1.488,2.53L4.885,5.927L1.343,9.47C1.05,9.763 1.05,10.237 1.343,10.53C1.636,10.823 2.11,10.823 2.403,10.53L5.946,6.988L9.488,10.53C9.781,10.823 10.256,10.823 10.549,10.53C10.842,10.237 10.842,9.763 10.549,9.47L7.006,5.927L10.403,2.53Z"
|
|
||||||
android:fillColor="#8D97A5" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="12dp"
|
|
||||||
android:height="12dp"
|
|
||||||
android:viewportWidth="12"
|
|
||||||
android:viewportHeight="12">
|
|
||||||
<path
|
|
||||||
android:pathData="M2.649,7.355C2.655,7.316 2.672,7.28 2.699,7.251L8.404,1.064C8.479,0.983 8.605,0.978 8.686,1.053L9.863,2.138C9.944,2.213 9.949,2.339 9.874,2.42L4.169,8.607C4.143,8.636 4.108,8.656 4.069,8.665L2.668,9.005C2.529,9.039 2.401,8.92 2.423,8.779L2.649,7.355Z"
|
|
||||||
android:fillColor="#8D97A5" />
|
|
||||||
<path
|
|
||||||
android:pathData="M1.75,9.443C1.336,9.443 1,9.779 1,10.193C1,10.608 1.336,10.943 1.75,10.943L10.75,10.943C11.164,10.943 11.5,10.608 11.5,10.193C11.5,9.779 11.164,9.443 10.75,9.443L1.75,9.443Z"
|
|
||||||
android:fillColor="#8D97A5" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="36dp"
|
|
||||||
android:height="36dp"
|
|
||||||
android:viewportWidth="36"
|
|
||||||
android:viewportHeight="36">
|
|
||||||
<path
|
|
||||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
|
||||||
android:fillColor="#0DBD8B" />
|
|
||||||
<path
|
|
||||||
android:pathData="M9.818,18.787L14.705,23.818L26.182,12"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2.5"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#ffffff"
|
|
||||||
android:strokeLineCap="round" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="44dp"
|
|
||||||
android:height="44dp"
|
|
||||||
android:viewportWidth="44"
|
|
||||||
android:viewportHeight="44">
|
|
||||||
<path
|
|
||||||
android:pathData="M24.897,17.154C24.235,15.821 22.876,15.21 21.374,15.372C19.05,15.622 18.44,17.423 18.722,18.592C19.032,19.872 20.046,20.37 21.839,20.826H29.92C30.517,20.826 31,21.351 31,22C31,22.648 30.517,23.174 29.92,23.174H14.08C13.483,23.174 13,22.648 13,22C13,21.351 13.483,20.826 14.08,20.826H17.355C17.041,20.377 16.791,19.839 16.633,19.189C16.003,16.581 17.554,13.424 21.16,13.036C23.285,12.807 25.615,13.661 26.798,16.038C27.081,16.608 26.886,17.32 26.361,17.629C25.836,17.937 25.181,17.725 24.897,17.154Z"
|
|
||||||
android:fillColor="#8D97A5" />
|
|
||||||
<path
|
|
||||||
android:pathData="M25.427,25.13H27.67C27.888,26.306 27.721,27.56 27.05,28.632C26.114,30.125 24.37,31 21.985,31C18.076,31 16.279,28.584 15.912,26.986C15.768,26.357 16.12,25.72 16.698,25.563C17.277,25.406 17.863,25.788 18.008,26.417C18.119,26.902 19.002,28.652 21.985,28.652C23.907,28.652 24.854,27.965 25.264,27.31C25.642,26.707 25.708,25.909 25.427,25.13Z"
|
|
||||||
android:fillColor="#8D97A5" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="44dp"
|
|
||||||
android:height="44dp"
|
|
||||||
android:viewportWidth="44"
|
|
||||||
android:viewportHeight="44">
|
|
||||||
<group>
|
|
||||||
<clip-path android:pathData="M10,10h24v24h-24z" />
|
|
||||||
<path
|
|
||||||
android:pathData="M22.79,26.95C25.82,26.56 28,23.84 28,20.79V14.25C28,13.56 27.44,13 26.75,13C26.06,13 25.5,13.56 25.5,14.25V20.9C25.5,22.57 24.37,24.09 22.73,24.42C20.48,24.89 18.5,23.17 18.5,21V14.25C18.5,13.56 17.94,13 17.25,13C16.56,13 16,13.56 16,14.25V21C16,24.57 19.13,27.42 22.79,26.95ZM15,30C15,30.55 15.45,31 16,31H28C28.55,31 29,30.55 29,30C29,29.45 28.55,29 28,29H16C15.45,29 15,29.45 15,30Z"
|
|
||||||
android:fillColor="#8D97A5" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="20dp"
|
|
||||||
android:height="14dp"
|
|
||||||
android:viewportWidth="20"
|
|
||||||
android:viewportHeight="14">
|
|
||||||
<path
|
|
||||||
android:pathData="M19,5H1M19,1H1M10,9H1M10,13H1"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:strokeColor="#9E9E9E"
|
|
||||||
android:strokeLineCap="round" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="36dp"
|
|
||||||
android:height="36dp"
|
|
||||||
android:viewportWidth="36"
|
|
||||||
android:viewportHeight="36">
|
|
||||||
<path
|
|
||||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
|
||||||
android:fillColor="#F4F6FA" />
|
|
||||||
<path
|
|
||||||
android:pathData="M11.251,18H24.751M18.001,11.25V24.75"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#737D8C"
|
|
||||||
android:strokeLineCap="round" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="36dp"
|
|
||||||
android:height="36dp"
|
|
||||||
android:viewportWidth="36"
|
|
||||||
android:viewportHeight="36">
|
|
||||||
<path
|
|
||||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
|
||||||
android:fillColor="#0DBD8B" />
|
|
||||||
<path
|
|
||||||
android:pathData="M27.83,19.085L12.26,26.867C11.21,27.391 10.119,26.266 10.632,25.24C10.632,25.24 12.561,21.343 13.092,20.322C13.623,19.301 14.231,19.124 19.874,18.395C20.083,18.368 20.253,18.21 20.253,18C20.253,17.79 20.083,17.632 19.874,17.605C14.231,16.876 13.623,16.699 13.092,15.678C12.561,14.658 10.632,10.76 10.632,10.76C10.119,9.734 11.21,8.609 12.26,9.133L27.83,16.915C28.725,17.362 28.725,18.638 27.83,19.085Z"
|
|
||||||
android:fillColor="#ffffff" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/composerLayout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<!-- EAx: Remove android:background="@drawable/bg_composer_rich_bottom_sheet" from ^ -->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
There are issues here:
|
|
||||||
|
|
||||||
View class androidx.appcompat.widget.AppCompatImageView is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
|
||||||
View class io.element.android.wysiwyg.EditorEditText is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
|
||||||
layout_constraintHeight_default="wrap" is deprecated. Use layout_height="WRAP_CONTENT" and layout_constrainedHeight="true" instead.
|
|
||||||
View class com.google.android.material.textfield.TextInputEditText is an AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant).
|
|
||||||
layout_constraintHeight_default="wrap" is deprecated. Use layout_height="WRAP_CONTENT" and layout_constrainedHeight="true" instead.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/bottomSheetHandle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="5dp"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:background="@drawable/bottomsheet_handle" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/composerLayoutContent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/attachmentButton"
|
|
||||||
android:layout_width="56dp"
|
|
||||||
android:layout_height="60dp"
|
|
||||||
android:layout_margin="@dimen/composer_attachment_margin"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:contentDescription="@string/a11y_send_files"
|
|
||||||
android:src="@drawable/ic_rich_composer_add"
|
|
||||||
android:paddingStart="4dp"
|
|
||||||
app:layout_constraintVertical_bias="1"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/sendButton"
|
|
||||||
app:layout_goneMarginBottom="57dp"
|
|
||||||
tools:ignore="MissingPrefix,RtlSymmetry" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/composerEditTextOuterBorder"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:minHeight="40dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginHorizontal="12dp"
|
|
||||||
app:layout_constraintVertical_bias="0"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/sendButton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
|
||||||
android:id="@+id/composerModeIconView"
|
|
||||||
android:layout_width="11dp"
|
|
||||||
android:layout_height="11dp"
|
|
||||||
tools:src="@drawable/ic_quote"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/composerModeTitleView"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composerModeTitleView"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:tint="?vctr_content_tertiary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/composerModeTitleView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="6dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:paddingBottom="2dp"
|
|
||||||
android:fontFamily="sans-serif-medium"
|
|
||||||
tools:text="Editing"
|
|
||||||
style="@style/BottomSheetItemTime"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/composerModeIconView" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/composerModeCloseView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:src="@drawable/ic_composer_rich_text_editor_close"
|
|
||||||
android:background="?android:selectableItemBackground"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:contentDescription="@string/action_close"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/composerModeIconView"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/composerModeBarrier"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:barrierDirection="bottom"
|
|
||||||
app:constraint_referenced_ids="composerModeIconView,composerModeTitleView,composerModeCloseView" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Group
|
|
||||||
android:id="@+id/composerModeGroup"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible"
|
|
||||||
app:constraint_referenced_ids="composerModeIconView,composerModeTitleView,composerModeCloseView" />
|
|
||||||
|
|
||||||
<io.element.android.wysiwyg.EditorEditText
|
|
||||||
android:id="@+id/richTextComposerEditText"
|
|
||||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintHeight_default="wrap"
|
|
||||||
android:gravity="top"
|
|
||||||
android:hint="@string/rich_text_editor_composer_placeholder"
|
|
||||||
android:nextFocusLeft="@id/richTextComposerEditText"
|
|
||||||
android:nextFocusUp="@id/richTextComposerEditText"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:imeOptions="flagNoExtractUi"
|
|
||||||
app:layout_constraintVertical_bias="0"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
|
|
||||||
tools:text="@tools:sample/lorem/random" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/plainTextComposerEditText"
|
|
||||||
style="@style/Widget.Vector.EditText.RichTextComposer"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintHeight_default="wrap"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:hint="@string/rich_text_editor_composer_placeholder"
|
|
||||||
android:nextFocusLeft="@id/plainTextComposerEditText"
|
|
||||||
android:nextFocusUp="@id/plainTextComposerEditText"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:gravity="top"
|
|
||||||
android:imeOptions="flagNoExtractUi"
|
|
||||||
app:layout_constraintVertical_bias="0"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
|
|
||||||
tools:text="@tools:sample/lorem/random" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/composerFullScreenButton"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/composerModeBarrier"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintVertical_bias="0"
|
|
||||||
android:src="@drawable/ic_composer_full_screen"
|
|
||||||
android:background="?android:attr/selectableItemBackground"
|
|
||||||
android:contentDescription="@string/rich_text_editor_full_screen_toggle" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/sendButton"
|
|
||||||
android:layout_width="56dp"
|
|
||||||
android:layout_height="60dp"
|
|
||||||
android:paddingEnd="4dp"
|
|
||||||
android:contentDescription="@string/action_send"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_rich_composer_send"
|
|
||||||
android:visibility="invisible"
|
|
||||||
android:background="?android:selectableItemBackground"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="1"
|
|
||||||
tools:ignore="MissingPrefix,RtlSymmetry"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<HorizontalScrollView
|
|
||||||
android:id="@+id/richTextMenuScrollView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minHeight="52dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:fillViewport="true">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/richTextMenu"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingVertical="4dp">
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_marginHorizontal="2dp"
|
|
||||||
android:background="@drawable/bg_rich_text_menu_button"
|
|
||||||
app:tint="@color/selector_rich_text_menu_icon"
|
|
||||||
tools:src="@drawable/ic_composer_bold"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:7441f73e8567bbac360867f9b860621ec4766a67d5295d04cda45a09f942d0b5
|
|
||||||
size 47865
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1207e4830152c3387f9464fa4f10bd92f605d14b37f1c556d8e610060246ac2e
|
||||||
|
size 14094
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:127ec47cd443c96270f07d6d3ca132a184c6ad99ab8ade04dc3d60f5a6f555da
|
||||||
|
size 13575
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f44fe7919578afe43e77b414f294e2ae7f1761c9528feeab548303428bfeba43
|
|
||||||
size 46117
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8922908539dc4978a23f72342e46a4c259322559d1f0dbe978dcf3ae8a4e79bf
|
||||||
|
size 66820
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1a5b67e808ef8d171167e7fb241ba76b7e6d5ae48d193f6384bf3e2c4a0743f3
|
||||||
|
size 66423
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:97fb04ff64617a6ba8c683a39921864bded4b05269c958aa4c91fda0bc21963a
|
||||||
|
size 39298
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fd9f6eaf4bfc4b226f1fde5bdaf0262b95e3f9ac2e7e0d90648d72ef0add11d2
|
||||||
|
size 37492
|
||||||
Loading…
Add table
Add a link
Reference in a new issue