android, desktop: group member mentions (#5574)

* initial wip

* initial parser

* limit mentions

* wip types and ohter changes

* small animation

* better limit

* show mentioned member when mention is in selectable area

* better space handling

* animation working

* changes

* auto tagging

* centralize state

* focus in desktop fix

* close picker on click outside

* use profile display name, avoid local

* show box with max number of mentions

* scrollbar in group mentions desktop

* sending and displaying mentions in views based on latest core code

* latest types and updates new api

* desktop selection area fix

* show mentions correctly

* new notifications UI changes

* local alias support

* mention notifications working

* mentions markdown changes

* fix notifications

* Revert "fix notifications"

This reverts commit 59643c24725d3caee3c629df6732f4b5bc294f8f.

* simple cleanup

* mentions in info view

* refactor/renames

* show member name to replies of my messages as primary

* show local alias and display name for mentions

* show 4 rows and almost all of 5th as picker max height

* only call list members api on new @ and searchn in all names

* fix

* correction

* fixes

* unread mentions chat stats

* unread indication in chat

* filtering of unread

* show @ in chat previews

* @ style

* alone @

* forgotten change

* deleted

* remove whitespace

* fix to make clear chat mark tags red

* comments changes

* @ as icon to avoid issues

* change

* simplify like ios

* renames

* wip using haskell parser

* show mention name containing @ in quotes

* cleanup and position of cursor after replace

* move

* show selected tick and edits working

* cimention in map

* eol

* text selection

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2025-02-03 18:05:40 +00:00
1 parent 82dffd55a9
commit 760ea17fb9
29 files changed
+830 -225

No files matched your search

@@ -10,8 +10,7 @@ import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.*
@@ -51,19 +50,21 @@ actual fun PlatformTextField(
userIsObserver: Boolean,
placeholder: String,
showVoiceButton: Boolean,
onMessageChange: (String) -> Unit,
onMessageChange: (ComposeMessage) -> Unit,
onUpArrow: () -> Unit,
onFilesPasted: (List<URI>) -> Unit,
focusRequester: FocusRequester?,
onDone: () -> Unit,
) {
val cs = composeState.value
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current
val focusReq = focusRequester ?: remember { FocusRequester() }
LaunchedEffect(cs.contextItem) {
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
// In replying state
focusRequester.requestFocus()
focusReq.requestFocus()
delay(50)
keyboard?.show()
}
@@ -74,9 +75,9 @@ actual fun PlatformTextField(
keyboard?.hide()
}
}
val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.subSequence(0, min(50, cs.message.length)))) }
val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.text.subSequence(0, min(50, cs.message.text.length)))) }
val isRtlByCharacters = remember(cs.message) {
if (cs.message.isNotEmpty()) isRtl(cs.message.subSequence(0, min(50, cs.message.length))) else lastTimeWasRtlByCharacters.value
if (cs.message.text.isNotEmpty()) isRtl(cs.message.text.subSequence(0, min(50, cs.message.text.length))) else lastTimeWasRtlByCharacters.value
}
LaunchedEffect(isRtlByCharacters) {
lastTimeWasRtlByCharacters.value = isRtlByCharacters
@@ -84,12 +85,12 @@ actual fun PlatformTextField(
val isLtrGlobally = LocalLayoutDirection.current == LayoutDirection.Ltr
// Different padding here is for a text that is considered RTL with non-RTL locale set globally.
// In this case padding from right side should be bigger
val startEndPadding = if (cs.message.isEmpty() && showVoiceButton && isRtlByCharacters && isLtrGlobally) 95.dp else 50.dp
val startEndPadding = if (cs.message.text.isEmpty() && showVoiceButton && isRtlByCharacters && isLtrGlobally) 95.dp else 50.dp
val startPadding = if (isRtlByCharacters && isLtrGlobally) startEndPadding else 0.dp
val endPadding = if (isRtlByCharacters && isLtrGlobally) 0.dp else startEndPadding
val padding = PaddingValues(startPadding, 12.dp, endPadding, 0.dp)
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
val textFieldValue = textFieldValueState.copy(text = cs.message)
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message.text, selection = cs.message.selection)) }
val textFieldValue = textFieldValueState.copy(text = cs.message.text, selection = cs.message.selection)
val clipboard = LocalClipboardManager.current
BasicTextField(
value = textFieldValue,
@@ -105,7 +106,7 @@ actual fun PlatformTextField(
}
}
textFieldValueState = it
onMessageChange(it.text)
onMessageChange(ComposeMessage(it.text, it.selection))
}
},
textStyle = textStyle.value,
@@ -118,7 +119,7 @@ actual fun PlatformTextField(
.padding(start = startPadding, end = endPadding)
.offset(y = (-5).dp)
.fillMaxWidth()
.focusRequester(focusRequester)
.focusRequester(focusReq)
.onPreviewKeyEvent {
if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyDown) {
if (it.isShiftPressed) {
@@ -129,12 +130,12 @@ actual fun PlatformTextField(
text = newText,
selection = TextRange(textFieldValue.selection.min + 1)
)
onMessageChange(newText)
onMessageChange(ComposeMessage(newText, textFieldValueState.selection))
} else if (!sendMsgButtonDisabled) {
onDone()
}
true
} else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) {
} else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.text.isEmpty()) {
onUpArrow()
true
} else if (it.key == Key.V &&
@@ -166,7 +167,7 @@ actual fun PlatformTextField(
chatModel.filesToDelete.add(tempFile)
tempFile.writeBytes(bytes)
composeState.processPickedMedia(listOf(tempFile.toURI()), composeState.value.message)
composeState.processPickedMedia(listOf(tempFile.toURI()), composeState.value.message.text)
}
} catch (e: Exception) {
Log.e(TAG, "Pasting image exception: ${e.stackTraceToString()}")
@@ -200,7 +201,7 @@ actual fun PlatformTextField(
}
}
)
showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress
showDeleteTextButton.value = cs.message.text.split("\n").size >= 4 && !cs.inProgress
if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
} else if (userIsObserver) {
@@ -111,7 +111,9 @@ actual fun LazyColumnWithScrollBarNoAppBar(
additionalBarOffset: State<Dp>?,
additionalTopBar: State<Boolean>,
chatBottomBar: State<Boolean>,
content: LazyListScope.() -> Unit
maxHeight: State<Dp>?,
containerAlignment: Alignment,
content: LazyListScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
val scrollBarAlpha = remember { Animatable(0f) }
@@ -135,9 +137,11 @@ actual fun LazyColumnWithScrollBarNoAppBar(
// When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state
// (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row)
val scrollBarDraggingState = remember { mutableStateOf(false) }
Box {
Box(contentAlignment = containerAlignment) {
LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content)
ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar)
Box(if (maxHeight?.value != null) Modifier.height(maxHeight.value).fillMaxWidth() else Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState)
}
}
}