Browse Source

Send typing notification #2240

pull/2302/head
Benoit Marty 8 months ago
parent
commit
bfb6b32740
  1. 1
      changelog.d/2240.feature
  2. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt
  3. 15
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  4. 5
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
  5. 15
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  6. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  7. 4
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  8. 8
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  9. 9
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

1
changelog.d/2240.feature

@ -0,0 +1 @@
Send typing notification

1
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 class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
data object CancelSendAttachment : MessageComposerEvents data object CancelSendAttachment : MessageComposerEvents
data class Error(val error: Throwable) : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents
data class TypingNotice(val isTyping: Boolean) : MessageComposerEvents
data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents data class SuggestionReceived(val suggestion: Suggestion?) : MessageComposerEvents
data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents data class InsertMention(val mention: MentionSuggestion) : MessageComposerEvents
} }

15
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.annotation.SuppressLint
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -207,6 +208,15 @@ class MessageComposerPresenter @Inject constructor(
.collect() .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) { fun handleEvents(event: MessageComposerEvents) {
when (event) { when (event) {
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
@ -299,6 +309,11 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerEvents.Error -> { is MessageComposerEvents.Error -> {
analyticsService.trackError(event.error) analyticsService.trackError(event.error)
} }
is MessageComposerEvents.TypingNotice -> {
localCoroutineScope.launch {
room.typingNotice(event.isTyping)
}
}
is MessageComposerEvents.SuggestionReceived -> { is MessageComposerEvents.SuggestionReceived -> {
suggestionSearchTrigger.value = event.suggestion suggestionSearchTrigger.value = event.suggestion
} }

5
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)) state.eventSink(MessageComposerEvents.Error(error))
} }
fun onTyping(typing: Boolean) {
state.eventSink(MessageComposerEvents.TypingNotice(typing))
}
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
fun onRequestFocus() { fun onRequestFocus() {
coroutineScope.launch { coroutineScope.launch {
@ -121,6 +125,7 @@ internal fun MessageComposerView(
onDeleteVoiceMessage = onDeleteVoiceMessage, onDeleteVoiceMessage = onDeleteVoiceMessage,
onSuggestionReceived = ::onSuggestionReceived, onSuggestionReceived = ::onSuggestionReceived,
onError = ::onError, onError = ::onError,
onTyping = ::onTyping,
currentUserId = state.currentUserId, currentUserId = state.currentUserId,
onRichContentSelected = ::sendUri, onRichContentSelected = ::sendUri,
) )

15
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<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState { private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount) skipItems(skipCount)

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

@ -224,6 +224,12 @@ interface MatrixRoom : Closeable {
progressCallback: ProgressCallback? progressCallback: ProgressCallback?
): Result<MediaUploadHandler> ): Result<MediaUploadHandler>
/**
* Send a typing notification.
* @param isTyping True if the user is typing, false otherwise.
*/
suspend fun typingNotice(isTyping: Boolean): Result<Unit>
/** /**
* Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters. * Generates a Widget url to display in a [android.webkit.WebView] given the provided parameters.
* @param widgetSettings The widget settings to use. * @param widgetSettings The widget settings to use.

4
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( override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings, widgetSettings: MatrixWidgetSettings,
clientId: String, clientId: String,

8
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<Boolean> = Result.success(true) private var canUserJoinCallResult: Result<Boolean> = Result.success(true)
var sendMessageMentions = emptyList<Mention>() var sendMessageMentions = emptyList<Mention>()
val editMessageCalls = mutableListOf<Pair<String, String?>>() val editMessageCalls = mutableListOf<Pair<String, String?>>()
private val _typingRecord = mutableListOf<Boolean>()
val typingRecord: List<Boolean>
get() = _typingRecord
var sendMediaCount = 0 var sendMediaCount = 0
private set private set
@ -426,6 +429,11 @@ class FakeMatrixRoom(
progressCallback: ProgressCallback? progressCallback: ProgressCallback?
): Result<MediaUploadHandler> = fakeSendMedia(progressCallback) ): Result<MediaUploadHandler> = fakeSendMedia(progressCallback)
override suspend fun typingNotice(isTyping: Boolean): Result<Unit> {
_typingRecord += isTyping
return Result.success(Unit)
}
override suspend fun generateWidgetWebViewUrl( override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings, widgetSettings: MatrixWidgetSettings,
clientId: String, clientId: String,

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

@ -112,6 +112,7 @@ fun TextComposer(
onSendVoiceMessage: () -> Unit, onSendVoiceMessage: () -> Unit,
onDeleteVoiceMessage: () -> Unit, onDeleteVoiceMessage: () -> Unit,
onError: (Throwable) -> Unit, onError: (Throwable) -> Unit,
onTyping: (Boolean) -> Unit,
onSuggestionReceived: (Suggestion?) -> Unit, onSuggestionReceived: (Suggestion?) -> Unit,
onRichContentSelected: ((Uri) -> Unit)?, onRichContentSelected: ((Uri) -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -165,6 +166,7 @@ fun TextComposer(
resolveMentionDisplay = { text, url -> TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(text, url)) }, resolveMentionDisplay = { text, url -> TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor(text, url)) },
resolveRoomMentionDisplay = { TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor("@room", "#")) }, resolveRoomMentionDisplay = { TextDisplay.Custom(mentionSpanProvider.getMentionSpanFor("@room", "#")) },
onError = onError, onError = onError,
onTyping = onTyping,
onRichContentSelected = onRichContentSelected, onRichContentSelected = onRichContentSelected,
) )
} }
@ -400,9 +402,10 @@ private fun TextInput(
onResetComposerMode: () -> Unit, onResetComposerMode: () -> Unit,
resolveRoomMentionDisplay: () -> TextDisplay, resolveRoomMentionDisplay: () -> TextDisplay,
resolveMentionDisplay: (text: String, url: String) -> TextDisplay, resolveMentionDisplay: (text: String, url: String) -> TextDisplay,
onError: (Throwable) -> Unit,
onTyping: (Boolean) -> Unit,
onRichContentSelected: ((Uri) -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onError: (Throwable) -> Unit = {},
onRichContentSelected: ((Uri) -> Unit)? = null,
) { ) {
val bgColor = ElementTheme.colors.bgSubtleSecondary val bgColor = ElementTheme.colors.bgSubtleSecondary
val borderColor = ElementTheme.colors.borderDisabled val borderColor = ElementTheme.colors.borderDisabled
@ -451,6 +454,7 @@ private fun TextInput(
resolveRoomMentionDisplay = resolveRoomMentionDisplay, resolveRoomMentionDisplay = resolveRoomMentionDisplay,
onError = onError, onError = onError,
onRichContentSelected = onRichContentSelected, onRichContentSelected = onRichContentSelected,
onTyping = onTyping,
) )
} }
} }
@ -920,6 +924,7 @@ private fun ATextComposer(
onSendVoiceMessage = {}, onSendVoiceMessage = {},
onDeleteVoiceMessage = {}, onDeleteVoiceMessage = {},
onError = {}, onError = {},
onTyping = {},
onSuggestionReceived = {}, onSuggestionReceived = {},
onRichContentSelected = null, onRichContentSelected = null,
) )

Loading…
Cancel
Save