Send typing notification #2240
This commit is contained in:
parent
8a2490f1e8
commit
2ef8f67951
9 changed files with 62 additions and 2 deletions
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 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue