Extract MessageComposerContext class from MessageComposerPresenter (#876)

When sending "Composer" analytics from screens other than the composer's (e.g. send location from map) we need to know the composer's mode in order to properly fill the analytics event. `MessageComposerContext` hoists this state so that other presenters can also read it.

Related to:
https://github.com/vector-im/element-meta/issues/1674
https://github.com/vector-im/element-meta/issues/1682
This commit is contained in:
Marco Romano
2023-07-14 13:32:09 +02:00
committed by GitHub
parent fcf4454c1c
commit a1ca7cf2ca
9 changed files with 165 additions and 36 deletions

View File

@@ -25,4 +25,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
api(projects.libraries.textcomposer)
}

View File

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

View File

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

View File

@@ -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<MessageComposerState> {
@SuppressLint("UnsafeOptInUsageError")
@@ -96,14 +97,11 @@ class MessageComposerPresenter @Inject constructor(
val text: MutableState<StableCharSequence> = remember {
mutableStateOf(StableCharSequence(""))
}
val composerMode: MutableState<MessageComposerMode> = 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<MessageComposerMode>.setToNormal() {
value = MessageComposerMode.Normal("")
}
private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState<MessageComposerMode>, textState: MutableState<StableCharSequence>) =
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<StableCharSequence>
) = 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,

View File

@@ -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(),

View File

@@ -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(),
)
}

View File

@@ -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)
}

View File

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

View File

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