Initial support for member suggestions (#1631)

* Initial support for member suggestion (search and UI)

* Add custom `BottomSheetScaffold` implementation to workaround several scrolling bugs

* Start searching as soon as `@` is typed, add UI following initial designs

* Extract suggestion processing code

* Extract component, add previews, fix tests

* Add tests

* Add exception from kover to the forked bottom sheet code

* Add a feature flag for mentions

- Extract composer & mention suggestions to their composable.
- Extract mentions suggestions processing to its own class.
- Add `MatrixRoom.canTriggerRoomNotification` function.
- Update strings and conditions for displaying the `@room` mention.

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-10-27 12:26:40 +02:00 committed by GitHub
parent d7ce326612
commit 0bd9c78836
312 changed files with 1522 additions and 51 deletions

View file

@ -37,6 +37,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@ -75,6 +76,7 @@ import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
@ -83,6 +85,7 @@ import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlin.time.Duration.Companion.seconds
import uniffi.wysiwyg_composer.MenuAction
@Composable
fun TextComposer(
@ -104,6 +107,7 @@ fun TextComposer(
onSendVoiceMessage: () -> Unit = {},
onDeleteVoiceMessage: () -> Unit = {},
onError: (Throwable) -> Unit = {},
onSuggestionReceived: (Suggestion?) -> Unit = {},
) {
val onSendClicked = {
val html = if (enableTextFormatting) state.messageHtml else null
@ -122,27 +126,31 @@ fun TextComposer(
.fillMaxSize()
.height(IntrinsicSize.Min)
val composerOptionsButton = @Composable {
ComposerOptionsButton(
modifier = Modifier
.size(48.dp),
onClick = onAddAttachment
)
val composerOptionsButton: @Composable () -> Unit = remember {
@Composable {
ComposerOptionsButton(
modifier = Modifier
.size(48.dp),
onClick = onAddAttachment
)
}
}
val textInput = @Composable {
TextInput(
state = state,
subcomposing = subcomposing,
placeholder = if (composerMode.inThread) {
stringResource(id = CommonStrings.action_reply_in_thread)
} else {
stringResource(id = R.string.rich_text_editor_composer_placeholder)
},
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
onError = onError,
)
val textInput: @Composable () -> Unit = remember(state, subcomposing, composerMode, onResetComposerMode, onError) {
@Composable {
TextInput(
state = state,
subcomposing = subcomposing,
placeholder = if (composerMode.inThread) {
stringResource(id = CommonStrings.action_reply_in_thread)
} else {
stringResource(id = R.string.rich_text_editor_composer_placeholder)
},
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
onError = onError,
)
}
}
val canSendMessage by remember { derivedStateOf { state.messageHtml.isNotEmpty() } }
@ -249,6 +257,16 @@ fun TextComposer(
SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it }
}
val menuAction = state.menuAction
LaunchedEffect(menuAction) {
if (menuAction is MenuAction.Suggestion) {
val suggestion = Suggestion(menuAction.suggestionPattern)
onSuggestionReceived(suggestion)
} else {
onSuggestionReceived(null)
}
}
}
@Composable

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.textcomposer.model
import uniffi.wysiwyg_composer.PatternKey
import uniffi.wysiwyg_composer.SuggestionPattern
data class Suggestion(
val start: Int,
val end: Int,
val type: SuggestionType,
val text: String,
) {
constructor(suggestion: SuggestionPattern): this(
suggestion.start.toInt(),
suggestion.end.toInt(),
SuggestionType.fromPatternKey(suggestion.key),
suggestion.text,
)
}
enum class SuggestionType {
Mention,
Command,
Room;
companion object {
fun fromPatternKey(key: PatternKey): SuggestionType {
return when (key) {
PatternKey.AT -> Mention
PatternKey.SLASH -> Command
PatternKey.HASH -> Room
}
}
}
}