Browse Source

Merge branch 'develop' into dla/feature/connect_sdk_to_global_notifications_ui

pull/1304/head
David Langley 1 year ago committed by GitHub
parent
commit
0d53626077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      changelog.d/1289.feature
  2. 17
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  3. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
  4. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
  5. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
  6. 16
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt
  7. 16
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  8. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
  9. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
  10. 35
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
  11. 2
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
  12. 4
      gradle/libs.versions.toml
  13. 10
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt
  14. 5
      libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt
  15. 1
      libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt
  16. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  17. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt
  18. 23
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  19. 10
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  20. 2
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt
  21. 17
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  22. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png
  23. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png
  24. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png
  25. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png
  26. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png
  27. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
  28. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png
  29. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png
  30. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png
  31. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png
  32. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png
  33. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png
  34. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png
  35. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png
  36. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png
  37. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png
  38. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png
  39. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png

1
changelog.d/1289.feature

@ -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.

17
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

@ -66,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.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState 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.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -95,6 +97,7 @@ class MessagesPresenter @AssistedInject constructor(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
private val clipboardHelper: ClipboardHelper, private val clipboardHelper: ClipboardHelper,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService,
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
@ -143,6 +146,11 @@ class MessagesPresenter @AssistedInject constructor(
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId)) timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
} }
var enableTextFormatting by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
enableTextFormatting = featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)
}
fun handleEvents(event: MessagesEvents) { fun handleEvents(event: MessagesEvents) {
when (event) { when (event) {
is MessagesEvents.HandleAction -> { is MessagesEvents.HandleAction -> {
@ -178,6 +186,7 @@ class MessagesPresenter @AssistedInject constructor(
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
showReinvitePrompt = showReinvitePrompt, showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value, inviteProgress = inviteProgress.value,
enableTextFormatting = enableTextFormatting,
eventSink = { handleEvents(it) } eventSink = { handleEvents(it) }
) )
} }
@ -250,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( val composerMode = MessageComposerMode.Edit(
targetEvent.eventId, targetEvent.eventId,
(targetEvent.content as? TimelineItemTextBasedContent)?.let { (targetEvent.content as? TimelineItemTextBasedContent)?.let {
it.htmlBody ?: it.body if (featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)) {
it.htmlBody ?: it.body
} else {
it.body
}
}.orEmpty(), }.orEmpty(),
targetEvent.transactionId, targetEvent.transactionId,
) )

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

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

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

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

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

@ -304,6 +304,7 @@ private fun MessagesViewContent(
state = state.composerState, state = state.composerState,
onSendLocationClicked = onSendLocationClicked, onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked, onCreatePollClicked = onCreatePollClicked,
enableTextFormatting = state.enableTextFormatting,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight(Alignment.Bottom) .wrapContentHeight(Alignment.Bottom)

16
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt

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

16
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -148,14 +148,6 @@ class MessageComposerPresenter @Inject constructor(
) )
is MessageComposerEvents.SetMode -> { is MessageComposerEvents.SetMode -> {
messageComposerContext.composerMode = event.composerMode messageComposerContext.composerMode = event.composerMode
analyticsService.capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
isEditing = messageComposerContext.composerMode.isEditing,
isReply = messageComposerContext.composerMode.isReply,
messageType = Composer.MessageType.Text,
)
)
} }
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch { MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
showAttachmentSourcePicker = true showAttachmentSourcePicker = true
@ -238,6 +230,14 @@ class MessageComposerPresenter @Inject constructor(
message.html, 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( private fun CoroutineScope.sendAttachment(

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt

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

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt

@ -628,6 +628,7 @@ class MessagesPresenterTest {
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
val featureFlagsService = FakeFeatureFlagService(mapOf(FeatureFlags.RichTextEditor.key to true))
return MessagesPresenter( return MessagesPresenter(
room = matrixRoom, room = matrixRoom,
composerPresenter = messageComposerPresenter, composerPresenter = messageComposerPresenter,
@ -642,6 +643,7 @@ class MessagesPresenterTest {
navigator = navigator, navigator = navigator,
clipboardHelper = clipboardHelper, clipboardHelper = clipboardHelper,
analyticsService = analyticsService, analyticsService = analyticsService,
featureFlagService = featureFlagsService,
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
) )
} }

35
features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt

@ -24,6 +24,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
@ -57,6 +58,7 @@ import io.element.android.libraries.textcomposer.Message
import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.libraries.textcomposer.MessageComposerMode
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -208,6 +210,15 @@ class MessageComposerPresenterTest {
val messageSentState = awaitItem() val messageSentState = awaitItem()
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse() assertThat(messageSentState.canSendMessage).isFalse()
waitForPredicate { analyticsService.capturedEvents.size == 1 }
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = false,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
} }
} }
@ -240,6 +251,14 @@ class MessageComposerPresenterTest {
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse() assertThat(messageSentState.canSendMessage).isFalse()
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE)
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = true,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
} }
} }
@ -272,6 +291,14 @@ class MessageComposerPresenterTest {
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse() assertThat(messageSentState.canSendMessage).isFalse()
assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE)
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = true,
isReply = false,
messageType = Composer.MessageType.Text,
)
)
} }
} }
@ -304,6 +331,14 @@ class MessageComposerPresenterTest {
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("")
assertThat(messageSentState.canSendMessage).isFalse() assertThat(messageSentState.canSendMessage).isFalse()
assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY) assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY)
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
inThread = false,
isEditing = false,
isReply = true,
messageType = Composer.MessageType.Text,
)
)
} }
} }

2
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt

@ -41,6 +41,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconToggleButton import io.element.android.libraries.designsystem.theme.components.IconToggleButton
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonPlurals
@ -111,6 +112,7 @@ fun PollAnswerView(
answerItem.isSelected -> 1f answerItem.isSelected -> 1f
else -> 0f else -> 0f
}, },
trackColor = ElementTheme.colors.progressIndicatorTrackColor,
strokeCap = StrokeCap.Round, strokeCap = StrokeCap.Round,
) )
} }

4
gradle/libs.versions.toml

@ -46,7 +46,7 @@ dependencyanalysis = "1.21.0"
stem = "2.3.0" stem = "2.3.0"
sqldelight = "1.5.5" sqldelight = "1.5.5"
telephoto = "0.6.0" telephoto = "0.6.0"
wysiwyg = "2.9.0" wysiwyg = "2.10.0"
# DI # DI
dagger = "2.48" 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" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1" 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 = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", 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" } sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }

10
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt

@ -65,6 +65,16 @@ val SemanticColors.messageFromOtherBackground
Color(0xFF26282D) Color(0xFF26282D)
} }
// This color is not present in Semantic color, so put hard-coded value for now
val SemanticColors.progressIndicatorTrackColor
get() = if (isLight) {
// We want LightDesignTokens.colorAlphaGray500
Color(0x33052448)
} else {
// We want DarkDesignTokens.colorAlphaGray500
Color(0x25F4F7FA)
}
// Temporary color, which is not in the token right now // Temporary color, which is not in the token right now
val SemanticColors.temporaryColorBgSpecial val SemanticColors.temporaryColorBgSpecial
get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048) get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048)

5
libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt

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

1
libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt

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

6
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt

@ -85,11 +85,11 @@ interface MatrixRoom : Closeable {
suspend fun userAvatarUrl(userId: UserId): Result<String?> 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> suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt

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

23
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

@ -63,10 +63,12 @@ import org.matrix.rustcomponents.sdk.RequiredState
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation
import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.RoomSubscription
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.genTransactionId
import org.matrix.rustcomponents.sdk.messageEventContentFromHtml import org.matrix.rustcomponents.sdk.messageEventContentFromHtml
import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber import timber.log.Timber
import java.io.File 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() val transactionId = genTransactionId()
messageEventContentFromHtml(body, htmlBody).use { content -> messageEventContentFromParts(body, htmlBody).use { content ->
runCatching { runCatching {
innerRoom.send(content, transactionId) 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) { withContext(roomDispatcher) {
if (originalEventId != null) { if (originalEventId != null) {
runCatching { runCatching {
innerRoom.edit(messageEventContentFromHtml(body, htmlBody), originalEventId.value, transactionId?.value) innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value)
} }
} else { } else {
runCatching { runCatching {
transactionId?.let { cancelSend(it) } 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 { 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()) MediaUploadHandlerImpl(files, handle())
} }
} }
private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation =
if(htmlBody != null) {
messageEventContentFromHtml(body, htmlBody)
} else {
messageEventContentFromMarkdown(body)
}
} }

10
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

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

2
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt

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

17
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

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

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-D-13_14_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.event_null_TimelineItemPollView-N-13_15_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_TimelineItemEventTimestampBelow_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedNotSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerDisclosedSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerNotSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerEndedWinnerSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedNotSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollAnswerUndisclosedSelected_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-D-1_1_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentDisclosed-N-1_2_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-D-0_0_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentUndisclosed-N-0_1_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save