mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-3308 Space-level chat | Enhancement | Enable mention query while user is typing (#2084)
This commit is contained in:
parent
93d59016c1
commit
5c520be4ec
2 changed files with 115 additions and 47 deletions
|
@ -263,31 +263,63 @@ class ChatViewModel @Inject constructor(
|
|||
selection: IntRange,
|
||||
text: String
|
||||
) {
|
||||
val query = resolveMentionQuery(
|
||||
text = text,
|
||||
selectionStart = selection.start
|
||||
)
|
||||
if (isMentionTriggered(text, selection.start)) {
|
||||
mentionPanelState.value = MentionPanelState.Visible(
|
||||
results = members.get().let { store ->
|
||||
when(store) {
|
||||
is Store.Data -> {
|
||||
store.members.map { member ->
|
||||
MentionPanelState.Member(
|
||||
member.id,
|
||||
name = member.name.orEmpty(),
|
||||
icon = SpaceMemberIconView.icon(
|
||||
obj = member,
|
||||
urlBuilder = urlBuilder
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Store.Empty -> {
|
||||
emptyList()
|
||||
val results = getMentionedMembers(query)
|
||||
if (query != null) {
|
||||
mentionPanelState.value = MentionPanelState.Visible(
|
||||
results = results,
|
||||
query = query
|
||||
)
|
||||
} else {
|
||||
Timber.w("Query is empty when mention is triggered")
|
||||
}
|
||||
} else if (shouldHideMention(text, selection.start)) {
|
||||
mentionPanelState.value = MentionPanelState.Hidden
|
||||
} else {
|
||||
val results = getMentionedMembers(query)
|
||||
if (results.isNotEmpty() && query != null) {
|
||||
mentionPanelState.value = MentionPanelState.Visible(
|
||||
results = results,
|
||||
query = query
|
||||
)
|
||||
} else {
|
||||
mentionPanelState.value = MentionPanelState.Hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMentionedMembers(query: MentionPanelState.Query?): List<MentionPanelState.Member> {
|
||||
val results = members.get().let { store ->
|
||||
when (store) {
|
||||
is Store.Data -> {
|
||||
store.members.map { member ->
|
||||
MentionPanelState.Member(
|
||||
member.id,
|
||||
name = member.name.orEmpty(),
|
||||
icon = SpaceMemberIconView.icon(
|
||||
obj = member,
|
||||
urlBuilder = urlBuilder
|
||||
)
|
||||
)
|
||||
}.filter { m ->
|
||||
if (query != null) {
|
||||
m.name.contains(query.query, true)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
mentionPanelState.value = MentionPanelState.Hidden
|
||||
|
||||
Store.Empty -> {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
fun onMessageSent(msg: String, markup: List<Block.Content.Text.Mark>) {
|
||||
|
@ -356,7 +388,7 @@ class ChatViewModel @Inject constructor(
|
|||
params = Command.ChatCommand.AddMessage(
|
||||
chat = vmParams.ctx,
|
||||
message = Chat.Message.new(
|
||||
text = msg,
|
||||
text = msg.trim(),
|
||||
attachments = attachments,
|
||||
marks = markup
|
||||
)
|
||||
|
@ -379,7 +411,7 @@ class ChatViewModel @Inject constructor(
|
|||
chat = vmParams.ctx,
|
||||
message = Chat.Message.updated(
|
||||
id = mode.msg,
|
||||
text = msg,
|
||||
text = msg.trim(),
|
||||
attachments = editedMessage?.attachments.orEmpty()
|
||||
)
|
||||
)
|
||||
|
@ -398,7 +430,7 @@ class ChatViewModel @Inject constructor(
|
|||
params = Command.ChatCommand.AddMessage(
|
||||
chat = vmParams.ctx,
|
||||
message = Chat.Message.new(
|
||||
text = msg,
|
||||
text = msg.trim(),
|
||||
replyToMessageId = mode.msg,
|
||||
attachments = attachments,
|
||||
marks = markup
|
||||
|
@ -632,19 +664,32 @@ class ChatViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun isMentionTriggered(text: String, selectionStart: Int): Boolean {
|
||||
// Ensure selectionStart is valid and not out of bounds
|
||||
if (selectionStart <= 0 || selectionStart > text.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check the character before the cursor position
|
||||
fun isMentionTriggered(text: String, selectionStart: Int): Boolean {
|
||||
if (selectionStart <= 0 || selectionStart > text.length) return false
|
||||
val previousChar = text[selectionStart - 1]
|
||||
|
||||
// Trigger mention if the previous character is '@'
|
||||
return previousChar == '@'
|
||||
&& (selectionStart == 1 || !text[selectionStart - 2].isLetterOrDigit())
|
||||
}
|
||||
|
||||
fun shouldHideMention(text: String, selectionStart: Int): Boolean {
|
||||
if (selectionStart > text.length) return false
|
||||
// Check if the current character is a space
|
||||
val currentChar = if (selectionStart > 0) text[selectionStart - 1] else null
|
||||
// Hide mention when a space is typed, or '@' character has been deleted (even if it was the first character)
|
||||
val atCharExists = text.lastIndexOf('@', selectionStart - 1) != -1
|
||||
return currentChar == ' ' || !atCharExists
|
||||
}
|
||||
|
||||
fun resolveMentionQuery(text: String, selectionStart: Int): MentionPanelState.Query? {
|
||||
val atIndex = text.lastIndexOf('@', selectionStart - 1)
|
||||
if (atIndex == -1 || (atIndex > 0 && text[atIndex - 1].isLetterOrDigit())) return null
|
||||
val endIndex = text.indexOf(' ', atIndex).takeIf { it != -1 } ?: text.length
|
||||
val query = text.substring(atIndex + 1, endIndex)
|
||||
// Allow empty queries if there's no space after '@'
|
||||
return MentionPanelState.Query(query, atIndex until endIndex)
|
||||
}
|
||||
|
||||
|
||||
sealed class ViewModelCommand {
|
||||
data object Exit : ViewModelCommand()
|
||||
data object OpenWidgets : ViewModelCommand()
|
||||
|
@ -672,13 +717,20 @@ class ChatViewModel @Inject constructor(
|
|||
|
||||
sealed class MentionPanelState {
|
||||
data object Hidden : MentionPanelState()
|
||||
data class Visible(val results: List<Member>) : MentionPanelState()
|
||||
data class Visible(
|
||||
val results: List<Member>,
|
||||
val query: Query
|
||||
) : MentionPanelState()
|
||||
data class Member(
|
||||
val id: Id,
|
||||
val name: String,
|
||||
val icon: SpaceMemberIconView,
|
||||
val isUser: Boolean = false
|
||||
)
|
||||
data class Query(
|
||||
val query: String,
|
||||
val range: IntRange
|
||||
)
|
||||
}
|
||||
|
||||
sealed class HeaderView {
|
||||
|
|
|
@ -355,35 +355,53 @@ fun ChatScreen(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.noRippleClickable {
|
||||
val start = text.selection.start
|
||||
val end = text.selection.end
|
||||
|
||||
val query = mentionPanelState.query
|
||||
val input = text.text
|
||||
|
||||
val adjustedStart = (start - 1).coerceAtLeast(0)
|
||||
|
||||
val replacementText = member.name + " "
|
||||
|
||||
val lengthDifference = replacementText.length - (query.range.last - query.range.first + 1)
|
||||
|
||||
val updatedText = input.replaceRange(
|
||||
startIndex = adjustedStart,
|
||||
endIndex = end,
|
||||
replacement = replacementText
|
||||
query.range,
|
||||
replacementText
|
||||
)
|
||||
|
||||
// After inserting a mention, all existing spans after the insertion point are shifted based on the text length difference.
|
||||
|
||||
val updatedSpans = spans.map { span ->
|
||||
if (span.start > query.range.last) {
|
||||
when(span) {
|
||||
is ChatBoxSpan.Mention -> {
|
||||
span.copy(
|
||||
start = span.start + lengthDifference,
|
||||
end = span.end + lengthDifference
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
|
||||
text = text.copy(
|
||||
text = updatedText,
|
||||
selection = TextRange(
|
||||
index = (adjustedStart + replacementText.length))
|
||||
index = (query.range.start + replacementText.length)
|
||||
)
|
||||
)
|
||||
|
||||
val mentionSpan = ChatBoxSpan.Mention(
|
||||
start = adjustedStart,
|
||||
end = adjustedStart + member.name.length,
|
||||
start = query.range.start,
|
||||
end = query.range.start + member.name.length,
|
||||
style = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
param = member.id
|
||||
)
|
||||
|
||||
spans = spans + mentionSpan
|
||||
spans = updatedSpans + mentionSpan
|
||||
|
||||
onTextChanged(text)
|
||||
}
|
||||
|
@ -427,9 +445,7 @@ fun ChatScreen(
|
|||
onValueChange = { t, s ->
|
||||
text = t
|
||||
spans = s
|
||||
onTextChanged(
|
||||
t
|
||||
)
|
||||
onTextChanged(t)
|
||||
},
|
||||
text = text,
|
||||
spans = spans
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue