Send typing notification #2240
This commit is contained in:
1
changelog.d/2240.feature
Normal file
1
changelog.d/2240.feature
Normal file
@@ -0,0 +1 @@
|
||||
Send typing notification
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -520,6 +520,10 @@ class RustMatrixRoom(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun typingNotice(isTyping: Boolean) = runCatching {
|
||||
innerRoom.typingNotice(isTyping)
|
||||
}
|
||||
|
||||
override suspend fun generateWidgetWebViewUrl(
|
||||
widgetSettings: MatrixWidgetSettings,
|
||||
clientId: String,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user