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

15
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -20,6 +20,7 @@ import android.Manifest @@ -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( @@ -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( @@ -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
}

5
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt

@ -78,6 +78,10 @@ internal fun MessageComposerView( @@ -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( @@ -121,6 +125,7 @@ internal fun MessageComposerView(
onDeleteVoiceMessage = onDeleteVoiceMessage,
onSuggestionReceived = ::onSuggestionReceived,
onError = ::onError,
onTyping = ::onTyping,
currentUserId = state.currentUserId,
onRichContentSelected = ::sendUri,
)

15
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt

@ -873,6 +873,21 @@ class MessageComposerPresenterTest { @@ -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)

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

@ -224,6 +224,12 @@ interface MatrixRoom : Closeable { @@ -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.

4
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

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

8
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

@ -115,6 +115,9 @@ class FakeMatrixRoom( @@ -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( @@ -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,

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

@ -112,6 +112,7 @@ fun TextComposer( @@ -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( @@ -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( @@ -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( @@ -451,6 +454,7 @@ private fun TextInput(
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
onError = onError,
onRichContentSelected = onRichContentSelected,
onTyping = onTyping,
)
}
}
@ -920,6 +924,7 @@ private fun ATextComposer( @@ -920,6 +924,7 @@ private fun ATextComposer(
onSendVoiceMessage = {},
onDeleteVoiceMessage = {},
onError = {},
onTyping = {},
onSuggestionReceived = {},
onRichContentSelected = null,
)

Loading…
Cancel
Save