Merge branch 'develop' of https://github.com/vector-im/element-x-android into feature/dla/emojibase_integration
This commit is contained in:
commit
64c156bd4f
73 changed files with 1608 additions and 253 deletions
|
|
@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
|
|
@ -64,6 +65,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
@Assisted plugins: List<Plugin>,
|
||||
private val sendLocationEntryPoint: SendLocationEntryPoint,
|
||||
private val showLocationEntryPoint: ShowLocationEntryPoint,
|
||||
private val createPollEntryPoint: CreatePollEntryPoint,
|
||||
) : BackstackNode<MessagesFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Messages,
|
||||
|
|
@ -101,6 +103,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object SendLocation : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object CreatePoll : NavTarget
|
||||
}
|
||||
|
||||
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
||||
|
|
@ -140,6 +145,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
override fun onSendLocationClicked() {
|
||||
backstack.push(NavTarget.SendLocation)
|
||||
}
|
||||
|
||||
override fun onCreatePollClicked() {
|
||||
backstack.push(NavTarget.CreatePoll)
|
||||
}
|
||||
}
|
||||
createNode<MessagesNode>(buildContext, listOf(callback))
|
||||
}
|
||||
|
|
@ -179,6 +188,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
|||
NavTarget.SendLocation -> {
|
||||
sendLocationEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.CreatePoll -> {
|
||||
createPollEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
fun onForwardEventClicked(eventId: EventId)
|
||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||
fun onSendLocationClicked()
|
||||
fun onCreatePollClicked()
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -99,6 +100,10 @@ class MessagesNode @AssistedInject constructor(
|
|||
callback?.onSendLocationClicked()
|
||||
}
|
||||
|
||||
private fun onCreatePollClicked() {
|
||||
callback?.onCreatePollClicked()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -110,6 +115,7 @@ class MessagesNode @AssistedInject constructor(
|
|||
onPreviewAttachments = this::onPreviewAttachments,
|
||||
onUserDataClicked = this::onUserDataClicked,
|
||||
onSendLocationClicked = this::onSendLocationClicked,
|
||||
onCreatePollClicked = this::onCreatePollClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ fun MessagesView(
|
|||
onUserDataClicked: (UserId) -> Unit,
|
||||
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LogCompositions(tag = "MessagesScreen", msg = "Root")
|
||||
|
|
@ -175,6 +176,7 @@ fun MessagesView(
|
|||
onReactionLongClicked = ::onEmojiReactionLongClicked,
|
||||
onMoreReactionsClicked = ::onMoreReactionsClicked,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
onSwipeToReply = { targetEvent ->
|
||||
state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent))
|
||||
},
|
||||
|
|
@ -266,6 +268,7 @@ private fun MessagesViewContent(
|
|||
onMessageLongClicked: (TimelineItem.Event) -> Unit,
|
||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||
) {
|
||||
|
|
@ -294,6 +297,7 @@ private fun MessagesViewContent(
|
|||
MessageComposerView(
|
||||
state = state.composerState,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(Alignment.Bottom)
|
||||
|
|
@ -400,5 +404,6 @@ private fun ContentToPreview(state: MessagesState) {
|
|||
onPreviewAttachments = {},
|
||||
onUserDataClicked = {},
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
||||
|
|
@ -96,6 +97,22 @@ class ActionListPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
is TimelineItemPollContent -> {
|
||||
buildList {
|
||||
if (timelineItem.content.canBeCopied()) {
|
||||
add(TimelineItemAction.Copy)
|
||||
}
|
||||
if (buildMeta.isDebuggable) {
|
||||
add(TimelineItemAction.Developer)
|
||||
}
|
||||
if (!timelineItem.isMine) {
|
||||
add(TimelineItemAction.ReportContent)
|
||||
}
|
||||
if (timelineItem.isMine || userCanRedact) {
|
||||
add(TimelineItemAction.Redact)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> buildList<TimelineItemAction> {
|
||||
if (timelineItem.isRemote) {
|
||||
// Can only reply or forward messages already uploaded to the server
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.ListItem
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material.icons.filled.Collections
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
|
|
@ -52,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||
internal fun AttachmentsBottomSheet(
|
||||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val localView = LocalView.current
|
||||
|
|
@ -85,6 +87,7 @@ internal fun AttachmentsBottomSheet(
|
|||
AttachmentSourcePickerMenu(
|
||||
state = state,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +98,7 @@ internal fun AttachmentsBottomSheet(
|
|||
internal fun AttachmentSourcePickerMenu(
|
||||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -131,6 +135,16 @@ internal fun AttachmentSourcePickerMenu(
|
|||
text = { Text(stringResource(R.string.screen_room_attachment_source_location)) },
|
||||
)
|
||||
}
|
||||
if (state.canCreatePoll) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll)
|
||||
onCreatePollClicked()
|
||||
},
|
||||
icon = { Icon(Icons.Default.BarChart, null) },
|
||||
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,5 +156,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
|
|||
canShareLocation = true,
|
||||
),
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ sealed interface MessageComposerEvents {
|
|||
data object PhotoFromCamera : PickAttachmentSource
|
||||
data object VideoFromCamera : PickAttachmentSource
|
||||
data object Location : PickAttachmentSource
|
||||
data object Poll : PickAttachmentSource
|
||||
}
|
||||
data object CancelSendAttachment : MessageComposerEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ class MessageComposerPresenter @Inject constructor(
|
|||
canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing)
|
||||
}
|
||||
|
||||
val canCreatePoll = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
canCreatePoll.value = featureFlagService.isFeatureEnabled(FeatureFlags.Polls)
|
||||
}
|
||||
|
||||
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
|
||||
handlePickedMedia(attachmentsState, uri, mimeType)
|
||||
}
|
||||
|
|
@ -179,6 +184,10 @@ class MessageComposerPresenter @Inject constructor(
|
|||
showAttachmentSourcePicker = false
|
||||
// Navigation to the location picker screen is done at the view layer
|
||||
}
|
||||
MessageComposerEvents.PickAttachmentSource.Poll -> {
|
||||
showAttachmentSourcePicker = false
|
||||
// Navigation to the create poll screen is done at the view layer
|
||||
}
|
||||
is MessageComposerEvents.CancelSendAttachment -> {
|
||||
ongoingSendAttachmentJob.value?.let {
|
||||
it.cancel()
|
||||
|
|
@ -195,6 +204,7 @@ class MessageComposerPresenter @Inject constructor(
|
|||
mode = messageComposerContext.composerMode,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
canShareLocation = canShareLocation.value,
|
||||
canCreatePoll = canCreatePoll.value,
|
||||
attachmentsState = attachmentsState.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ data class MessageComposerState(
|
|||
val mode: MessageComposerMode,
|
||||
val showAttachmentSourcePicker: Boolean,
|
||||
val canShareLocation: Boolean,
|
||||
val canCreatePoll: Boolean,
|
||||
val attachmentsState: AttachmentsState,
|
||||
val eventSink: (MessageComposerEvents) -> Unit
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ fun aMessageComposerState(
|
|||
mode: MessageComposerMode = MessageComposerMode.Normal(content = ""),
|
||||
showAttachmentSourcePicker: Boolean = false,
|
||||
canShareLocation: Boolean = true,
|
||||
canCreatePoll: Boolean = true,
|
||||
attachmentsState: AttachmentsState = AttachmentsState.None,
|
||||
) = MessageComposerState(
|
||||
text = text,
|
||||
|
|
@ -41,6 +42,7 @@ fun aMessageComposerState(
|
|||
mode = mode,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
canShareLocation = canShareLocation,
|
||||
canCreatePoll = canCreatePoll,
|
||||
attachmentsState = attachmentsState,
|
||||
eventSink = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import io.element.android.libraries.textcomposer.TextComposer
|
|||
fun MessageComposerView(
|
||||
state: MessageComposerState,
|
||||
onSendLocationClicked: () -> Unit,
|
||||
onCreatePollClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onFullscreenToggle() {
|
||||
|
|
@ -59,6 +60,7 @@ fun MessageComposerView(
|
|||
AttachmentsBottomSheet(
|
||||
state = state,
|
||||
onSendLocationClicked = onSendLocationClicked,
|
||||
onCreatePollClicked = onCreatePollClicked,
|
||||
)
|
||||
|
||||
TextComposer(
|
||||
|
|
@ -88,6 +90,7 @@ internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerSta
|
|||
private fun ContentToPreview(state: MessageComposerState) {
|
||||
MessageComposerView(
|
||||
state = state,
|
||||
onSendLocationClicked = {}
|
||||
onSendLocationClicked = {},
|
||||
onCreatePollClicked = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -35,6 +37,7 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText
|
|||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
|
||||
@Composable
|
||||
fun TimelineItemTextView(
|
||||
|
|
@ -45,31 +48,33 @@ fun TimelineItemTextView(
|
|||
onTextClicked: () -> Unit = {},
|
||||
onTextLongClicked: () -> Unit = {},
|
||||
) {
|
||||
val htmlDocument = content.htmlDocument
|
||||
if (htmlDocument != null) {
|
||||
// For now we ignore the extra padding for html content, so add some spacing
|
||||
// below the content (as previous behavior)
|
||||
Column(modifier = modifier) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = Modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
val textWithPadding = remember(content.body) {
|
||||
content.body + extraPadding.getStr(16.sp).toAnnotatedString()
|
||||
CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) {
|
||||
val htmlDocument = content.htmlDocument
|
||||
if (htmlDocument != null) {
|
||||
// For now we ignore the extra padding for html content, so add some spacing
|
||||
// below the content (as previous behavior)
|
||||
Column(modifier = modifier) {
|
||||
HtmlDocument(
|
||||
document = htmlDocument,
|
||||
modifier = Modifier,
|
||||
onTextClicked = onTextClicked,
|
||||
onTextLongClicked = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
} else {
|
||||
Box(modifier) {
|
||||
val textWithPadding = remember(content.body) {
|
||||
content.body + extraPadding.getStr(16.sp).toAnnotatedString()
|
||||
}
|
||||
ClickableLinkText(
|
||||
text = textWithPadding,
|
||||
onClick = onTextClicked,
|
||||
onLongClick = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
ClickableLinkText(
|
||||
text = textWithPadding,
|
||||
onClick = onTextClicked,
|
||||
onLongClick = onTextLongClicked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ class TimelineItemContentPollFactory @Inject constructor(
|
|||
return TimelineItemPollContent(
|
||||
question = content.question,
|
||||
answerItems = answerItems,
|
||||
votes = content.votes,
|
||||
pollKind = content.kind,
|
||||
isEnded = isEndedPoll,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,13 +17,11 @@
|
|||
package io.element.android.features.messages.impl.timeline.model.event
|
||||
|
||||
import io.element.android.features.poll.api.PollAnswerItem
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
data class TimelineItemPollContent(
|
||||
val question: String,
|
||||
val answerItems: List<PollAnswerItem>,
|
||||
val votes: Map<String, List<UserId>>,
|
||||
val pollKind: PollKind,
|
||||
val isEnded: Boolean,
|
||||
) : TimelineItemEventContent {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,5 @@ fun aTimelineItemPollContent(): TimelineItemPollContent {
|
|||
question = "What type of food should we have at the party?",
|
||||
answerItems = aPollAnswerItemList(),
|
||||
isEnded = false,
|
||||
votes = emptyMap(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue