Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

[Android] Add support for mentions to the Compose library #854

Merged
merged 31 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0682888
Add support for mentions to the Compose library
jmartinesp Oct 23, 2023
c0db8f3
Rename `SetLinkSuggestion`
jmartinesp Oct 23, 2023
8d9eeeb
Remove style comparison
jmartinesp Oct 23, 2023
c45252f
Use `MaterialColor` for the default background of a pill
jmartinesp Oct 23, 2023
2734580
Fix recompositions
jmartinesp Oct 23, 2023
c5ddd52
Extract suggestion related UI and logic to a separate file
jmartinesp Oct 24, 2023
7220b54
Add `MentionDetector` to `MentionDisplayHandler` impls
jmartinesp Oct 24, 2023
ac71bb6
Fix `gitignore` for some reason git was taking into account the build…
jmartinesp Oct 24, 2023
e2fcab1
Fix tests
jmartinesp Oct 24, 2023
c9af2ab
Fix VM leaking the View thought lambdas.
jmartinesp Oct 24, 2023
ce455e7
Solve several PR review issues
jmartinesp Oct 24, 2023
9bf8844
Fix tests (again)
jmartinesp Oct 24, 2023
817e5bb
Wrap `update` blocks in `remember` to prevent unnecessary recompositi…
jmartinesp Oct 25, 2023
59f03ad
Remove `contentEditable` param in hyperlink parsing
jmartinesp Oct 25, 2023
6ba1f1b
Read the right color value for pills in `EditorEditTextAttributeReader`
jmartinesp Oct 25, 2023
b685147
Add docs for new functions in `RichTextEditorState`
jmartinesp Oct 25, 2023
c332b70
Add default `TextDisplay` mention resolvers to `RichTextEditorDefaults`
jmartinesp Oct 25, 2023
1671f7a
Document the `remember(keys..., update)` workaround.
jmartinesp Oct 25, 2023
b9831de
Throw an error if the `HtmlConverter` is not set in the `EditorViewMo…
jmartinesp Oct 25, 2023
18ed041
Fix UI tests
jmartinesp Oct 25, 2023
2d180dc
Add missing `onError` arg to `remember`
jmartinesp Oct 26, 2023
f20c31e
Check `contenteditable` value and apply extra spans accordingly
jmartinesp Oct 26, 2023
8e0aab2
Remove unused import
jmartinesp Oct 26, 2023
f166a9b
Log error when `HtmlConverter` is missing instead of throwing an error
jmartinesp Oct 26, 2023
d604672
Rename `setupHtmlConverter` to `setStyle`.
jmartinesp Oct 26, 2023
d1ca6a4
Remove `MenuAction.Suggestion` when either `ViewAction.ReplaceSuggest…
jmartinesp Oct 26, 2023
438441d
In `FakeViewActionCollector`, replace the mentions text with either a…
jmartinesp Nov 2, 2023
cc01c08
Add tests
jmartinesp Nov 2, 2023
ac1c05f
Add missing dependencies to `EditorViewModel`.
jmartinesp Nov 2, 2023
ee02bfb
Revert "Add missing dependencies to `EditorViewModel`."
jonnyandrew Nov 3, 2023
a8972c0
Fix mention detection logic
jonnyandrew Nov 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bindings/wysiwyg-ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ crate-type = ["cdylib", "staticlib"]
# Keep the uniffi version here in sync with the installed version of
# uniffi-bindgen that is called from
# ../../examples/example-android/app/build.gradle
matrix_mentions = { path = "../../crates/matrix_mentions" }
uniffi = { workspace = true }
uniffi_macros = { workspace = true }
widestring = "1.0.2"
Expand Down
17 changes: 17 additions & 0 deletions bindings/wysiwyg-ffi/src/ffi_mention_detector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::sync::Arc;

#[derive(Default, uniffi::Object)]
pub struct MentionDetector {}

impl MentionDetector {
pub fn new() -> Self {
Self {}
}
}

#[uniffi::export]
impl MentionDetector {
pub fn is_mention(self: &Arc<Self>, url: String) -> bool {
matrix_mentions::is_mention(&url)
}
}
7 changes: 7 additions & 0 deletions bindings/wysiwyg-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod ffi_composer_state;
mod ffi_composer_update;
mod ffi_dom_creation_error;
mod ffi_link_actions;
mod ffi_mention_detector;
mod ffi_mentions_state;
mod ffi_menu_action;
mod ffi_menu_state;
Expand All @@ -39,6 +40,7 @@ pub use crate::ffi_composer_state::ComposerState;
pub use crate::ffi_composer_update::ComposerUpdate;
pub use crate::ffi_dom_creation_error::DomCreationError;
pub use crate::ffi_link_actions::LinkAction;
use crate::ffi_mention_detector::MentionDetector;
pub use crate::ffi_mentions_state::MentionsState;
pub use crate::ffi_menu_action::MenuAction;
pub use crate::ffi_menu_state::MenuState;
Expand All @@ -50,3 +52,8 @@ pub use crate::ffi_text_update::TextUpdate;
pub fn new_composer_model() -> Arc<ComposerModel> {
Arc::new(ComposerModel::new())
}

#[uniffi::export]
pub fn new_mention_detector() -> Arc<MentionDetector> {
Arc::new(MentionDetector::new())
}
4 changes: 4 additions & 0 deletions crates/matrix_mentions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
mod mention;

pub use crate::mention::{Mention, MentionKind, RoomIdentificationType};

pub fn is_mention(url: &str) -> bool {
Mention::from_uri(url).is_some()
}
2 changes: 1 addition & 1 deletion platforms/android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/.idea/kotlinc.xml
/.idea/androidTestResultsUserPreferences.xml
.DS_Store
/build
**/build
/captures
.externalNativeBuild
.cxx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
</activity>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.element.android.wysiwyg.display.MentionDisplayHandler
import io.element.android.wysiwyg.display.TextDisplay

class DefaultMentionDisplayHandler : MentionDisplayHandler {

override fun resolveMentionDisplay(
text: String, url: String
): TextDisplay {
Expand All @@ -13,5 +14,4 @@ class DefaultMentionDisplayHandler : MentionDisplayHandler {
override fun resolveAtRoomMentionDisplay(): TextDisplay {
return TextDisplay.Pill
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand All @@ -26,25 +30,45 @@ import io.element.android.wysiwyg.compose.RichTextEditor
import io.element.android.wysiwyg.compose.RichTextEditorDefaults
import io.element.android.wysiwyg.compose.StyledHtmlConverter
import io.element.android.wysiwyg.compose.rememberRichTextEditorState
import io.element.android.wysiwyg.display.TextDisplay
import io.element.android.wysiwyg.view.models.InlineFormat
import io.element.android.wysiwyg.view.models.LinkAction
import io.element.wysiwyg.compose.matrix.Mention
import io.element.wysiwyg.compose.ui.components.FormattingButtons
import io.element.wysiwyg.compose.ui.theme.RichTextEditorTheme
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.launch
import timber.log.Timber
import uniffi.wysiwyg_composer.ComposerAction
import uniffi.wysiwyg_composer.newMentionDetector

class MainActivity : ComponentActivity() {

private val roomMemberSuggestions = mutableStateListOf<Mention>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mentionDisplayHandler = DefaultMentionDisplayHandler()
val htmlConverter = StyledHtmlConverter(this, mentionDisplayHandler)
val mentionDetector = if (window.decorView.isInEditMode) null else newMentionDetector()
val htmlConverter = StyledHtmlConverter(
context = this,
mentionDisplayHandler = mentionDisplayHandler,
isMention = mentionDetector?.let { detector ->
{ _, url ->
detector.isMention(url)
}
}
)
setContent {
val style = RichTextEditorDefaults.style()
htmlConverter.configureWith(style = style)
RichTextEditorTheme {
val state = rememberRichTextEditorState()
val style = RichTextEditorDefaults.style()
htmlConverter.configureWith(style = style)

val state = rememberRichTextEditorState(initialFocus = true)

LaunchedEffect(state.menuAction) {
processMenuAction(state.menuAction, roomMemberSuggestions)
}

var linkDialogAction by remember { mutableStateOf<LinkAction?>(null) }
val coroutineScope = rememberCoroutineScope()
Expand Down Expand Up @@ -84,18 +108,38 @@ class MainActivity : ComponentActivity() {
) {
RichTextEditor(
state = state,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(10.dp),
style = RichTextEditorDefaults.style(),
onError = Timber::e,
mentionDisplayHandler = mentionDisplayHandler
resolveMentionDisplay = { _,_ -> TextDisplay.Pill },
resolveRoomMentionDisplay = { TextDisplay.Pill },
)
}
EditorStyledText(
text = htmlText,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
resolveMentionDisplay = { _,_ -> TextDisplay.Pill },
resolveRoomMentionDisplay = { TextDisplay.Pill },
)

Spacer(modifier = Modifier.weight(1f))
jonnyandrew marked this conversation as resolved.
Show resolved Hide resolved
SuggestionView(
modifier = Modifier.heightIn(max = 320.dp),
roomMemberSuggestions = roomMemberSuggestions,
onReplaceSuggestionText = {
coroutineScope.launch {
state.replaceSuggestion(it)
}
},
onInsertMentionAtSuggestion = { text, link ->
coroutineScope.launch {
state.insertMentionAtSuggestion(text, link)
}
},
)

FormattingButtons(onResetText = {
coroutineScope.launch {
state.setHtml("")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package io.element.wysiwyg.compose

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Divider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.wysiwyg.compose.matrix.Mention
import uniffi.wysiwyg_composer.MenuAction
import uniffi.wysiwyg_composer.PatternKey

@Composable
fun SuggestionView(
modifier: Modifier = Modifier,
roomMemberSuggestions: SnapshotStateList<Mention>,
onReplaceSuggestionText: (String) -> Unit,
onInsertMentionAtSuggestion: (text: String, link: String) -> Unit,
) {
LazyColumn(
modifier = modifier.fillMaxWidth()
) {
items(roomMemberSuggestions) { item ->
Column {
Text(
text = item.display,
modifier = Modifier.fillMaxWidth()
.padding(10.dp)
.clickable {
if (item == Mention.NotifyEveryone) {
onReplaceSuggestionText(item.text)
} else {
onInsertMentionAtSuggestion(item.text, item.link)
}
})
Divider(modifier = Modifier.fillMaxWidth())
}
}
}
}

/**
* Process the menu action and updates the suggestions accordingly. When the [MenuAction] is [MenuAction.Suggestion],
* different mention suggestions are generated based on the [PatternKey] and the [MenuAction.Suggestion.suggestionPattern].
* Otherwise, the suggestions are cleared.
*/
fun processMenuAction(menuAction: MenuAction?, roomMemberSuggestions: SnapshotStateList<Mention>) {
when (menuAction) {
is MenuAction.Suggestion -> {
processSuggestion(menuAction, roomMemberSuggestions)
}
else -> {
roomMemberSuggestions.clear()
}
}
}

private fun processSuggestion(suggestion: MenuAction.Suggestion, roomMemberSuggestions: SnapshotStateList<Mention>) {
val text = suggestion.suggestionPattern.text
val people = listOf("alice", "bob", "carol", "dan").map(Mention::User)
val rooms = listOf("matrix", "element").map(Mention::Room)
val everyone = Mention.NotifyEveryone
val names = when (suggestion.suggestionPattern.key) {
PatternKey.AT -> people + everyone
PatternKey.HASH -> rooms
PatternKey.SLASH ->
emptyList() // TODO
}
val suggestions = names
.filter { it.display.contains(text) }
roomMemberSuggestions.clear()
roomMemberSuggestions.addAll(suggestions)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.element.wysiwyg.compose.matrix

/**
* Utility model class for the sample app to represent a mention to a
* matrix.org user or room
*/
sealed class Mention(
val display: String,
) {
abstract val key: String
val link get() = "https://matrix.to/#/$key$display:matrix.org"
val text get() = "$key$display"

class Room(
display: String
): Mention(display) {
override val key: String = "#"
}

class User(
display: String
): Mention(display) {
override val key: String = "@"
}

object NotifyEveryone: Mention("room") {
override val key: String = "@"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.element.wysiwyg.compose.matrix

enum class MentionType {
User, Room
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import android.widget.LinearLayout
import androidx.core.view.isGone
import androidx.core.view.isVisible
import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.view.models.InlineFormat
import io.element.android.wysiwyg.view.models.LinkAction
import io.element.android.wysiwyg.poc.databinding.ViewRichTextEditorBinding
import io.element.android.wysiwyg.poc.matrix.Mention
import io.element.android.wysiwyg.poc.matrix.MatrixMentionMentionDisplayHandler
import io.element.android.wysiwyg.poc.matrix.Mention
import io.element.android.wysiwyg.view.models.InlineFormat
import io.element.android.wysiwyg.view.models.LinkAction
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
import uniffi.wysiwyg_composer.MenuAction
Expand Down Expand Up @@ -119,7 +119,7 @@ class RichTextEditor : LinearLayout {
EditorEditText.OnMenuActionChangedListener { menuAction ->
updateSuggestions(menuAction)
}
richTextEditText.mentionDisplayHandler = MatrixMentionMentionDisplayHandler()
richTextEditText.updateStyle(richTextEditText.styleConfig, mentionDisplayHandler = MatrixMentionMentionDisplayHandler)
}
}

Expand Down Expand Up @@ -171,7 +171,7 @@ class RichTextEditor : LinearLayout {
if(item == Mention.NotifyEveryone) {
binding.richTextEditText.replaceTextSuggestion(item.text)
} else {
binding.richTextEditText.setLinkSuggestion(
binding.richTextEditText.insertMentionAtSuggestion(
item.link, item.text
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package io.element.android.wysiwyg.poc.matrix

import io.element.android.wysiwyg.display.TextDisplay
import io.element.android.wysiwyg.display.MentionDisplayHandler
import uniffi.wysiwyg_composer.MentionDetector

/**
* Convenience implementation of a [MentionDisplayHandler] that detects Matrix mentions and
* displays them as default pills.
*/
class MatrixMentionMentionDisplayHandler : MentionDisplayHandler {
object MatrixMentionMentionDisplayHandler: MentionDisplayHandler {
override fun resolveMentionDisplay(text: String, url: String): TextDisplay =
TextDisplay.Pill

override fun resolveAtRoomMentionDisplay(): TextDisplay =
TextDisplay.Pill
}
}
Loading
Loading