Merge branch 'develop' into feature/fga/timeline_thread_decoration

This commit is contained in:
ganfra
2023-09-13 11:42:46 +02:00
124 changed files with 566 additions and 304 deletions

1
changelog.d/1173.bugfix Normal file
View File

@@ -0,0 +1 @@
Reply action: harmonize conditions in bottom sheet and swipe to reply.

1
changelog.d/1289.feature Normal file
View File

@@ -0,0 +1 @@
[Rich text editor] Add feature flag for rich text editor. Markdown support can now be enabled by disabling the rich text editor.

View File

@@ -26,10 +26,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@@ -119,9 +119,8 @@ class SendLocationPresenter @Inject constructor(
Composer(
inThread = messageComposerContext.composerMode.inThread,
isEditing = messageComposerContext.composerMode.isEditing,
isLocation = true,
isReply = messageComposerContext.composerMode.isReply,
locationType = Composer.LocationType.PinDrop,
messageType = Composer.MessageType.LocationPin,
)
)
}
@@ -138,9 +137,8 @@ class SendLocationPresenter @Inject constructor(
Composer(
inThread = messageComposerContext.composerMode.inThread,
isEditing = messageComposerContext.composerMode.isEditing,
isLocation = true,
isReply = messageComposerContext.composerMode.isReply,
locationType = Composer.LocationType.MyLocation,
messageType = Composer.MessageType.LocationUser,
)
)
}

View File

@@ -308,9 +308,8 @@ class SendLocationPresenterTest {
Composer(
inThread = false,
isEditing = false,
isLocation = true,
isReply = false,
locationType = Composer.LocationType.MyLocation,
messageType = Composer.MessageType.LocationUser,
)
)
}
@@ -365,9 +364,8 @@ class SendLocationPresenterTest {
Composer(
inThread = false,
isEditing = false,
isLocation = true,
isReply = false,
locationType = Composer.LocationType.PinDrop,
messageType = Composer.MessageType.LocationPin,
)
)
}
@@ -412,9 +410,8 @@ class SendLocationPresenterTest {
Composer(
inThread = false,
isEditing = true,
isLocation = true,
isReply = false,
locationType = Composer.LocationType.PinDrop,
messageType = Composer.MessageType.LocationPin,
)
)
}

View File

@@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
@@ -65,6 +66,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@@ -74,6 +77,7 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.matrix.ui.room.canRedactAsState
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -92,6 +96,8 @@ class MessagesPresenter @AssistedInject constructor(
private val messageSummaryFormatter: MessageSummaryFormatter,
private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper,
private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService,
@Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> {
@@ -140,6 +146,11 @@ class MessagesPresenter @AssistedInject constructor(
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
}
var enableTextFormatting by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
enableTextFormatting = featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)
}
fun handleEvents(event: MessagesEvents) {
when (event) {
is MessagesEvents.HandleAction -> {
@@ -175,6 +186,7 @@ class MessagesPresenter @AssistedInject constructor(
snackbarMessage = snackbarMessage,
showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting,
eventSink = { handleEvents(it) }
)
}
@@ -247,11 +259,15 @@ class MessagesPresenter @AssistedInject constructor(
}
}
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
private suspend fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
val composerMode = MessageComposerMode.Edit(
targetEvent.eventId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
it.htmlBody ?: it.body
if (featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)) {
it.htmlBody ?: it.body
} else {
it.body
}
}.orEmpty(),
targetEvent.transactionId,
)
@@ -321,8 +337,10 @@ class MessagesPresenter @AssistedInject constructor(
}
private suspend fun handleEndPollAction(event: TimelineItem.Event) {
event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") }
// TODO Polls: Send poll end analytic
event.eventId?.let {
room.endPoll(it, "The poll with event id: $it has ended.")
analyticsService.capture(PollEnd())
}
}
private suspend fun handleCopyContents(event: TimelineItem.Event) {

View File

@@ -45,5 +45,6 @@ data class MessagesState(
val snackbarMessage: SnackbarMessage?,
val inviteProgress: Async<Unit>,
val showReinvitePrompt: Boolean,
val enableTextFormatting: Boolean,
val eventSink: (MessagesEvents) -> Unit
)

View File

@@ -82,5 +82,6 @@ fun aMessagesState() = MessagesState(
snackbarMessage = null,
inviteProgress = Async.Uninitialized,
showReinvitePrompt = false,
enableTextFormatting = true,
eventSink = {}
)

View File

@@ -123,7 +123,13 @@ fun MessagesView(
fun onMessageLongClicked(event: TimelineItem.Event) {
Timber.v("OnMessageLongClicked= ${event.id}")
localView.hideKeyboard()
state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event, state.userHasPermissionToRedact))
state.actionListState.eventSink(
ActionListEvents.ComputeForMessage(
event = event,
canRedact = state.userHasPermissionToRedact,
canSendMessage = state.userHasPermissionToSendMessage,
)
)
}
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
@@ -203,8 +209,8 @@ fun MessagesView(
CustomReactionBottomSheet(
state = state.customReactionState,
onEmojiSelected = { eventId, emoji ->
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId))
state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet)
}
)
@@ -298,6 +304,7 @@ private fun MessagesViewContent(
state = state.composerState,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
enableTextFormatting = state.enableTextFormatting,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(Alignment.Bottom)

View File

@@ -20,5 +20,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
sealed interface ActionListEvents {
data object Clear : ActionListEvents
data class ComputeForMessage(val event: TimelineItem.Event, val canRedact: Boolean) : ActionListEvents
data class ComputeForMessage(
val event: TimelineItem.Event,
val canRedact: Boolean,
val canSendMessage: Boolean,
) : ActionListEvents
}

View File

@@ -62,6 +62,7 @@ class ActionListPresenter @Inject constructor(
is ActionListEvents.ComputeForMessage -> localCoroutineScope.computeForMessage(
timelineItem = event.event,
userCanRedact = event.canRedact,
userCanSendMessage = event.canSendMessage,
target = target,
)
}
@@ -77,6 +78,7 @@ class ActionListPresenter @Inject constructor(
private fun CoroutineScope.computeForMessage(
timelineItem: TimelineItem.Event,
userCanRedact: Boolean,
userCanSendMessage: Boolean,
target: MutableState<ActionListState.Target>
) = launch {
target.value = ActionListState.Target.Loading(timelineItem)
@@ -101,7 +103,8 @@ class ActionListPresenter @Inject constructor(
buildList {
val isMineOrCanRedact = timelineItem.isMine || userCanRedact
// TODO Poll: Reply to poll
// TODO Poll: Reply to poll. Ensure to update `fun TimelineItemEventContent.canBeReplied()`
// when touching this
// if (timelineItem.isRemote) {
// // Can only reply or forward messages already uploaded to the server
// add(TimelineItemAction.Reply)
@@ -126,7 +129,9 @@ class ActionListPresenter @Inject constructor(
else -> buildList<TimelineItemAction> {
if (timelineItem.isRemote) {
// Can only reply or forward messages already uploaded to the server
add(TimelineItemAction.Reply)
if (userCanSendMessage) {
add(TimelineItemAction.Reply)
}
add(TimelineItemAction.Forward)
}
if (timelineItem.isMine && timelineItem.isTextMessage) {

View File

@@ -35,5 +35,5 @@ sealed class TimelineItemAction(
data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit)
data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode)
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true)
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll)
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.PollEnd)
}

View File

@@ -55,6 +55,7 @@ internal fun AttachmentsBottomSheet(
state: MessageComposerState,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
) {
val localView = LocalView.current
@@ -87,6 +88,7 @@ internal fun AttachmentsBottomSheet(
) {
AttachmentSourcePickerMenu(
state = state,
enableTextFormatting = enableTextFormatting,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
)
@@ -100,6 +102,7 @@ internal fun AttachmentSourcePickerMenu(
state: MessageComposerState,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
) {
Column(
@@ -146,11 +149,13 @@ internal fun AttachmentSourcePickerMenu(
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
)
}
ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
icon = { Icon(Icons.Default.FormatColorText, null) },
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
)
if (enableTextFormatting) {
ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
icon = { Icon(Icons.Default.FormatColorText, null) },
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
)
}
}
}
@@ -163,5 +168,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview {
),
onSendLocationClicked = {},
onCreatePollClicked = {},
enableTextFormatting = true,
)
}

View File

@@ -148,14 +148,6 @@ class MessageComposerPresenter @Inject constructor(
)
is MessageComposerEvents.SetMode -> {
messageComposerContext.composerMode = event.composerMode
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
isEditing = messageComposerContext.composerMode.isEditing,
isReply = messageComposerContext.composerMode.isReply,
isLocation = false,
)
)
}
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
showAttachmentSourcePicker = true
@@ -238,6 +230,14 @@ class MessageComposerPresenter @Inject constructor(
message.html,
)
}
analyticsService.capture(
Composer(
inThread = capturedMode.inThread,
isEditing = capturedMode.isEditing,
isReply = capturedMode.isReply,
messageType = Composer.MessageType.Text, // Set proper type when we'll be sending other types of messages.
)
)
}
private fun CoroutineScope.sendAttachment(

View File

@@ -31,6 +31,7 @@ fun MessageComposerView(
state: MessageComposerState,
onSendLocationClicked: () -> Unit,
onCreatePollClicked: () -> Unit,
enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
) {
fun onFullscreenToggle() {
@@ -62,6 +63,7 @@ fun MessageComposerView(
state = state,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
enableTextFormatting = enableTextFormatting,
)
TextComposer(
@@ -74,6 +76,7 @@ fun MessageComposerView(
onResetComposerMode = ::onCloseSpecialMode,
onAddAttachment = ::onAddAttachment,
onDismissTextFormatting = ::onDismissTextFormatting,
enableTextFormatting = enableTextFormatting,
onError = ::onError,
)
}
@@ -95,5 +98,6 @@ private fun ContentToPreview(state: MessageComposerState) {
state = state,
onSendLocationClicked = {},
onCreatePollClicked = {},
enableTextFormatting = true,
)
}

View File

@@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.architecture.Presenter
@@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@@ -51,6 +53,7 @@ class TimelinePresenter @Inject constructor(
private val room: MatrixRoom,
private val dispatchers: CoroutineDispatchers,
private val appScope: CoroutineScope,
private val analyticsService: AnalyticsService,
) : Presenter<TimelineState> {
private val timeline = room.timeline
@@ -93,7 +96,7 @@ class TimelinePresenter @Inject constructor(
pollStartId = event.pollStartId,
answers = listOf(event.answerId),
)
// TODO Polls: Send poll vote analytic
analyticsService.capture(PollVote())
}
}
}
@@ -116,7 +119,7 @@ class TimelinePresenter @Inject constructor(
return TimelineState(
highlightedEventId = highlightedEventId.value,
canReply = userHasPermissionToSendMessage,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
paginationState = paginationState,
timelineItems = timelineItems,
hasNewItems = hasNewItems.value,

View File

@@ -26,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
data class TimelineState(
val timelineItems: ImmutableList<TimelineItem>,
val highlightedEventId: EventId?,
val canReply: Boolean,
val userHasPermissionToSendMessage: Boolean,
val paginationState: MatrixTimeline.PaginationState,
val hasNewItems: Boolean,
val eventSink: (TimelineEvents) -> Unit

View File

@@ -44,7 +44,7 @@ fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf
timelineItems = timelineItems,
paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true),
highlightedEventId = null,
canReply = true,
userHasPermissionToSendMessage = true,
hasNewItems = false,
eventSink = {},
)

View File

@@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
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.TimelineItemStateContent
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
@@ -119,7 +120,7 @@ fun TimelineView(
TimelineItemRow(
timelineItem = timelineItem,
highlightedItem = state.highlightedEventId?.value,
canReply = state.canReply,
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked,
onUserDataClick = onUserDataClicked,
@@ -156,7 +157,7 @@ fun TimelineView(
fun TimelineItemRow(
timelineItem: TimelineItem,
highlightedItem: String?,
canReply: Boolean,
userHasPermissionToSendMessage: Boolean,
onUserDataClick: (UserId) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
@@ -189,7 +190,7 @@ fun TimelineItemRow(
TimelineItemEventRow(
event = timelineItem,
isHighlighted = highlightedItem == timelineItem.identifier(),
canReply = canReply,
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onUserDataClick = onUserDataClick,
@@ -228,7 +229,7 @@ fun TimelineItemRow(
TimelineItemRow(
timelineItem = subGroupEvent,
highlightedItem = highlightedItem,
canReply = false,
userHasPermissionToSendMessage = false,
onClick = onClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,

View File

@@ -18,10 +18,14 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@@ -69,3 +73,10 @@ fun ExtraPadding.getStr(fontSize: TextUnit): String {
// A space and some unbreakable spaces
return " " + "\u00A0".repeat(nbOfSpaces)
}
@Composable
fun ExtraPadding.getDpSize(): Dp {
if (nbChars == 0) return 0.dp
val timestampFontSize = ElementTheme.typography.fontBodyXsRegular.fontSize // 11.sp
return nbChars * timestampFontSize.toDp() / 3
}

View File

@@ -18,9 +18,6 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.interaction.MutableInteractionSource
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
@@ -28,7 +25,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.impl.timeline.components.html.HtmlDocument
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
@@ -51,18 +47,14 @@ fun TimelineItemTextView(
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))
}
HtmlDocument(
document = htmlDocument,
extraPadding = extraPadding,
modifier = modifier,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
interactionSource = interactionSource
)
} else {
Box(modifier) {
val textWithPadding = remember(content.body) {

View File

@@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
@@ -53,13 +55,18 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.messages.impl.timeline.components.event.ExtraPadding
import io.element.android.features.messages.impl.timeline.components.event.getDpSize
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
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.toDp
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.theme.LinkColor
import kotlinx.collections.immutable.persistentMapOf
import org.jsoup.nodes.Document
@@ -72,18 +79,28 @@ private const val CHIP_ID = "chip"
@Composable
fun HtmlDocument(
document: Document,
extraPadding: ExtraPadding,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
HtmlBody(
body = document.body(),
interactionSource = interactionSource,
FlowRow(
modifier = modifier,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
)
) {
HtmlBody(
body = document.body(),
interactionSource = interactionSource,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
)
Spacer(
modifier = Modifier.size(
width = extraPadding.getDpSize(),
height = ElementTheme.typography.fontBodyXsRegular.fontSize.toDp() * 1.25f
)
)
}
}
@Composable
@@ -603,5 +620,9 @@ internal fun HtmlDocumentDarkPreview(@PreviewParameter(DocumentProvider::class)
@Composable
private fun ContentToPreview(document: Document) {
HtmlDocument(document, remember { MutableInteractionSource() })
HtmlDocument(
document = document,
extraPadding = noExtraPadding,
interactionSource = remember { MutableInteractionSource() }
)
}

View File

@@ -34,6 +34,18 @@ fun TimelineItemEventContent.canBeCopied(): Boolean =
else -> false
}
/**
* Determine if the event content can be replied to.
* Note: it should match the logic in [io.element.android.features.messages.impl.actionlist.ActionListPresenter].
*/
fun TimelineItemEventContent.canBeRepliedTo(): Boolean =
when (this) {
is TimelineItemRedactedContent,
is TimelineItemStateContent,
is TimelineItemPollContent -> false
else -> true
}
/**
* Return true if user can react (i.e. send a reaction) on the event content.
*/

View File

@@ -21,6 +21,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.messages.fixtures.aMessageEvent
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.InviteDialogAction
@@ -575,7 +576,11 @@ class MessagesPresenterTest {
@Test
fun `present - handle poll end`() = runTest {
val room = FakeMatrixRoom()
val presenter = createMessagePresenter(matrixRoom = room)
val analyticsService = FakeAnalyticsService()
val presenter = createMessagePresenter(
matrixRoom = room,
analyticsService = analyticsService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -586,7 +591,8 @@ class MessagesPresenterTest {
assertThat(room.endPollInvocations.size).isEqualTo(1)
assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.")
// TODO Polls: Test poll end analytic
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd())
}
}
@@ -595,6 +601,7 @@ class MessagesPresenterTest {
matrixRoom: MatrixRoom = FakeMatrixRoom(),
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): MessagesPresenter {
val messageComposerPresenter = MessageComposerPresenter(
appCoroutineScope = this,
@@ -604,7 +611,7 @@ class MessagesPresenterTest {
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
snackbarDispatcher = SnackbarDispatcher(),
analyticsService = FakeAnalyticsService(),
analyticsService = analyticsService,
messageComposerContext = MessageComposerContextImpl(),
richTextEditorStateFactory = TestRichTextEditorStateFactory(),
@@ -613,13 +620,15 @@ class MessagesPresenterTest {
timelineItemsFactory = aTimelineItemsFactory(),
room = matrixRoom,
dispatchers = coroutineDispatchers,
appScope = this
appScope = this,
analyticsService = analyticsService,
)
val buildMeta = aBuildMeta()
val actionListPresenter = ActionListPresenter(buildMeta = buildMeta)
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
val featureFlagsService = FakeFeatureFlagService(mapOf(FeatureFlags.RichTextEditor.key to true))
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
@@ -633,6 +642,8 @@ class MessagesPresenterTest {
messageSummaryFormatter = FakeMessageSummaryFormatter(),
navigator = navigator,
clipboardHelper = clipboardHelper,
analyticsService = analyticsService,
featureFlagService = featureFlagsService,
dispatchers = coroutineDispatchers,
)
}

View File

@@ -64,7 +64,7 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -89,7 +89,7 @@ class ActionListPresenterTest {
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -117,7 +117,7 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -138,6 +138,37 @@ class ActionListPresenterTest {
}
}
@Test
fun `present - compute for others message cannot sent message`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = false))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
messageEvent,
persistentListOf(
TimelineItemAction.Forward,
TimelineItemAction.Copy,
TimelineItemAction.Developer,
TimelineItemAction.ReportContent,
)
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
}
}
@Test
fun `present - compute for others message and can redact`() = runTest {
val presenter = anActionListPresenter(isBuildDebuggable = true)
@@ -149,7 +180,7 @@ class ActionListPresenterTest {
isMine = false,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, true))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = true, canSendMessage = true))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@@ -180,7 +211,7 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -213,7 +244,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemImageContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -244,7 +275,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -273,7 +304,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemStateEventContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(stateEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -301,7 +332,7 @@ class ActionListPresenterTest {
isMine = true,
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false)
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
// val loadingState = awaitItem()
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
val successState = awaitItem()
@@ -338,10 +369,10 @@ class ActionListPresenterTest {
content = TimelineItemRedactedContent,
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
assertThat(awaitItem().target).isInstanceOf(ActionListState.Target.Success::class.java)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(redactedEvent, canRedact = false, canSendMessage = true))
awaitItem().run {
assertThat(target).isEqualTo(ActionListState.Target.None)
assertThat(displayEmojiReactions).isFalse()
@@ -362,7 +393,7 @@ class ActionListPresenterTest {
content = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@@ -389,7 +420,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemPollContent(),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
@@ -415,7 +446,7 @@ class ActionListPresenterTest {
isMine = true,
content = aTimelineItemPollContent(isEnded = true),
)
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false))
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, canRedact = false, canSendMessage = true))
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(

View File

@@ -20,6 +20,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
@@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.aMessageContent
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitWithLatch
import io.element.android.tests.testutils.testCoroutineDispatchers
@@ -260,7 +262,11 @@ class TimelinePresenterTest {
@Test
fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest {
val room = FakeMatrixRoom()
val presenter = createTimelinePresenter(room)
val analyticsService = FakeAnalyticsService()
val presenter = createTimelinePresenter(
room = room,
analyticsService = analyticsService,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@@ -271,7 +277,8 @@ class TimelinePresenterTest {
assertThat(room.sendPollResponseInvocations.size).isEqualTo(1)
assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId"))
assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID)
// TODO Polls: Test poll vote analytic
assertThat(analyticsService.capturedEvents.size).isEqualTo(1)
assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote())
}
private fun TestScope.createTimelinePresenter(
@@ -282,18 +289,21 @@ class TimelinePresenterTest {
timelineItemsFactory = timelineItemsFactory,
room = FakeMatrixRoom(matrixTimeline = timeline),
dispatchers = testCoroutineDispatchers(),
appScope = this
appScope = this,
analyticsService = FakeAnalyticsService(),
)
}
private fun TestScope.createTimelinePresenter(
room: MatrixRoom,
analyticsService: FakeAnalyticsService = FakeAnalyticsService(),
): TimelinePresenter {
return TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
room = room,
dispatchers = testCoroutineDispatchers(),
appScope = this
appScope = this,
analyticsService = analyticsService,
)
}
}

View File

@@ -24,8 +24,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Poll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -86,13 +84,13 @@ internal fun PollTitle(
) {
if (isPollEnded) {
Icon(
resourceId = VectorIcons.EndPoll,
resourceId = VectorIcons.PollEnd,
contentDescription = null,
modifier = Modifier.size(22.dp)
)
} else {
Icon(
imageVector = Icons.Outlined.Poll,
resourceId = VectorIcons.Poll,
contentDescription = null,
modifier = Modifier.size(22.dp)
)

View File

@@ -39,6 +39,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.services.analytics.api)
implementation(projects.features.messages.api)
implementation(projects.libraries.uiStrings)
testImplementation(libs.test.junit)
@@ -48,6 +49,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor)

View File

@@ -24,15 +24,17 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.RoomScope
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
class CreatePollNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: CreatePollPresenter.Factory,
// analyticsService: AnalyticsService, // TODO Polls: add analytics
analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
private val presenter = presenterFactory.create(backNavigator = ::navigateUp)
@@ -40,8 +42,7 @@ class CreatePollNode @AssistedInject constructor(
init {
lifecycle.subscribe(
onResume = {
// TODO Polls: add analytics
// analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.PollView))
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreatePollView))
}
)
}

View File

@@ -29,9 +29,13 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import im.vector.app.features.analytics.plan.PollCreation
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@@ -44,9 +48,9 @@ private const val MAX_SELECTIONS = 1
class CreatePollPresenter @AssistedInject constructor(
private val room: MatrixRoom,
// private val analyticsService: AnalyticsService, // TODO Polls: add analytics
private val analyticsService: AnalyticsService,
private val messageComposerContext: MessageComposerContext,
@Assisted private val navigateUp: () -> Unit,
// private val messageComposerContext: MessageComposerContext, // TODO Polls: add analytics
) : Presenter<CreatePollState> {
@AssistedFactory
@@ -78,7 +82,21 @@ class CreatePollPresenter @AssistedInject constructor(
maxSelections = MAX_SELECTIONS,
pollKind = pollKind,
)
// analyticsService.capture(PollCreate()) // TODO Polls: add analytics
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
isEditing = messageComposerContext.composerMode.isEditing,
isReply = messageComposerContext.composerMode.isReply,
messageType = Composer.MessageType.Poll,
)
)
analyticsService.capture(
PollCreation(
action = PollCreation.Action.Create,
isUndisclosed = pollKind == PollKind.Undisclosed,
numberOfAnswers = answers.size,
)
)
navigateUp()
} else {
Timber.d("Cannot create poll")
@@ -153,7 +171,7 @@ private val pollKindSaver: Saver<MutableState<PollKind>, Boolean> = Saver(
},
restore = {
mutableStateOf(
when(it) {
when (it) {
true -> PollKind.Undisclosed
else -> PollKind.Disclosed
}

View File

@@ -20,9 +20,13 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import im.vector.app.features.analytics.plan.Composer
import im.vector.app.features.analytics.plan.PollCreation
import io.element.android.features.messages.test.MessageComposerContextFake
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.test.room.CreatePollInvocation
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
@@ -37,11 +41,13 @@ class CreatePollPresenterTest {
private var navUpInvocationsCount = 0
private val fakeMatrixRoom = FakeMatrixRoom()
// private val fakeAnalyticsService = FakeAnalyticsService() // TODO Polls: add analytics
private val fakeAnalyticsService = FakeAnalyticsService()
private val messageComposerContextFake = MessageComposerContextFake()
private val presenter = CreatePollPresenter(
room = fakeMatrixRoom,
// analyticsService = fakeAnalyticsService, // TODO Polls: add analytics
analyticsService = fakeAnalyticsService,
messageComposerContext = messageComposerContextFake,
navigateUp = { navUpInvocationsCount++ },
)
@@ -104,6 +110,22 @@ class CreatePollPresenterTest {
pollKind = PollKind.Disclosed
)
)
Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
Truth.assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
Composer(
inThread = false,
isEditing = false,
isReply = false,
messageType = Composer.MessageType.Poll,
)
)
Truth.assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo(
PollCreation(
action = PollCreation.Action.Create,
isUndisclosed = false,
numberOfAnswers = 2,
)
)
}
}

View File

@@ -158,13 +158,19 @@ private fun DefaultRoomListTopBar(
.nestedScroll(scrollBehavior.nestedScrollConnection)
.avatarBloom(
avatarData = avatarData,
background = ElementTheme.materialColors.background,
background = if (ElementTheme.isLightTheme) {
// Workaround to display a very subtle bloom for avatars with very soft colors
Color(0xFFF9F9F9)
} else {
ElementTheme.materialColors.background
},
blurSize = DpSize(avatarBloomSize, avatarBloomSize),
offset = DpOffset(24.dp, 24.dp + statusBarPadding),
clipToSize = if (appBarHeight > 0) DpSize(
avatarBloomSize,
appBarHeight.toDp()
) else DpSize.Unspecified,
bottomSoftEdgeColor = ElementTheme.materialColors.background,
bottomSoftEdgeAlpha = 1f - collapsedFraction,
alpha = if (areSearchResultsDisplayed) 0f else 1f,
)

View File

@@ -46,7 +46,7 @@ dependencyanalysis = "1.21.0"
stem = "2.3.0"
sqldelight = "1.5.5"
telephoto = "0.6.0"
wysiwyg = "2.9.0"
wysiwyg = "2.10.0"
# DI
dagger = "2.48"
@@ -149,7 +149,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.50"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.51"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
@@ -169,7 +169,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1"
# Analytics
posthog = "com.posthog.android:posthog:2.0.3"
sentry = "io.sentry:sentry-android:6.29.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f"
# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3"

View File

@@ -27,7 +27,8 @@ object VectorIcons {
val ReportContent = R.drawable.ic_report_content
val Groups = R.drawable.ic_groups
val Share = R.drawable.ic_share
val EndPoll = R.drawable.ic_poll_end
val Poll = R.drawable.ic_poll
val PollEnd = R.drawable.ic_poll_end
val Bold = R.drawable.ic_bold
val BulletList = R.drawable.ic_bullet_list
val CodeBlock = R.drawable.ic_code_block

View File

@@ -45,6 +45,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -96,8 +97,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import coil.compose.rememberAsyncImagePainter
import coil.imageLoader
import coil.request.DefaultRequestOptions
import coil.request.ImageRequest
import coil.size.Size
import com.airbnb.android.showkase.annotation.ShowkaseComposable
import com.vanniktech.blurhash.BlurHash
import io.element.android.libraries.designsystem.R
@@ -114,6 +117,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.roundToInt
@@ -125,7 +130,7 @@ object BloomDefaults {
* Number of components to use with BlurHash to generate the blur effect.
* Larger values mean more detailed blurs.
*/
const val HASH_COMPONENTS = 8
const val HASH_COMPONENTS = 5
/** Default bloom layers. */
@Composable
@@ -159,6 +164,7 @@ data class BloomLayer(
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
* @param layerConfiguration The configuration for the bloom layers. If not specified the default layers configuration will be used.
* @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used.
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
* @param alpha The alpha value to apply to the bloom effect.
@@ -170,6 +176,7 @@ fun Modifier.bloom(
offset: DpOffset = DpOffset.Unspecified,
clipToSize: DpSize = DpSize.Unspecified,
layerConfiguration: ImmutableList<BloomLayer>? = null,
bottomSoftEdgeColor: Color = background,
bottomSoftEdgeHeight: Dp = 40.dp,
@FloatRange(from = 0.0, to = 1.0)
bottomSoftEdgeAlpha: Float = 1.0f,
@@ -238,7 +245,7 @@ fun Modifier.bloom(
val bottomEdgeGradient = LinearGradientShader(
from = IntOffset(0, clipToPixelSize.height - bottomSoftEdgeHeightPixels).toOffset(),
to = IntOffset(0, clipToPixelSize.height).toOffset(),
listOf(Color.Transparent, background),
listOf(Color.Transparent, bottomSoftEdgeColor),
listOf(0f, 1f)
)
val bottomEdgeGradientBrush = ShaderBrush(bottomEdgeGradient)
@@ -297,6 +304,7 @@ fun Modifier.bloom(
* @param blurSize The size of the bloom effect. If not specified the bloom effect will be the size of the component.
* @param offset The offset to use for the bloom effect. If not specified the bloom effect will be centered on the component.
* @param clipToSize The size to use for clipping the bloom effect. If not specified the bloom effect will not be clipped.
* @param bottomSoftEdgeColor The color to use for the bottom soft edge. If not specified the [background] color will be used.
* @param bottomSoftEdgeHeight The height of the bottom soft edge. If not specified the bottom soft edge will not be drawn.
* @param bottomSoftEdgeAlpha The alpha value to apply to the bottom soft edge.
* @param alpha The alpha value to apply to the bloom effect.
@@ -307,6 +315,7 @@ fun Modifier.avatarBloom(
blurSize: DpSize = DpSize.Unspecified,
offset: DpOffset = DpOffset.Unspecified,
clipToSize: DpSize = DpSize.Unspecified,
bottomSoftEdgeColor: Color = background,
bottomSoftEdgeHeight: Dp = 40.dp,
@FloatRange(from = 0.0, to = 1.0)
bottomSoftEdgeAlpha: Float = 1.0f,
@@ -317,21 +326,35 @@ fun Modifier.avatarBloom(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return@composed this
avatarData ?: return@composed this
// Request the avatar contents to use as the bloom source
val context = LocalContext.current
val density = LocalDensity.current
if (avatarData.url != null) {
// Request the avatar contents to use as the bloom source
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current)
val painterRequest = remember(avatarData) {
ImageRequest.Builder(context)
.data(avatarData)
// Allow cache and default dispatchers
.defaults(DefaultRequestOptions())
// Needed to be able to read pixels from the Bitmap for the hash
.allowHardware(false)
// Reduce size so it loads faster for large avatars
.size(with(density) { Size(64.dp.roundToPx(), 64.dp.roundToPx()) })
.build()
)
var blurHash by remember { mutableStateOf<String?>(null) }
}
// By making it saveable, we'll 'cache' the previous bloom effect until a new one is loaded
var blurHash by rememberSaveable(avatarData) { mutableStateOf<String?>(null) }
LaunchedEffect(avatarData) {
val drawable = painter.imageLoader.execute(painter.request).drawable ?: return@LaunchedEffect
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@LaunchedEffect
blurHash = BlurHash.encode(bitmap, BloomDefaults.HASH_COMPONENTS, BloomDefaults.HASH_COMPONENTS)
withContext(Dispatchers.IO) {
val drawable =
context.imageLoader.execute(painterRequest).drawable ?: return@withContext
val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return@withContext
blurHash = BlurHash.encode(
bitmap,
BloomDefaults.HASH_COMPONENTS,
BloomDefaults.HASH_COMPONENTS
)
}
}
bloom(
@@ -363,6 +386,7 @@ fun Modifier.avatarBloom(
blurSize = blurSize,
offset = offset,
clipToSize = clipToSize,
bottomSoftEdgeColor = bottomSoftEdgeColor,
bottomSoftEdgeHeight = bottomSoftEdgeHeight,
bottomSoftEdgeAlpha = bottomSoftEdgeAlpha,
alpha = alpha,
@@ -517,7 +541,13 @@ internal fun BloomInitialsPreview(@PreviewParameter(InitialsColorStateProvider::
modifier = Modifier.size(256.dp)
.bloom(
hash = hash,
background = ElementTheme.materialColors.background,
background = if (ElementTheme.isLightTheme) {
// Workaround to display a very subtle bloom for avatars with very soft colors
Color(0xFFF9F9F9)
} else {
ElementTheme.materialColors.background
},
bottomSoftEdgeColor = ElementTheme.materialColors.background,
blurSize = DpSize(256.dp, 256.dp),
),
contentAlignment = Alignment.Center

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M7.333,15.583C7.593,15.583 7.811,15.495 7.986,15.32C8.162,15.144 8.25,14.926 8.25,14.667V10.083C8.25,9.824 8.162,9.606 7.986,9.43C7.811,9.255 7.593,9.167 7.333,9.167C7.074,9.167 6.856,9.255 6.68,9.43C6.505,9.606 6.417,9.824 6.417,10.083V14.667C6.417,14.926 6.505,15.144 6.68,15.32C6.856,15.495 7.074,15.583 7.333,15.583ZM11,15.583C11.26,15.583 11.477,15.495 11.653,15.32C11.829,15.144 11.917,14.926 11.917,14.667V7.333C11.917,7.074 11.829,6.856 11.653,6.68C11.477,6.505 11.26,6.417 11,6.417C10.74,6.417 10.523,6.505 10.347,6.68C10.171,6.856 10.083,7.074 10.083,7.333V14.667C10.083,14.926 10.171,15.144 10.347,15.32C10.523,15.495 10.74,15.583 11,15.583ZM14.667,15.583C14.926,15.583 15.144,15.495 15.32,15.32C15.495,15.144 15.583,14.926 15.583,14.667V12.833C15.583,12.574 15.495,12.356 15.32,12.18C15.144,12.005 14.926,11.917 14.667,11.917C14.407,11.917 14.189,12.005 14.014,12.18C13.838,12.356 13.75,12.574 13.75,12.833V14.667C13.75,14.926 13.838,15.144 14.014,15.32C14.189,15.495 14.407,15.583 14.667,15.583ZM4.583,19.25C4.079,19.25 3.648,19.07 3.289,18.712C2.93,18.352 2.75,17.921 2.75,17.417V4.583C2.75,4.079 2.93,3.648 3.289,3.289C3.648,2.93 4.079,2.75 4.583,2.75H17.417C17.921,2.75 18.352,2.93 18.712,3.289C19.07,3.648 19.25,4.079 19.25,4.583V17.417C19.25,17.921 19.07,18.352 18.712,18.712C18.352,19.07 17.921,19.25 17.417,19.25H4.583ZM4.583,17.417H17.417V4.583H4.583V17.417Z"
android:fillColor="#1B1D22"/>
</vector>

View File

@@ -1,14 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="19dp"
android:viewportWidth="20"
android:viewportHeight="19">
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M18,8.659V17C18,17.55 17.804,18.021 17.413,18.413C17.021,18.804 16.55,19 16,19H2C1.45,19 0.979,18.804 0.587,18.413C0.196,18.021 0,17.55 0,17V3C0,2.45 0.196,1.979 0.587,1.587C0.979,1.196 1.45,1 2,1H10.341C10.12,1.626 10,2.299 10,3L2,3V17H16V9C16.701,9 17.374,8.88 18,8.659ZM5.713,14.712C5.521,14.904 5.283,15 5,15C4.717,15 4.479,14.904 4.287,14.712C4.096,14.521 4,14.283 4,14V9C4,8.717 4.096,8.479 4.287,8.288C4.479,8.096 4.717,8 5,8C5.283,8 5.521,8.096 5.713,8.288C5.904,8.479 6,8.717 6,9V14C6,14.283 5.904,14.521 5.713,14.712ZM9.712,14.712C9.521,14.904 9.283,15 9,15C8.717,15 8.479,14.904 8.288,14.712C8.096,14.521 8,14.283 8,14V6C8,5.717 8.096,5.479 8.288,5.287C8.479,5.096 8.717,5 9,5C9.283,5 9.521,5.096 9.712,5.287C9.904,5.479 10,5.717 10,6V14C10,14.283 9.904,14.521 9.712,14.712ZM13.712,14.712C13.521,14.904 13.283,15 13,15C12.717,15 12.479,14.904 12.288,14.712C12.096,14.521 12,14.283 12,14V12C12,11.717 12.096,11.479 12.288,11.288C12.479,11.096 12.717,11 13,11C13.283,11 13.521,11.096 13.712,11.288C13.904,11.479 14,11.717 14,12V14C14,14.283 13.904,14.521 13.712,14.712Z"
android:fillColor="#1B1D22"
android:fillType="evenOdd"/>
android:pathData="M17.148,7.065L20.815,3.399C21.173,3.041 21.173,2.46 20.815,2.102C20.457,1.744 19.876,1.744 19.518,2.102L16.5,5.121L15.315,3.936C14.957,3.578 14.377,3.578 14.019,3.936C13.66,4.294 13.66,4.874 14.019,5.232L15.852,7.065C16.21,7.423 16.79,7.423 17.148,7.065Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M19.707,0.293C20.098,0.683 20.098,1.317 19.707,1.707L15.707,5.707C15.317,6.098 14.683,6.098 14.293,5.707L12.293,3.707C11.902,3.317 11.902,2.683 12.293,2.293C12.683,1.902 13.317,1.902 13.707,2.293L15,3.586L18.293,0.293C18.683,-0.098 19.317,-0.098 19.707,0.293Z"
android:fillColor="#1B1D22"
android:fillType="evenOdd"/>
android:pathData="M19.25,17.417V9.771C18.677,9.974 18.059,10.084 17.417,10.084V17.417H4.583V4.584L11.917,4.584C11.917,3.941 12.027,3.324 12.23,2.751H4.583C4.079,2.751 3.648,2.93 3.289,3.289C2.93,3.648 2.75,4.08 2.75,4.584V17.417C2.75,17.921 2.93,18.353 3.289,18.712C3.648,19.071 4.079,19.251 4.583,19.251H17.417C17.921,19.251 18.352,19.071 18.712,18.712C19.07,18.353 19.25,17.921 19.25,17.417Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M7.333,15.584C7.593,15.584 7.811,15.496 7.986,15.32C8.162,15.145 8.25,14.927 8.25,14.667V10.084C8.25,9.824 8.162,9.607 7.986,9.431C7.811,9.255 7.593,9.167 7.333,9.167C7.074,9.167 6.856,9.255 6.68,9.431C6.505,9.607 6.417,9.824 6.417,10.084V14.667C6.417,14.927 6.505,15.145 6.68,15.32C6.856,15.496 7.074,15.584 7.333,15.584Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M11,15.584C11.26,15.584 11.477,15.496 11.653,15.32C11.829,15.145 11.917,14.927 11.917,14.667V7.334C11.917,7.074 11.829,6.857 11.653,6.681C11.477,6.505 11.26,6.417 11,6.417C10.74,6.417 10.523,6.505 10.347,6.681C10.171,6.857 10.083,7.074 10.083,7.334V14.667C10.083,14.927 10.171,15.145 10.347,15.32C10.523,15.496 10.74,15.584 11,15.584Z"
android:fillColor="#1B1D22"/>
<path
android:pathData="M14.667,15.584C14.926,15.584 15.144,15.496 15.32,15.32C15.495,15.145 15.583,14.927 15.583,14.667V12.834C15.583,12.574 15.495,12.357 15.32,12.181C15.144,12.005 14.926,11.917 14.667,11.917C14.407,11.917 14.189,12.005 14.014,12.181C13.838,12.357 13.75,12.574 13.75,12.834V14.667C13.75,14.927 13.838,15.145 14.014,15.32C14.189,15.496 14.407,15.584 14.667,15.584Z"
android:fillColor="#1B1D22"/>
</vector>

View File

@@ -44,4 +44,9 @@ enum class FeatureFlags(
// Do not forget to edit StaticFeatureFlagProvider when enabling the feature.
defaultValue = false,
),
RichTextEditor(
key = "feature.richtexteditor",
title = "Enable rich text editor",
defaultValue = true,
),
}

View File

@@ -35,6 +35,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.LocationSharing -> true
FeatureFlags.Polls -> true
FeatureFlags.NotificationSettings -> false
FeatureFlags.RichTextEditor -> true
}
} else {
false

View File

@@ -79,11 +79,11 @@ interface MatrixRoom : Closeable {
suspend fun userAvatarUrl(userId: UserId): Result<String?>
suspend fun sendMessage(body: String, htmlBody: String): Result<Unit>
suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit>
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit>
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit>
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>

View File

@@ -26,6 +26,9 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration(
logoUri = "https://element.io/mobile-icon.png",
tosUri = "https://element.io/acceptable-use-policy-terms",
policyUri = "https://element.io/privacy",
contacts = listOf(
"support@element.io",
),
/**
* Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually
*/

View File

@@ -63,10 +63,12 @@ import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber
import java.io.File
@@ -227,32 +229,32 @@ class RustMatrixRoom(
}
}
override suspend fun sendMessage(body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId()
messageEventContentFromHtml(body, htmlBody).use { content ->
messageEventContentFromParts(body, htmlBody).use { content ->
runCatching {
innerRoom.send(content, transactionId)
}
}
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> =
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> =
withContext(roomDispatcher) {
if (originalEventId != null) {
runCatching {
innerRoom.edit(messageEventContentFromHtml(body, htmlBody), originalEventId.value, transactionId?.value)
innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value)
}
} else {
runCatching {
transactionId?.let { cancelSend(it) }
innerRoom.send(messageEventContentFromHtml(body, htmlBody), genTransactionId())
innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId())
}
}
}
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> = withContext(roomDispatcher) {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = withContext(roomDispatcher) {
runCatching {
innerRoom.sendReply(messageEventContentFromHtml(body, htmlBody), eventId.value, genTransactionId())
innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId())
}
}
@@ -456,4 +458,11 @@ class RustMatrixRoom(
MediaUploadHandlerImpl(files, handle())
}
}
private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation =
if(htmlBody != null) {
messageEventContentFromHtml(body, htmlBody)
} else {
messageEventContentFromMarkdown(body)
}
}

View File

@@ -92,7 +92,7 @@ class FakeMatrixRoom(
private var sendPollResponseResult = Result.success(Unit)
private var endPollResult = Result.success(Unit)
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
val editMessageCalls = mutableListOf<Pair<String, String>>()
val editMessageCalls = mutableListOf<Pair<String, String?>>()
var sendMediaCount = 0
private set
@@ -171,7 +171,7 @@ class FakeMatrixRoom(
userAvatarUrlResult
}
override suspend fun sendMessage(body: String, htmlBody: String) = simulateLongTask {
override suspend fun sendMessage(body: String, htmlBody: String?) = simulateLongTask {
Result.success(Unit)
}
@@ -200,15 +200,15 @@ class FakeMatrixRoom(
return cancelSendResult
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> {
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> {
editMessageCalls += body to htmlBody
return Result.success(Unit)
}
var replyMessageParameter: Pair<String, String>? = null
var replyMessageParameter: Pair<String, String?>? = null
private set
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> {
replyMessageParameter = body to htmlBody
return Result.success(Unit)
}

View File

@@ -17,6 +17,6 @@
package io.element.android.libraries.textcomposer
data class Message(
val html: String,
val html: String?,
val markdown: String,
)

View File

@@ -91,6 +91,7 @@ fun TextComposer(
state: RichTextEditorState,
composerMode: MessageComposerMode,
canSendMessage: Boolean,
enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
showTextFormatting: Boolean = false,
onRequestFocus: () -> Unit = {},
@@ -101,7 +102,8 @@ fun TextComposer(
onError: (Throwable) -> Unit = {},
) {
val onSendClicked = {
onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown))
val html = if (enableTextFormatting) state.messageHtml else null
onSendMessage(Message(html = html, markdown = state.messageMarkdown))
}
Column(
@@ -600,6 +602,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
onSendMessage = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message", fake = true).apply { requestFocus() },
@@ -607,6 +610,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
onSendMessage = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState(
@@ -619,6 +623,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
onSendMessage = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message without focus", fake = true),
@@ -626,6 +631,7 @@ internal fun TextComposerSimplePreview() = ElementPreview {
onSendMessage = {},
composerMode = MessageComposerMode.Normal(""),
onResetComposerMode = {},
enableTextFormatting = true,
)
}
}
@@ -639,18 +645,21 @@ internal fun TextComposerFormattingPreview() = ElementPreview {
canSendMessage = false,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal(""),
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message", fake = true),
canSendMessage = true,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal(""),
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true),
canSendMessage = true,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal(""),
enableTextFormatting = true,
)
}
}
@@ -664,6 +673,7 @@ internal fun TextComposerEditPreview() = ElementPreview {
onSendMessage = {},
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
onResetComposerMode = {},
enableTextFormatting = true,
)
}
@@ -684,6 +694,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
"To preview larger textfields and long lines with overflow"
),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message", fake = true),
@@ -701,6 +712,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
defaultContent = "image.jpg"
),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message", fake = true),
@@ -718,6 +730,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
defaultContent = "video.mp4"
),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message", fake = true),
@@ -735,6 +748,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
defaultContent = "logs.txt"
),
onResetComposerMode = {},
enableTextFormatting = true,
)
TextComposer(
RichTextEditorState("A message", fake = true).apply { requestFocus() },
@@ -752,6 +766,7 @@ internal fun TextComposerReplyPreview() = ElementPreview {
defaultContent = "Shared location"
),
onResetComposerMode = {},
enableTextFormatting = true,
)
}
}

Some files were not shown because too many files have changed in this diff Show More