diff --git a/features/messages/api/build.gradle.kts b/features/messages/api/build.gradle.kts index d63fe04dc8..756014e97d 100644 --- a/features/messages/api/build.gradle.kts +++ b/features/messages/api/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + api(projects.libraries.textcomposer) } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt new file mode 100644 index 0000000000..5a0596c7bb --- /dev/null +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.api + +import io.element.android.libraries.textcomposer.MessageComposerMode + +/** + * Hoist-able state of the message composer. + * + * Typical use case is inside other presenters, to know if + * the composer is in a thread, if it's editing a message, etc. + */ +interface MessageComposerContext { + val composerMode: MessageComposerMode +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt new file mode 100644 index 0000000000..73481cd617 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerContextImpl.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.messagecomposer + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.messages.api.MessageComposerContext +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.textcomposer.MessageComposerMode +import javax.inject.Inject + +@SingleIn(RoomScope::class) +@ContributesBinding(RoomScope::class) +class MessageComposerContextImpl @Inject constructor() : MessageComposerContext { + override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal("")) + internal set +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 3ad2c497ce..f7c80b4320 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -64,6 +64,7 @@ class MessageComposerPresenter @Inject constructor( private val mediaSender: MediaSender, private val snackbarDispatcher: SnackbarDispatcher, private val analyticsService: AnalyticsService, + private val messageComposerContext: MessageComposerContextImpl, ) : Presenter { @SuppressLint("UnsafeOptInUsageError") @@ -96,14 +97,11 @@ class MessageComposerPresenter @Inject constructor( val text: MutableState = remember { mutableStateOf(StableCharSequence("")) } - val composerMode: MutableState = rememberSaveable { - mutableStateOf(MessageComposerMode.Normal("")) - } var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } - LaunchedEffect(composerMode.value) { - when (val modeValue = composerMode.value) { + LaunchedEffect(messageComposerContext.composerMode) { + when (val modeValue = messageComposerContext.composerMode) { is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence() else -> Unit } @@ -125,17 +123,21 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence() MessageComposerEvents.CloseSpecialMode -> { text.value = "".toStableCharSequence() - composerMode.setToNormal() + messageComposerContext.composerMode = MessageComposerMode.Normal("") } - is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) + is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( + text = event.message, + updateComposerMode = { messageComposerContext.composerMode = it }, + textState = text + ) is MessageComposerEvents.SetMode -> { - composerMode.value = event.composerMode + messageComposerContext.composerMode = event.composerMode analyticsService.capture( Composer( - inThread = false, - isEditing = composerMode.value is MessageComposerMode.Edit, - isReply = composerMode.value is MessageComposerMode.Reply, + inThread = messageComposerContext.composerMode.inThread, + isEditing = messageComposerContext.composerMode.isEditing, + isReply = messageComposerContext.composerMode.isReply, isLocation = false, ) ) @@ -171,7 +173,7 @@ class MessageComposerPresenter @Inject constructor( text = text.value, isFullScreen = isFullScreen.value, hasFocus = hasFocus.value, - mode = composerMode.value, + mode = messageComposerContext.composerMode, showAttachmentSourcePicker = showAttachmentSourcePicker, attachmentsState = attachmentsState.value, eventSink = ::handleEvents @@ -184,31 +186,30 @@ class MessageComposerPresenter @Inject constructor( } } - private fun MutableState.setToNormal() { - value = MessageComposerMode.Normal("") - } - - private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState, textState: MutableState) = - launch { - val capturedMode = composerMode.value - // Reset composer right away - textState.value = "".toStableCharSequence() - composerMode.setToNormal() - when (capturedMode) { - is MessageComposerMode.Normal -> room.sendMessage(text) - is MessageComposerMode.Edit -> { - val eventId = capturedMode.eventId - val transactionId = capturedMode.transactionId - room.editMessage(eventId, transactionId, text) - } - - is MessageComposerMode.Quote -> TODO() - is MessageComposerMode.Reply -> room.replyMessage( - capturedMode.eventId, - text - ) + private fun CoroutineScope.sendMessage( + text: String, + updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit, + textState: MutableState + ) = launch { + val capturedMode = messageComposerContext.composerMode + // Reset composer right away + textState.value = "".toStableCharSequence() + updateComposerMode(MessageComposerMode.Normal("")) + when (capturedMode) { + is MessageComposerMode.Normal -> room.sendMessage(text) + is MessageComposerMode.Edit -> { + val eventId = capturedMode.eventId + val transactionId = capturedMode.transactionId + room.editMessage(eventId, transactionId, text) } + + is MessageComposerMode.Quote -> TODO() + is MessageComposerMode.Reply -> room.replyMessage( + capturedMode.eventId, + text + ) } + } private fun CoroutineScope.sendAttachment( attachment: Attachment, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 53034131dc..990b0c5a9b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +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.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter @@ -568,6 +569,7 @@ class MessagesPresenterTest { mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom), snackbarDispatcher = SnackbarDispatcher(), analyticsService = FakeAnalyticsService(), + messageComposerContext = MessageComposerContextImpl(), ) val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 97bbf925bd..742f994352 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -26,6 +26,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.analytics.test.FakeAnalyticsService import io.element.android.features.messages.impl.messagecomposer.AttachmentsState +import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState @@ -503,7 +504,8 @@ class MessageComposerPresenterTest { localMediaFactory, MediaSender(mediaPreProcessor, room), snackbarDispatcher, - FakeAnalyticsService() + FakeAnalyticsService(), + MessageComposerContextImpl(), ) } diff --git a/features/messages/test/build.gradle.kts b/features/messages/test/build.gradle.kts new file mode 100644 index 0000000000..27360e9567 --- /dev/null +++ b/features/messages/test/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.messages.test" +} + +dependencies { + api(projects.features.messages.api) +} diff --git a/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt new file mode 100644 index 0000000000..75c992f495 --- /dev/null +++ b/features/messages/test/src/main/kotlin/io/element/android/features/messages/test/MessageComposerContextFake.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.test + +import io.element.android.features.messages.api.MessageComposerContext +import io.element.android.libraries.textcomposer.MessageComposerMode + +class MessageComposerContextFake( + override var composerMode: MessageComposerMode = MessageComposerMode.Normal(null) +) : MessageComposerContext diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt index f0ccc76f3c..1a9f20a060 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt @@ -51,4 +51,13 @@ sealed interface MessageComposerMode : Parcelable { is Quote -> eventId is Reply -> eventId } + + val isEditing: Boolean + get() = this is Edit + + val isReply: Boolean + get() = this is Reply + + val inThread: Boolean + get() = false // TODO }