Skip to content

Commit a63d3df

Browse files
author
Marco Romano
authored
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: element-hq/element-meta#1674 element-hq/element-meta#1682
1 parent 753d444 commit a63d3df

File tree

9 files changed

+165
-36
lines changed

9 files changed

+165
-36
lines changed

features/messages/api/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ android {
2525
dependencies {
2626
implementation(projects.libraries.architecture)
2727
implementation(projects.libraries.matrix.api)
28+
api(projects.libraries.textcomposer)
2829
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.api
18+
19+
import io.element.android.libraries.textcomposer.MessageComposerMode
20+
21+
/**
22+
* Hoist-able state of the message composer.
23+
*
24+
* Typical use case is inside other presenters, to know if
25+
* the composer is in a thread, if it's editing a message, etc.
26+
*/
27+
interface MessageComposerContext {
28+
val composerMode: MessageComposerMode
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.messagecomposer
18+
19+
import androidx.compose.runtime.getValue
20+
import androidx.compose.runtime.mutableStateOf
21+
import androidx.compose.runtime.setValue
22+
import com.squareup.anvil.annotations.ContributesBinding
23+
import io.element.android.features.messages.api.MessageComposerContext
24+
import io.element.android.libraries.di.RoomScope
25+
import io.element.android.libraries.di.SingleIn
26+
import io.element.android.libraries.textcomposer.MessageComposerMode
27+
import javax.inject.Inject
28+
29+
@SingleIn(RoomScope::class)
30+
@ContributesBinding(RoomScope::class)
31+
class MessageComposerContextImpl @Inject constructor() : MessageComposerContext {
32+
override var composerMode: MessageComposerMode by mutableStateOf(MessageComposerMode.Normal(""))
33+
internal set
34+
}

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

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class MessageComposerPresenter @Inject constructor(
6464
private val mediaSender: MediaSender,
6565
private val snackbarDispatcher: SnackbarDispatcher,
6666
private val analyticsService: AnalyticsService,
67+
private val messageComposerContext: MessageComposerContextImpl,
6768
) : Presenter<MessageComposerState> {
6869

6970
@SuppressLint("UnsafeOptInUsageError")
@@ -96,14 +97,11 @@ class MessageComposerPresenter @Inject constructor(
9697
val text: MutableState<StableCharSequence> = remember {
9798
mutableStateOf(StableCharSequence(""))
9899
}
99-
val composerMode: MutableState<MessageComposerMode> = rememberSaveable {
100-
mutableStateOf(MessageComposerMode.Normal(""))
101-
}
102100

103101
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
104102

105-
LaunchedEffect(composerMode.value) {
106-
when (val modeValue = composerMode.value) {
103+
LaunchedEffect(messageComposerContext.composerMode) {
104+
when (val modeValue = messageComposerContext.composerMode) {
107105
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence()
108106
else -> Unit
109107
}
@@ -125,17 +123,21 @@ class MessageComposerPresenter @Inject constructor(
125123
is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence()
126124
MessageComposerEvents.CloseSpecialMode -> {
127125
text.value = "".toStableCharSequence()
128-
composerMode.setToNormal()
126+
messageComposerContext.composerMode = MessageComposerMode.Normal("")
129127
}
130128

131-
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text)
129+
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
130+
text = event.message,
131+
updateComposerMode = { messageComposerContext.composerMode = it },
132+
textState = text
133+
)
132134
is MessageComposerEvents.SetMode -> {
133-
composerMode.value = event.composerMode
135+
messageComposerContext.composerMode = event.composerMode
134136
analyticsService.capture(
135137
Composer(
136-
inThread = false,
137-
isEditing = composerMode.value is MessageComposerMode.Edit,
138-
isReply = composerMode.value is MessageComposerMode.Reply,
138+
inThread = messageComposerContext.composerMode.inThread,
139+
isEditing = messageComposerContext.composerMode.isEditing,
140+
isReply = messageComposerContext.composerMode.isReply,
139141
isLocation = false,
140142
)
141143
)
@@ -171,7 +173,7 @@ class MessageComposerPresenter @Inject constructor(
171173
text = text.value,
172174
isFullScreen = isFullScreen.value,
173175
hasFocus = hasFocus.value,
174-
mode = composerMode.value,
176+
mode = messageComposerContext.composerMode,
175177
showAttachmentSourcePicker = showAttachmentSourcePicker,
176178
attachmentsState = attachmentsState.value,
177179
eventSink = ::handleEvents
@@ -184,31 +186,30 @@ class MessageComposerPresenter @Inject constructor(
184186
}
185187
}
186188

187-
private fun MutableState<MessageComposerMode>.setToNormal() {
188-
value = MessageComposerMode.Normal("")
189-
}
190-
191-
private fun CoroutineScope.sendMessage(text: String, composerMode: MutableState<MessageComposerMode>, textState: MutableState<StableCharSequence>) =
192-
launch {
193-
val capturedMode = composerMode.value
194-
// Reset composer right away
195-
textState.value = "".toStableCharSequence()
196-
composerMode.setToNormal()
197-
when (capturedMode) {
198-
is MessageComposerMode.Normal -> room.sendMessage(text)
199-
is MessageComposerMode.Edit -> {
200-
val eventId = capturedMode.eventId
201-
val transactionId = capturedMode.transactionId
202-
room.editMessage(eventId, transactionId, text)
203-
}
204-
205-
is MessageComposerMode.Quote -> TODO()
206-
is MessageComposerMode.Reply -> room.replyMessage(
207-
capturedMode.eventId,
208-
text
209-
)
189+
private fun CoroutineScope.sendMessage(
190+
text: String,
191+
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
192+
textState: MutableState<StableCharSequence>
193+
) = launch {
194+
val capturedMode = messageComposerContext.composerMode
195+
// Reset composer right away
196+
textState.value = "".toStableCharSequence()
197+
updateComposerMode(MessageComposerMode.Normal(""))
198+
when (capturedMode) {
199+
is MessageComposerMode.Normal -> room.sendMessage(text)
200+
is MessageComposerMode.Edit -> {
201+
val eventId = capturedMode.eventId
202+
val transactionId = capturedMode.transactionId
203+
room.editMessage(eventId, transactionId, text)
210204
}
205+
206+
is MessageComposerMode.Quote -> TODO()
207+
is MessageComposerMode.Reply -> room.replyMessage(
208+
capturedMode.eventId,
209+
text
210+
)
211211
}
212+
}
212213

213214
private fun CoroutineScope.sendAttachment(
214215
attachment: Attachment,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.MessagesPresenter
3131
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
3232
import io.element.android.features.messages.impl.actionlist.ActionListState
3333
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
34+
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
3435
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
3536
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
3637
import io.element.android.features.messages.impl.timeline.TimelinePresenter
@@ -568,6 +569,7 @@ class MessagesPresenterTest {
568569
mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom),
569570
snackbarDispatcher = SnackbarDispatcher(),
570571
analyticsService = FakeAnalyticsService(),
572+
messageComposerContext = MessageComposerContextImpl(),
571573
)
572574
val timelinePresenter = TimelinePresenter(
573575
timelineItemsFactory = aTimelineItemsFactory(),

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import app.cash.turbine.test
2626
import com.google.common.truth.Truth.assertThat
2727
import io.element.android.features.analytics.test.FakeAnalyticsService
2828
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
29+
import io.element.android.features.messages.impl.messagecomposer.MessageComposerContextImpl
2930
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
3031
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
3132
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
@@ -503,7 +504,8 @@ class MessageComposerPresenterTest {
503504
localMediaFactory,
504505
MediaSender(mediaPreProcessor, room),
505506
snackbarDispatcher,
506-
FakeAnalyticsService()
507+
FakeAnalyticsService(),
508+
MessageComposerContextImpl(),
507509
)
508510
}
509511

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
plugins {
18+
id("io.element.android-library")
19+
}
20+
21+
android {
22+
namespace = "io.element.android.features.messages.test"
23+
}
24+
25+
dependencies {
26+
api(projects.features.messages.api)
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.test
18+
19+
import io.element.android.features.messages.api.MessageComposerContext
20+
import io.element.android.libraries.textcomposer.MessageComposerMode
21+
22+
class MessageComposerContextFake(
23+
override var composerMode: MessageComposerMode = MessageComposerMode.Normal(null)
24+
) : MessageComposerContext

libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerMode.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,13 @@ sealed interface MessageComposerMode : Parcelable {
5151
is Quote -> eventId
5252
is Reply -> eventId
5353
}
54+
55+
val isEditing: Boolean
56+
get() = this is Edit
57+
58+
val isReply: Boolean
59+
get() = this is Reply
60+
61+
val inThread: Boolean
62+
get() = false // TODO
5463
}

0 commit comments

Comments
 (0)