Skip to content

Commit 22defe5

Browse files
authored
Merge pull request #2302 from element-hq/feature/bma/sendTyping
Send typing notification
2 parents 0bcd3ba + bfb6b32 commit 22defe5

File tree

9 files changed

+62
-2
lines changed

9 files changed

+62
-2
lines changed

changelog.d/2240.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Send typing notification

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ sealed interface MessageComposerEvents {
4343
data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
4444
data object CancelSendAttachment : MessageComposerEvents
4545
data class Error(val error: Throwable) : MessageComposerEvents
46+
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
4647
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
4748
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
4849
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.Manifest
2020
import android.annotation.SuppressLint
2121
import android.net.Uri
2222
import androidx.compose.runtime.Composable
23+
import androidx.compose.runtime.DisposableEffect
2324
import androidx.compose.runtime.LaunchedEffect
2425
import androidx.compose.runtime.MutableState
2526
import androidx.compose.runtime.getValue
@@ -207,6 +208,15 @@ class MessageComposerPresenter @Inject constructor(
207208
.collect()
208209
}
209210

211+
DisposableEffect(Unit) {
212+
// Declare that the user is not typing anymore when the composer is disposed
213+
onDispose {
214+
appCoroutineScope.launch {
215+
room.typingNotice(false)
216+
}
217+
}
218+
}
219+
210220
fun handleEvents(event: MessageComposerEvents) {
211221
when (event) {
212222
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
@@ -299,6 +309,11 @@ class MessageComposerPresenter @Inject constructor(
299309
is MessageComposerEvents.Error -> {
300310
analyticsService.trackError(event.error)
301311
}
312+
is MessageComposerEvents.TypingNotice -> {
313+
localCoroutineScope.launch {
314+
room.typingNotice(event.isTyping)
315+
}
316+
}
302317
is MessageComposerEvents.SuggestionReceived -> {
303318
suggestionSearchTrigger.value = event.suggestion
304319
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ internal fun MessageComposerView(
7878
state.eventSink(MessageComposerEvents.Error(error))
7979
}
8080

81+
fun onTyping(typing: Boolean) {
82+
state.eventSink(MessageComposerEvents.TypingNotice(typing))
83+
}
84+
8185
val coroutineScope = rememberCoroutineScope()
8286
fun onRequestFocus() {
8387
coroutineScope.launch {
@@ -121,6 +125,7 @@ internal fun MessageComposerView(
121125
onDeleteVoiceMessage = onDeleteVoiceMessage,
122126
onSuggestionReceived = ::onSuggestionReceived,
123127
onError = ::onError,
128+
onTyping = ::onTyping,
124129
currentUserId = state.currentUserId,
125130
onRichContentSelected = ::sendUri,
126131
)

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,21 @@ class MessageComposerPresenterTest {
873873
}
874874
}
875875

876+
@Test
877+
fun `present - handle typing notice event`() = runTest {
878+
val room = FakeMatrixRoom()
879+
val presenter = createPresenter(room = room, coroutineScope = this)
880+
moleculeFlow(RecompositionMode.Immediate) {
881+
presenter.present()
882+
}.test {
883+
val initialState = awaitFirstItem()
884+
assertThat(room.typingRecord).isEmpty()
885+
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true))
886+
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false))
887+
assertThat(room.typingRecord).isEqualTo(listOf(true, false))
888+
}
889+
}
890+
876891
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
877892
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
878893
skipItems(skipCount)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,12 @@ interface MatrixRoom : Closeable {
224224
progressCallback: ProgressCallback?
225225
): Result<MediaUploadHandler>
226226

227+
/**
228+
* Send a typing notification.
229+
* @param isTyping True if the user is typing, false otherwise.
230+
*/
231+
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
232+
227233
/**
228234
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
229235
* @param widgetSettings The widget settings to use.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,10 @@ class RustMatrixRoom(
520520
)
521521
}
522522

523+
override suspend fun typingNotice(isTyping: Boolean) = runCatching {
524+
innerRoom.typingNotice(isTyping)
525+
}
526+
523527
override suspend fun generateWidgetWebViewUrl(
524528
widgetSettings: MatrixWidgetSettings,
525529
clientId: String,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ class FakeMatrixRoom(
115115
private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
116116
var sendMessageMentions = emptyList<Mention>()
117117
val editMessageCalls = mutableListOf<Pair<String, String?>>()
118+
private val _typingRecord = mutableListOf<Boolean>()
119+
val typingRecord: List<Boolean>
120+
get() = _typingRecord
118121

119122
var sendMediaCount = 0
120123
private set
@@ -426,6 +429,11 @@ class FakeMatrixRoom(
426429
progressCallback: ProgressCallback?
427430
): Result<MediaUploadHandler> = fakeSendMedia(progressCallback)
428431

432+
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> {
433+
_typingRecord += isTyping
434+
return Result.success(Unit)
435+
}
436+
429437
override suspend fun generateWidgetWebViewUrl(
430438
widgetSettings: MatrixWidgetSettings,
431439
clientId: String,

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ fun TextComposer(
112112
onSendVoiceMessage: () -> Unit,
113113
onDeleteVoiceMessage: () -> Unit,
114114
onError: (Throwable) -> Unit,
115+
onTyping: (Boolean) -> Unit,
115116
onSuggestionReceived: (Suggestion?) -> Unit,
116117
onRichContentSelected: ((Uri) -> Unit)?,
117118
modifier: Modifier = Modifier,
@@ -165,6 +166,7 @@ fun TextComposer(
165166
resolveMentionDisplay = { text, url -> TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(text, url)) },
166167
resolveRoomMentionDisplay = { TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor("@room", "#")) },
167168
onError = onError,
169+
onTyping = onTyping,
168170
onRichContentSelected = onRichContentSelected,
169171
)
170172
}
@@ -400,9 +402,10 @@ private fun TextInput(
400402
onResetComposerMode: () -> Unit,
401403
resolveRoomMentionDisplay: () -> TextDisplay,
402404
resolveMentionDisplay: (text: String, url: String) -> TextDisplay,
405+
onError: (Throwable) -> Unit,
406+
onTyping: (Boolean) -> Unit,
407+
onRichContentSelected: ((Uri) -> Unit)?,
403408
modifier: Modifier = Modifier,
404-
onError: (Throwable) -> Unit = {},
405-
onRichContentSelected: ((Uri) -> Unit)? = null,
406409
) {
407410
val bgColor = ElementTheme.colors.bgSubtleSecondary
408411
val borderColor = ElementTheme.colors.borderDisabled
@@ -451,6 +454,7 @@ private fun TextInput(
451454
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
452455
onError = onError,
453456
onRichContentSelected = onRichContentSelected,
457+
onTyping = onTyping,
454458
)
455459
}
456460
}
@@ -920,6 +924,7 @@ private fun ATextComposer(
920924
onSendVoiceMessage = {},
921925
onDeleteVoiceMessage = {},
922926
onError = {},
927+
onTyping = {},
923928
onSuggestionReceived = {},
924929
onRichContentSelected = null,
925930
)

0 commit comments

Comments
 (0)