mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -19,6 +19,7 @@ import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@@ -149,7 +150,12 @@ fun processIntent(intent: Intent?) {
|
||||
"android.intent.action.VIEW" -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
chatModel.appOpenUrl.value = null to uri.toURI()
|
||||
val transformedUri = uri.toURIOrNull()
|
||||
if (transformedUri != null) {
|
||||
chatModel.appOpenUrl.value = null to transformedUri
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -18,5 +18,6 @@ val NotificationsMode.requiresIgnoringBattery
|
||||
lateinit var APPLICATION_ID: String
|
||||
|
||||
fun Uri.toURI(): URI = URI(toString().replace("\n", ""))
|
||||
fun Uri.toURIOrNull(): URI? = try { toURI() } catch (e: Exception) { null }
|
||||
|
||||
fun URI.toUri(): Uri = Uri.parse(toString())
|
||||
|
||||
+16
-4
@@ -15,13 +15,14 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
@@ -35,11 +36,12 @@ import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.ui.theme.CurrentColors
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.SharedContent
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.lang.reflect.Field
|
||||
import java.net.URI
|
||||
|
||||
@@ -52,6 +54,7 @@ actual fun PlatformTextField(
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onUpArrow: () -> Unit,
|
||||
onFilesPasted: (List<URI>) -> Unit,
|
||||
@@ -82,7 +85,15 @@ actual fun PlatformTextField(
|
||||
freeFocus = true
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { ModalManager.start.modalCount.value }
|
||||
.filter { it > 0 }
|
||||
.collect {
|
||||
freeFocus = true
|
||||
}
|
||||
}
|
||||
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
AndroidView(modifier = Modifier, factory = {
|
||||
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
|
||||
override fun setOnReceiveContentListener(
|
||||
@@ -113,7 +124,8 @@ actual fun PlatformTextField(
|
||||
editText.setTextColor(textColor.toArgb())
|
||||
editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
|
||||
editText.background = ColorDrawable(Color.Transparent.toArgb())
|
||||
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
|
||||
editText.textDirection = if (isRtl) EditText.TEXT_DIRECTION_LOCALE else EditText.TEXT_DIRECTION_ANY_RTL
|
||||
editText.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom)
|
||||
editText.setText(cs.message)
|
||||
editText.hint = placeholder
|
||||
editText.setHintTextColor(hintColor.toArgb())
|
||||
|
||||
+9
-4
@@ -241,10 +241,15 @@ private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int,
|
||||
}
|
||||
|
||||
actual fun getFileName(uri: URI): String? {
|
||||
return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
cursor.getString(nameIndex)
|
||||
return try {
|
||||
androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
// Can make an exception
|
||||
cursor.getString(nameIndex)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ fun StartPartOfScreen(settingsState: SettingsViewState) {
|
||||
fun CenterPartOfScreen() {
|
||||
val currentChatId = remember { ChatModel.chatId }
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { currentChatId }
|
||||
snapshotFlow { currentChatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it != null) {
|
||||
|
||||
+17
-5
@@ -275,6 +275,11 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
// mark chat non deleted
|
||||
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
@@ -879,6 +884,11 @@ interface NamedChat {
|
||||
val localAlias: String
|
||||
val chatViewName: String
|
||||
get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
|
||||
|
||||
fun anyNameContains(searchAnyCase: String): Boolean {
|
||||
val s = searchAnyCase.trim().lowercase()
|
||||
return chatViewName.lowercase().contains(s) || displayName.lowercase().contains(s) || fullName.lowercase().contains(s)
|
||||
}
|
||||
}
|
||||
|
||||
interface SomeChat {
|
||||
@@ -1482,21 +1492,23 @@ data class GroupMember (
|
||||
val memberContactId: Long? = null,
|
||||
val memberContactProfileId: Long,
|
||||
var activeConn: Connection? = null
|
||||
) {
|
||||
): NamedChat {
|
||||
val id: String get() = "#$groupId @$groupMemberId"
|
||||
val displayName: String
|
||||
override val displayName: String
|
||||
get() {
|
||||
val p = memberProfile
|
||||
val name = p.localAlias.ifEmpty { p.displayName }
|
||||
return pastMember(name)
|
||||
}
|
||||
val fullName: String get() = memberProfile.fullName
|
||||
val image: String? get() = memberProfile.image
|
||||
override val fullName: String get() = memberProfile.fullName
|
||||
override val image: String? get() = memberProfile.image
|
||||
val contactLink: String? = memberProfile.contactLink
|
||||
val verified get() = activeConn?.connectionCode != null
|
||||
val blocked get() = blockedByAdmin || !memberSettings.showMessages
|
||||
|
||||
val chatViewName: String
|
||||
override val localAlias: String = memberProfile.localAlias
|
||||
|
||||
override val chatViewName: String
|
||||
get() {
|
||||
val p = memberProfile
|
||||
val name = p.localAlias.ifEmpty { p.displayName + (if (p.fullName == "" || p.fullName == p.displayName) "" else " / ${p.fullName}") }
|
||||
|
||||
+2
-6
@@ -2136,12 +2136,6 @@ object ChatController {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (active(r.user)) {
|
||||
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||
withChats {
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
}
|
||||
withChats {
|
||||
addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
@@ -2529,6 +2523,8 @@ object ChatController {
|
||||
ModalManager.fullscreen.closeModals()
|
||||
fun showAlert(chatError: ChatError) {
|
||||
when {
|
||||
r.rcStopReason is RemoteCtrlStopReason.Disconnected ->
|
||||
{}
|
||||
r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed
|
||||
&& r.rcStopReason.chatError is ChatError.ChatErrorAgent
|
||||
&& r.rcStopReason.chatError.agentError is AgentErrorType.RCP
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ expect fun PlatformTextField(
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onUpArrow: () -> Unit,
|
||||
onFilesPasted: (List<URI>) -> Unit,
|
||||
|
||||
+48
-65
@@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
@@ -49,26 +48,17 @@ import kotlin.math.sign
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -> Unit) {
|
||||
val shouldReturn = remember { mutableStateOf(false) }
|
||||
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val activeChatInfo = remember {
|
||||
derivedStateOf {
|
||||
val info = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo
|
||||
if (info == null) {
|
||||
shouldReturn.value = true
|
||||
}
|
||||
return@derivedStateOf info ?: ChatInfo.Direct.sampleData
|
||||
}
|
||||
}
|
||||
val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } }
|
||||
val user = chatModel.currentUser.value
|
||||
if (shouldReturn.value || user == null) {
|
||||
val chatInfo = activeChatInfo.value
|
||||
if (chatInfo == null || user == null) {
|
||||
LaunchedEffect(Unit) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
} else {
|
||||
val chatInfo = activeChatInfo.value
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
@@ -96,21 +86,17 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
.collect { chatId ->
|
||||
markUnreadChatAsRead(chatId)
|
||||
showSearch.value = false
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
selectedChatItems.value = null
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
val chatRh = remoteHostId.value
|
||||
// We need to have real unreadCount value for displaying it inside top right button
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
val unreadCount = remember {
|
||||
derivedStateOf {
|
||||
chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0
|
||||
chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == activeChatInfo.value?.id }?.chatStats?.unreadCount ?: 0
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
@@ -267,9 +253,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
}
|
||||
}
|
||||
},
|
||||
loadPrevMessages = {
|
||||
if (chatModel.chatId.value != activeChatInfo.value.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||
loadPrevMessages = { chatId ->
|
||||
val c = chatModel.getChat(chatId)
|
||||
if (chatModel.chatId.value != chatId) return@ChatLayout
|
||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
||||
if (c != null && firstId != null) {
|
||||
withBGApi {
|
||||
@@ -279,7 +265,6 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
withBGApi {
|
||||
val cInfo = chatInfo
|
||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
@@ -295,8 +280,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
} else {
|
||||
chatModel.controller.apiDeleteChatItems(
|
||||
chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
type = chatInfo.chatType,
|
||||
id = chatInfo.apiId,
|
||||
itemIds = listOf(itemId),
|
||||
mode = mode
|
||||
)
|
||||
@@ -307,9 +292,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
toChatItem = deleted.toChatItem?.chatItem
|
||||
withChats {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,7 +457,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
markRead = { range, unreadCountAfter ->
|
||||
withBGApi {
|
||||
withChats {
|
||||
markChatItemsRead(chatRh, chatInfo, range, unreadCountAfter)
|
||||
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace
|
||||
withContext(Dispatchers.Main) {
|
||||
markChatItemsRead(chatRh, chatInfo, range, unreadCountAfter)
|
||||
}
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
@@ -486,8 +474,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
|
||||
onSearchValueChanged = { value ->
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
if (chatModel.chatId.value != activeChatInfo.value.id) return@ChatLayout
|
||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||
val c = chatModel.getChat(chatInfo.id) ?: return@ChatLayout
|
||||
if (chatModel.chatId.value != chatInfo.id) return@ChatLayout
|
||||
withBGApi {
|
||||
apiFindMessages(c, chatModel, value)
|
||||
searchText.value = value
|
||||
@@ -556,7 +544,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
@Composable
|
||||
fun ChatLayout(
|
||||
remoteHostId: State<Long?>,
|
||||
chatInfo: State<ChatInfo>,
|
||||
chatInfo: State<ChatInfo?>,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeView: (@Composable () -> Unit),
|
||||
@@ -569,7 +557,7 @@ fun ChatLayout(
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: () -> Unit,
|
||||
loadPrevMessages: (ChatId) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -601,12 +589,11 @@ fun ChatLayout(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.desktopOnExternalDrag(
|
||||
enabled = !attachmentDisabled.value && rememberUpdatedState(chatInfo.value).value.userCanSend,
|
||||
enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value,
|
||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
|
||||
onImage = {
|
||||
// TODO: file is not saved anywhere?!
|
||||
@@ -644,7 +631,10 @@ fun ChatLayout(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (selectedChatItems.value == null) {
|
||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
val chatInfo = chatInfo.value
|
||||
if (chatInfo != null) {
|
||||
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
|
||||
}
|
||||
} else {
|
||||
SelectedItemsTopToolbar(selectedChatItems)
|
||||
}
|
||||
@@ -669,13 +659,17 @@ fun ChatLayout(
|
||||
Modifier)
|
||||
.padding(contentPadding)
|
||||
) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
|
||||
)
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
if (chatInfo != null) {
|
||||
ChatItemsList(
|
||||
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
|
||||
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -685,7 +679,7 @@ fun ChatLayout(
|
||||
|
||||
@Composable
|
||||
fun ChatInfoToolbar(
|
||||
chatInfo: State<ChatInfo>,
|
||||
chatInfo: ChatInfo,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
@@ -710,7 +704,6 @@ fun ChatInfoToolbar(
|
||||
if (appPlatform.isAndroid) {
|
||||
BackHandler(onBack = onBackClicked)
|
||||
}
|
||||
val chatInfo = chatInfo.value
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
@@ -915,22 +908,10 @@ private fun ContactVerifiedShield() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
|
||||
|
||||
val CIListStateSaver = run {
|
||||
val scrolledKey = "scrolled"
|
||||
val countKey = "itemCount"
|
||||
val keyboardKey = "keyboardState"
|
||||
mapSaver(
|
||||
save = { mapOf(scrolledKey to it.scrolled, countKey to it.itemCount, keyboardKey to it.keyboardState) },
|
||||
restore = { CIListState(it[scrolledKey] as Boolean, it[countKey] as Int, it[keyboardKey] as KeyboardState) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxWithConstraintsScope.ChatItemsList(
|
||||
remoteHostId: State<Long?>,
|
||||
chatInfo: State<ChatInfo>,
|
||||
remoteHostId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
unreadCount: State<Int>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
searchValue: State<String>,
|
||||
@@ -938,7 +919,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
linkMode: SimplexLinkMode,
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadPrevMessages: () -> Unit,
|
||||
loadPrevMessages: (ChatId) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -964,8 +945,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val remoteHostId = remember { remoteHostId }.value
|
||||
val chatInfo = remember { chatInfo }.value
|
||||
ScrollToBottom(chatInfo.id, listState, chatModel.chatItems)
|
||||
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
||||
// Scroll to bottom when search value changes from something to nothing and back
|
||||
@@ -980,7 +959,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
|
||||
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
||||
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
||||
@@ -991,6 +970,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) }
|
||||
}
|
||||
}
|
||||
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
|
||||
LaunchedEffect(chatInfo.id) {
|
||||
var stopListening = false
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
|
||||
@@ -1343,14 +1323,17 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
|
||||
@Composable
|
||||
fun PreloadItems(
|
||||
chatId: String,
|
||||
listState: LazyListState,
|
||||
remaining: Int = 10,
|
||||
onLoadMore: () -> Unit,
|
||||
onLoadMore: (ChatId) -> Unit,
|
||||
) {
|
||||
// Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection
|
||||
val allowLoad = remember { mutableStateOf(false) }
|
||||
val chatId = rememberUpdatedState(chatId)
|
||||
val onLoadMore = rememberUpdatedState(onLoadMore)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
snapshotFlow { chatId.value }
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
allowLoad.value = listState.layoutInfo.totalItemsCount == listState.layoutInfo.visibleItemsInfo.size
|
||||
@@ -1370,7 +1353,7 @@ fun PreloadItems(
|
||||
}
|
||||
.filter { it > 0 }
|
||||
.collect {
|
||||
onLoadMore()
|
||||
onLoadMore.value(chatId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ fun SendMsgView(
|
||||
showDeleteTextButton,
|
||||
userIsObserver,
|
||||
if (clicksOnTextFieldDisabled) "" else placeholder,
|
||||
showVoiceButton,
|
||||
onMessageChange,
|
||||
editPrevMessage,
|
||||
onFilesPasted
|
||||
|
||||
+2
-1
@@ -89,7 +89,8 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
||||
.map { it.chatInfo }
|
||||
.filterIsInstance<ChatInfo.Direct>()
|
||||
.map { it.contact }
|
||||
.filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) }
|
||||
.filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s)
|
||||
}
|
||||
.sortedBy { it.displayName.lowercase() }
|
||||
.toList()
|
||||
}
|
||||
|
||||
+6
-1
@@ -278,7 +278,12 @@ fun GroupChatInfoLayout(
|
||||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim().lowercase()) } } }
|
||||
val filteredMembers = remember(members) {
|
||||
derivedStateOf {
|
||||
val s = searchText.value.text.trim().lowercase()
|
||||
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
|
||||
}
|
||||
}
|
||||
// LALAL strange scrolling
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier
|
||||
|
||||
+10
-12
@@ -421,7 +421,10 @@ fun SubscriptionStatusIndicator(click: (() -> Unit)) {
|
||||
}
|
||||
}
|
||||
|
||||
SimpleButtonFrame(click = click) {
|
||||
SimpleButtonFrame(
|
||||
click = click,
|
||||
disabled = chatModel.chatRunning.value != true
|
||||
) {
|
||||
SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess)
|
||||
}
|
||||
}
|
||||
@@ -698,7 +701,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredChats(
|
||||
fun filteredChats(
|
||||
showUnreadAndFavorites: Boolean,
|
||||
searchShowingSimplexLink: State<Boolean>,
|
||||
searchChatFilteredBySimplexLink: State<String?>,
|
||||
@@ -719,18 +722,16 @@ private fun filteredChats(
|
||||
if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
} else {
|
||||
(viewNameContains(cInfo, s) ||
|
||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
cInfo.anyNameContains(s)
|
||||
})
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||
} else {
|
||||
viewNameContains(cInfo, s)
|
||||
cInfo.anyNameContains(s)
|
||||
}
|
||||
is ChatInfo.Local -> s.isEmpty() || viewNameContains(cInfo, s)
|
||||
is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s)
|
||||
is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value)
|
||||
is ChatInfo.Local -> s.isEmpty() || cInfo.anyNameContains(s)
|
||||
is ChatInfo.ContactRequest -> s.isEmpty() || cInfo.anyNameContains(s)
|
||||
is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.anyNameContains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value)
|
||||
is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
@@ -742,6 +743,3 @@ private fun filtered(chat: Chat): Boolean =
|
||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
|
||||
+2
-6
@@ -203,12 +203,8 @@ private fun ShareList(
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val chats by remember(search) {
|
||||
derivedStateOf {
|
||||
val sorted = chatModel.chats.value.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||
if (search.isEmpty()) {
|
||||
sorted.filter { it.chatInfo.ready }
|
||||
} else {
|
||||
sorted.filter { it.chatInfo.ready && it.chatInfo.chatViewName.lowercase().contains(search.lowercase()) }
|
||||
}
|
||||
val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
|
||||
}
|
||||
}
|
||||
LazyColumnWithScrollBar(
|
||||
|
||||
-3
@@ -61,9 +61,6 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDel
|
||||
ContactType.CHAT_DELETED -> {
|
||||
withApi {
|
||||
openChat(rhId, chat.chatInfo, chatModel)
|
||||
withChats {
|
||||
updateContact(rhId, chat.chatInfo.contact.copy(chatDeleted = false))
|
||||
}
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
+22
-2
@@ -10,7 +10,6 @@ import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
@@ -25,10 +24,12 @@ import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SearchTextField(
|
||||
modifier: Modifier,
|
||||
@@ -50,6 +51,25 @@ fun SearchTextField(
|
||||
keyboard?.show()
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
LaunchedEffect(Unit) {
|
||||
val modalCountOnOpen = ModalManager.start.modalCount.value
|
||||
launch {
|
||||
snapshotFlow { ModalManager.start.modalCount.value }
|
||||
.filter { it > modalCountOnOpen }
|
||||
.collect {
|
||||
keyboard?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
if (chatModel.chatId.value != null) {
|
||||
// Delay is needed here because when ChatView is being opened and keyboard is hiding, bottom sheet (to choose attachment) is visible on a screen
|
||||
delay(300)
|
||||
keyboard?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
|
||||
+2
@@ -35,8 +35,10 @@ import java.net.URI
|
||||
@Composable
|
||||
fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) {
|
||||
val rhId = rh?.remoteHostId
|
||||
val view = LocalMultiplatformView()
|
||||
AddGroupLayout(
|
||||
createGroup = { incognito, groupProfile ->
|
||||
hideKeyboard(view)
|
||||
withBGApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
||||
if (groupInfo != null) {
|
||||
|
||||
+1
-6
@@ -491,9 +491,7 @@ private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: B
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
if (searchText.isNotEmpty()) {
|
||||
meetsPredicate = viewNameContains(cInfo, s) ||
|
||||
if (cInfo is ChatInfo.Direct) (cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s)) else false
|
||||
meetsPredicate = cInfo.anyNameContains(s)
|
||||
}
|
||||
|
||||
if (showUnreadAndFavorites) {
|
||||
@@ -503,9 +501,6 @@ private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: B
|
||||
return meetsPredicate
|
||||
}
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
|
||||
private val chatsByTypeComparator = Comparator<Chat> { chat1, chat2 ->
|
||||
val chat1Type = chatContactType(chat1)
|
||||
val chat2Type = chatContactType(chat2)
|
||||
|
||||
+4
@@ -100,10 +100,14 @@ fun SettingsLayout(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val closeSettings: () -> Unit = { scope.launch { drawerState.close() } }
|
||||
val view = LocalMultiplatformView()
|
||||
if (drawerState.isOpen) {
|
||||
BackHandler {
|
||||
closeSettings()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
hideKeyboard(view)
|
||||
}
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
+1
-1
@@ -307,7 +307,7 @@ private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return m.users.filter { u ->
|
||||
if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
|
||||
if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.anyNameContains(lower))) {
|
||||
true
|
||||
} else {
|
||||
correctPassword(u.user, s)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
<string name="you_will_join_group">You will connect to all group members.</string>
|
||||
<string name="connect_via_link_verb">Connect</string>
|
||||
<string name="connect_via_link_incognito">Connect incognito</string>
|
||||
<string name="error_parsing_uri_title">Invalid link</string>
|
||||
<string name="error_parsing_uri_desc">Please check that SimpleX link is correct.</string>
|
||||
|
||||
<!-- MainActivity.kt -->
|
||||
<string name="opening_database">Opening database…</string>
|
||||
|
||||
+17
-5
@@ -47,6 +47,7 @@ actual fun PlatformTextField(
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onUpArrow: () -> Unit,
|
||||
onFilesPasted: (List<URI>) -> Unit,
|
||||
@@ -56,7 +57,6 @@ actual fun PlatformTextField(
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val padding = PaddingValues(0.dp, 12.dp, 50.dp, 0.dp)
|
||||
LaunchedEffect(cs.contextItem) {
|
||||
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
|
||||
// In replying state
|
||||
@@ -71,7 +71,20 @@ actual fun PlatformTextField(
|
||||
keyboard?.hide()
|
||||
}
|
||||
}
|
||||
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
|
||||
val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.subSequence(0, min(50, cs.message.length)))) }
|
||||
val isRtlByCharacters = remember(cs.message) {
|
||||
if (cs.message.isNotEmpty()) isRtl(cs.message.subSequence(0, min(50, cs.message.length))) else lastTimeWasRtlByCharacters.value
|
||||
}
|
||||
LaunchedEffect(isRtlByCharacters) {
|
||||
lastTimeWasRtlByCharacters.value = isRtlByCharacters
|
||||
}
|
||||
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 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)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
@@ -165,9 +178,9 @@ actual fun PlatformTextField(
|
||||
decorationBox = { innerTextField ->
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
LocalLayoutDirection provides if (isRtlByCharacters) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
) {
|
||||
Column(Modifier.weight(1f).padding(start = 0.dp, end = 50.dp)) {
|
||||
Column(Modifier.weight(1f).padding(start = startPadding, end = endPadding)) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = textFieldValue.text,
|
||||
@@ -186,7 +199,6 @@ actual fun PlatformTextField(
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
)
|
||||
showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
|
||||
Reference in New Issue
Block a user