Send typing notification #2240

This commit is contained in:
Benoit Marty
2024-01-26 16:08:13 +01:00
parent 8a2490f1e8
commit 2ef8f67951
9 changed files with 62 additions and 2 deletions

1
changelog.d/2240.feature Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount)

View File

@@ -224,6 +224,12 @@ interface MatrixRoom : Closeable {
progressCallback: ProgressCallback?
): 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.
* @param widgetSettings The widget settings to use.

View File

@@ -520,6 +520,10 @@ class RustMatrixRoom(
)
}
override suspend fun typingNotice(isTyping: Boolean) = runCatching {
innerRoom.typingNotice(isTyping)
}
override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,

View File

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

View File

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