Skip to content

Commit

Permalink
Merge pull request #2302 from element-hq/feature/bma/sendTyping
Browse files Browse the repository at this point in the history
Send typing notification
  • Loading branch information
bmarty authored Jan 29, 2024
2 parents 0bcd3ba + bfb6b32 commit 22defe5
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog.d/2240.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Send typing notification
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -121,6 +125,7 @@ internal fun MessageComposerView(
onDeleteVoiceMessage = onDeleteVoiceMessage,
onSuggestionReceived = ::onSuggestionReceived,
onError = ::onError,
onTyping = ::onTyping,
currentUserId = state.currentUserId,
onRichContentSelected = ::sendUri,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ class RustMatrixRoom(
)
}

override suspend fun typingNotice(isTyping: Boolean) = runCatching {
innerRoom.typingNotice(isTyping)
}

override suspend fun generateWidgetWebViewUrl(
widgetSettings: MatrixWidgetSettings,
clientId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -451,6 +454,7 @@ private fun TextInput(
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
onError = onError,
onRichContentSelected = onRichContentSelected,
onTyping = onTyping,
)
}
}
Expand Down Expand Up @@ -920,6 +924,7 @@ private fun ATextComposer(
onSendVoiceMessage = {},
onDeleteVoiceMessage = {},
onError = {},
onTyping = {},
onSuggestionReceived = {},
onRichContentSelected = null,
)
Expand Down

0 comments on commit 22defe5

Please sign in to comment.