From bfb6b327409cfdd973bf82718d6ce6e2f3f5766c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Jan 2024 16:08:13 +0100 Subject: [PATCH] Send typing notification #2240 --- changelog.d/2240.feature | 1 + .../impl/messagecomposer/MessageComposerEvents.kt | 1 + .../messagecomposer/MessageComposerPresenter.kt | 15 +++++++++++++++ .../impl/messagecomposer/MessageComposerView.kt | 5 +++++ .../textcomposer/MessageComposerPresenterTest.kt | 15 +++++++++++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 6 ++++++ .../libraries/matrix/impl/room/RustMatrixRoom.kt | 4 ++++ .../libraries/matrix/test/room/FakeMatrixRoom.kt | 8 ++++++++ .../libraries/textcomposer/TextComposer.kt | 9 +++++++-- 9 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2240.feature diff --git a/changelog.d/2240.feature b/changelog.d/2240.feature new file mode 100644 index 0000000000..99d98058c8 --- /dev/null +++ b/changelog.d/2240.feature @@ -0,0 +1 @@ +Send typing notification diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index 4f5c2aba10..9ddf1f7aae 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -43,6 +43,7 @@ sealed interface MessageComposerEvents { data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents data object CancelSendAttachment : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents + data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents } 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 6044b1abe7..4fea2ee046 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 @@ -20,6 +20,7 @@ import android.Manifest import android.annotation.SuppressLint import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -207,6 +208,15 @@ class MessageComposerPresenter @Inject constructor( .collect() } + DisposableEffect(Unit) { + // Declare that the user is not typing anymore when the composer is disposed + onDispose { + appCoroutineScope.launch { + room.typingNotice(false) + } + } + } + fun handleEvents(event: MessageComposerEvents) { when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value @@ -299,6 +309,11 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerEvents.Error -> { analyticsService.trackError(event.error) } + is MessageComposerEvents.TypingNotice -> { + localCoroutineScope.launch { + room.typingNotice(event.isTyping) + } + } is MessageComposerEvents.SuggestionReceived -> { suggestionSearchTrigger.value = event.suggestion } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 47b5adea5e..6cb2900a20 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -78,6 +78,10 @@ internal fun MessageComposerView( state.eventSink(MessageComposerEvents.Error(error)) } + fun onTyping(typing: Boolean) { + state.eventSink(MessageComposerEvents.TypingNotice(typing)) + } + val coroutineScope = rememberCoroutineScope() fun onRequestFocus() { coroutineScope.launch { @@ -121,6 +125,7 @@ internal fun MessageComposerView( onDeleteVoiceMessage = onDeleteVoiceMessage, onSuggestionReceived = ::onSuggestionReceived, onError = ::onError, + onTyping = ::onTyping, currentUserId = state.currentUserId, onRichContentSelected = ::sendUri, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index 8f9842e309..d4b3c33cb1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -873,6 +873,21 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - handle typing notice event`() = runTest { + val room = FakeMatrixRoom() + val presenter = createPresenter(room = room, coroutineScope = this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(room.typingRecord).isEmpty() + initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true)) + initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false)) + assertThat(room.typingRecord).isEqualTo(listOf(true, false)) + } + } + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState { state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) skipItems(skipCount) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 41043d8ba5..6d5a552c83 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -224,6 +224,12 @@ interface MatrixRoom : Closeable { progressCallback: ProgressCallback? ): Result + /** + * Send a typing notification. + * @param isTyping True if the user is typing, false otherwise. + */ + suspend fun typingNotice(isTyping: Boolean): Result + /** * Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters. * @param widgetSettings The widget settings to use. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 647586431a..86bfd2dc12 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -520,6 +520,10 @@ class RustMatrixRoom( ) } + override suspend fun typingNotice(isTyping: Boolean) = runCatching { + innerRoom.typingNotice(isTyping) + } + override suspend fun generateWidgetWebViewUrl( widgetSettings: MatrixWidgetSettings, clientId: String, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 19b5132016..1b37f689a4 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -115,6 +115,9 @@ class FakeMatrixRoom( private var canUserJoinCallResult: Result = Result.success(true) var sendMessageMentions = emptyList() val editMessageCalls = mutableListOf>() + private val _typingRecord = mutableListOf() + val typingRecord: List + get() = _typingRecord var sendMediaCount = 0 private set @@ -426,6 +429,11 @@ class FakeMatrixRoom( progressCallback: ProgressCallback? ): Result = fakeSendMedia(progressCallback) + override suspend fun typingNotice(isTyping: Boolean): Result { + _typingRecord += isTyping + return Result.success(Unit) + } + override suspend fun generateWidgetWebViewUrl( widgetSettings: MatrixWidgetSettings, clientId: String, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 252e35d652..13359c1c0c 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -112,6 +112,7 @@ fun TextComposer( onSendVoiceMessage: () -> Unit, onDeleteVoiceMessage: () -> Unit, onError: (Throwable) -> Unit, + onTyping: (Boolean) -> Unit, onSuggestionReceived: (Suggestion?) -> Unit, onRichContentSelected: ((Uri) -> Unit)?, modifier: Modifier = Modifier, @@ -165,6 +166,7 @@ fun TextComposer( resolveMentionDisplay = { text, url -> TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(text, url)) }, resolveRoomMentionDisplay = { TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor("@room", "#")) }, onError = onError, + onTyping = onTyping, onRichContentSelected = onRichContentSelected, ) } @@ -400,9 +402,10 @@ private fun TextInput( onResetComposerMode: () -> Unit, resolveRoomMentionDisplay: () -> TextDisplay, resolveMentionDisplay: (text: String, url: String) -> TextDisplay, + onError: (Throwable) -> Unit, + onTyping: (Boolean) -> Unit, + onRichContentSelected: ((Uri) -> Unit)?, modifier: Modifier = Modifier, - onError: (Throwable) -> Unit = {}, - onRichContentSelected: ((Uri) -> Unit)? = null, ) { val bgColor = ElementTheme.colors.bgSubtleSecondary val borderColor = ElementTheme.colors.borderDisabled @@ -451,6 +454,7 @@ private fun TextInput( resolveRoomMentionDisplay = resolveRoomMentionDisplay, onError = onError, onRichContentSelected = onRichContentSelected, + onTyping = onTyping, ) } } @@ -920,6 +924,7 @@ private fun ATextComposer( onSendVoiceMessage = {}, onDeleteVoiceMessage = {}, onError = {}, + onTyping = {}, onSuggestionReceived = {}, onRichContentSelected = null, )