mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bd47130e7 | |||
| 12c1b348fe | |||
| ecc8a42b66 | |||
| cd5bb4c146 | |||
| 64f4bcc6fc | |||
| 060d3dd4d4 | |||
| cea3aad0d4 | |||
| f43a2d070b | |||
| 69ad380245 | |||
| 3133d01690 | |||
| 69da8b6345 | |||
| f053447f5f | |||
| a57a2c277d | |||
| 387faa0c27 | |||
| aa990da17c | |||
| a48c82f4a1 |
+7
@@ -6,6 +6,7 @@ import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.onRightClick
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>?
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
|
||||
if (oneHandUI != null && oneHandUI.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
if (!disabled) modifier = modifier
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
|
||||
+15
-1
@@ -802,6 +802,7 @@ interface SomeChat {
|
||||
val id: ChatId
|
||||
val apiId: Long
|
||||
val ready: Boolean
|
||||
val chatDeleted: Boolean
|
||||
val sendMsgEnabled: Boolean
|
||||
val ntfsEnabled: Boolean
|
||||
val incognito: Boolean
|
||||
@@ -882,6 +883,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contact.id
|
||||
override val apiId get() = contact.apiId
|
||||
override val ready get() = contact.ready
|
||||
override val chatDeleted get() = contact.chatDeleted
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||
override val incognito get() = contact.incognito
|
||||
@@ -906,6 +908,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = groupInfo.id
|
||||
override val apiId get() = groupInfo.apiId
|
||||
override val ready get() = groupInfo.ready
|
||||
override val chatDeleted get() = groupInfo.chatDeleted
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||
override val incognito get() = groupInfo.incognito
|
||||
@@ -930,6 +933,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = noteFolder.id
|
||||
override val apiId get() = noteFolder.apiId
|
||||
override val ready get() = noteFolder.ready
|
||||
override val chatDeleted get() = noteFolder.chatDeleted
|
||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
||||
override val incognito get() = noteFolder.incognito
|
||||
@@ -954,6 +958,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactRequest.id
|
||||
override val apiId get() = contactRequest.apiId
|
||||
override val ready get() = contactRequest.ready
|
||||
override val chatDeleted get() = contactRequest.chatDeleted
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||
override val incognito get() = contactRequest.incognito
|
||||
@@ -978,6 +983,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = contactConnection.id
|
||||
override val apiId get() = contactConnection.apiId
|
||||
override val ready get() = contactConnection.ready
|
||||
override val chatDeleted get() = contactConnection.chatDeleted
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = contactConnection.incognito
|
||||
@@ -1003,6 +1009,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val id get() = ""
|
||||
override val apiId get() = 0L
|
||||
override val ready get() = false
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
@@ -1079,6 +1086,7 @@ data class Contact(
|
||||
val chatTs: Instant?,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean,
|
||||
override val chatDeleted: Boolean,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Direct
|
||||
@@ -1152,6 +1160,7 @@ data class Contact(
|
||||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
contactGrpInvSent = false,
|
||||
chatDeleted = false,
|
||||
uiThemes = null,
|
||||
)
|
||||
}
|
||||
@@ -1160,7 +1169,8 @@ data class Contact(
|
||||
@Serializable
|
||||
enum class ContactStatus {
|
||||
@SerialName("active") Active,
|
||||
@SerialName("deleted") Deleted;
|
||||
@SerialName("deleted") Deleted,
|
||||
@SerialName("deletedByUser") DeletedByUser;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1303,6 +1313,7 @@ data class GroupInfo (
|
||||
override val id get() = "#$groupId"
|
||||
override val apiId get() = groupId
|
||||
override val ready get() = membership.memberActive
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||
override val incognito get() = membership.memberIncognito
|
||||
@@ -1608,6 +1619,7 @@ class NoteFolder(
|
||||
override val chatType get() = ChatType.Local
|
||||
override val id get() = "*$noteFolderId"
|
||||
override val apiId get() = noteFolderId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = true
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1644,6 +1656,7 @@ class UserContactRequest (
|
||||
override val chatType get() = ChatType.ContactRequest
|
||||
override val id get() = "<@$contactRequestId"
|
||||
override val apiId get() = contactRequestId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
@@ -1683,6 +1696,7 @@ class PendingContactConnection(
|
||||
override val chatType get() = ChatType.ContactConnection
|
||||
override val id get () = ":$pccConnId"
|
||||
override val apiId get() = pccConnId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
|
||||
+53
-13
@@ -216,13 +216,16 @@ class AppPreferences {
|
||||
|
||||
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||
|
||||
val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true)
|
||||
val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true)
|
||||
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||
|
||||
|
||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
||||
|
||||
|
||||
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, false)
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
get = fun() = settings.getInt(prefName, default),
|
||||
@@ -379,6 +382,7 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
|
||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
||||
private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
|
||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
||||
@@ -399,6 +403,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice"
|
||||
private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice"
|
||||
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||
|
||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
||||
@@ -1155,16 +1161,16 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChat(chat: Chat, notify: Boolean? = null) {
|
||||
suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val cInfo = chat.chatInfo
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) {
|
||||
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) {
|
||||
chatModel.removeChat(chat.remoteHostId, cInfo.id)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean {
|
||||
suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean {
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify))
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val success = when {
|
||||
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
||||
@@ -1185,6 +1191,22 @@ object ChatController {
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? {
|
||||
val type = ChatType.Direct
|
||||
chatModel.deletedChats.value += rh to type.type + id
|
||||
val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode))
|
||||
val contact = when {
|
||||
r is CR.ContactDeleted -> r.contact
|
||||
else -> {
|
||||
val titleId = MR.strings.error_deleting_contact
|
||||
apiErrorAlert("apiDeleteChat", generalGetString(titleId), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
chatModel.deletedChats.value -= rh to type.type + id
|
||||
return contact
|
||||
}
|
||||
|
||||
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
||||
withBGApi {
|
||||
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
||||
@@ -1997,6 +2019,10 @@ 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)
|
||||
chatModel.updateContact(rhId, updatedContact)
|
||||
}
|
||||
chatModel.addChatItem(rhId, cInfo, cItem)
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||
@@ -2710,7 +2736,7 @@ sealed class CC {
|
||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
|
||||
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
|
||||
class ApiClearChat(val type: ChatType, val id: Long): CC()
|
||||
class ApiListContacts(val userId: Long): CC()
|
||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||
@@ -2861,11 +2887,7 @@ sealed class CC {
|
||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
is ApiDeleteChat -> if (notify != null) {
|
||||
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
|
||||
} else {
|
||||
"/_delete ${chatRef(type, id)}"
|
||||
}
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||
is ApiListContacts -> "/_contacts $userId"
|
||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||
@@ -3084,8 +3106,6 @@ sealed class CC {
|
||||
null
|
||||
}
|
||||
|
||||
private fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||
|
||||
companion object {
|
||||
@@ -3095,6 +3115,8 @@ sealed class CC {
|
||||
}
|
||||
}
|
||||
|
||||
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||
|
||||
@Serializable
|
||||
data class NewUser(
|
||||
val profile: Profile?,
|
||||
@@ -5067,6 +5089,19 @@ fun chatError(r: CR): ChatErrorType? {
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ChatDeleteMode {
|
||||
@Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode()
|
||||
@Serializable @SerialName("messages") class Messages: ChatDeleteMode()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is ChatDeleteMode.Full -> "full notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}"
|
||||
is ChatDeleteMode.Messages -> "messages"
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class ConnectionPlan {
|
||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||
@@ -5953,6 +5988,7 @@ data class AppSettings(
|
||||
var uiDarkColorScheme: String? = null,
|
||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||
var uiThemes: List<ThemeOverrides>? = null,
|
||||
var oneHandUI: Boolean? = null
|
||||
) {
|
||||
fun prepareForExport(): AppSettings {
|
||||
val empty = AppSettings()
|
||||
@@ -5982,6 +6018,7 @@ data class AppSettings(
|
||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||
if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI }
|
||||
return empty
|
||||
}
|
||||
|
||||
@@ -6019,6 +6056,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||
oneHandUI?.let { def.oneHandUI.set(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -6049,6 +6087,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = null,
|
||||
uiThemes = null,
|
||||
oneHandUI = false
|
||||
)
|
||||
|
||||
val current: AppSettings
|
||||
@@ -6080,6 +6119,7 @@ data class AppSettings(
|
||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||
uiThemes = def.themeOverrides.get(),
|
||||
oneHandUI = def.oneHandUI.get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+301
-32
@@ -12,13 +12,17 @@ import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
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.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import androidx.compose.ui.text.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -45,9 +49,21 @@ import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
|
||||
sealed class ContactDeleteMode {
|
||||
class Full: ContactDeleteMode()
|
||||
class Entity: ContactDeleteMode()
|
||||
|
||||
fun toChatDeleteMode(notify: Boolean): ChatDeleteMode =
|
||||
when (this) {
|
||||
is Full -> ChatDeleteMode.Full(notify)
|
||||
is Entity -> ChatDeleteMode.Entity(notify)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoView(
|
||||
chatModel: ChatModel,
|
||||
openedFromChatView: Boolean,
|
||||
contact: Contact,
|
||||
connectionStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
@@ -68,6 +84,7 @@ fun ChatInfoView(
|
||||
val chatRh = chat.remoteHostId
|
||||
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||
ChatInfoLayout(
|
||||
openedFromChatView = openedFromChatView,
|
||||
chat,
|
||||
contact,
|
||||
currentUser,
|
||||
@@ -166,7 +183,8 @@ fun ChatInfoView(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
close = close
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -201,53 +219,127 @@ sealed class SendReceipts {
|
||||
|
||||
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
||||
buttons = {
|
||||
Column {
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) {
|
||||
// Delete and notify contact
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
buttons = {
|
||||
Column {
|
||||
// Delete contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = true)
|
||||
notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Full())
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete
|
||||
if (!chatInfo.contact.chatDeleted) {
|
||||
// Delete contact, keep conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Entity())
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_contact_keep_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, notify = false)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyDeleteContactDialog(
|
||||
chat: Chat,
|
||||
chatModel: ChatModel,
|
||||
close: (() -> Unit)? = null,
|
||||
contactDeleteMode: ContactDeleteMode = ContactDeleteMode.Full()
|
||||
) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactActive = chatInfo.contact.ready && chatInfo.contact.active
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (contactActive) generalGetString(MR.strings.notify_delete_contact_question) else generalGetString(MR.strings.confirm_delete_contact_question),
|
||||
text = when (contactDeleteMode) {
|
||||
is ContactDeleteMode.Full -> generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)
|
||||
is ContactDeleteMode.Entity -> generalGetString(MR.strings.delete_contact_cannot_undo_warning)
|
||||
},
|
||||
buttons = {
|
||||
Column {
|
||||
if (contactActive) {
|
||||
// Delete and notify contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = true))
|
||||
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Delete without notification
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false))
|
||||
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false))
|
||||
if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) {
|
||||
showDeleteContactNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeleteContactNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.contact_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteContactNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withBGApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
|
||||
if (ct != null) {
|
||||
when (chatDeleteMode) {
|
||||
is ChatDeleteMode.Full ->
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
is ChatDeleteMode.Entity ->
|
||||
chatModel.updateContact(chatRh, ct)
|
||||
is ChatDeleteMode.Messages ->
|
||||
chatModel.clearChat(chatRh, ChatInfo.Direct(ct))
|
||||
}
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@@ -280,6 +372,7 @@ fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) {
|
||||
|
||||
@Composable
|
||||
fun ChatInfoLayout(
|
||||
openedFromChatView: Boolean,
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
currentUser: User,
|
||||
@@ -300,6 +393,7 @@ fun ChatInfoLayout(
|
||||
syncContactConnection: () -> Unit,
|
||||
syncContactConnectionForce: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -319,7 +413,31 @@ fun ChatInfoLayout(
|
||||
}
|
||||
|
||||
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (contact.activeConn == null && contact.profile.contactLink != null && contact.active) {
|
||||
ConnectButton(openedFromChatView, chat, contact, close)
|
||||
} else if (!contact.active && !contact.chatDeleted) {
|
||||
OpenButton(openedFromChatView, chat, contact, close)
|
||||
} else {
|
||||
MessageButton(openedFromChatView, chat, contact, close)
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
CallButton(chat, contact)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
VideoButton(chat, contact)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (customUserProfile != null) {
|
||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||
SectionItemViewSpaceBetween {
|
||||
@@ -535,6 +653,155 @@ fun LocalAliasEditor(
|
||||
}
|
||||
}
|
||||
|
||||
// when contact is a "contact card"
|
||||
@Composable
|
||||
private fun ConnectButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_connect_button),
|
||||
disabled = false,
|
||||
onClick = {
|
||||
AlertManager.privacySensitive.showAlertDialogButtonsColumn(
|
||||
title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = false, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = true, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
},
|
||||
hostDevice = hostDevice(chat.remoteHostId),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun infoConnectContactViaAddress(openedFromChatView: Boolean, chat: Chat, contact: Contact, incognito: Boolean, close: () -> Unit) {
|
||||
withBGApi {
|
||||
val ok = connectContactViaAddress(chatModel, chat.remoteHostId, contact.contactId, incognito = incognito)
|
||||
if (ok) {
|
||||
if (openedFromChatView) {
|
||||
close.invoke()
|
||||
} else {
|
||||
if (contact.chatDeleted) {
|
||||
chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false))
|
||||
}
|
||||
close.invoke()
|
||||
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpenButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_open_button),
|
||||
disabled = false,
|
||||
onClick = {
|
||||
if (openedFromChatView) {
|
||||
close.invoke()
|
||||
} else {
|
||||
close.invoke()
|
||||
withBGApi {
|
||||
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_message_button),
|
||||
disabled = !contact.sendMsgEnabled,
|
||||
onClick = {
|
||||
if (openedFromChatView) {
|
||||
close.invoke()
|
||||
} else {
|
||||
if (contact.chatDeleted) {
|
||||
chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false))
|
||||
}
|
||||
close.invoke()
|
||||
withBGApi {
|
||||
openDirectChat(chat.remoteHostId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CallButton(chat: Chat, contact: Contact) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_call_filled),
|
||||
title = generalGetString(MR.strings.info_view_call_button),
|
||||
disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null,
|
||||
onClick = {
|
||||
startChatCall(chat, CallMediaType.Audio)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideoButton(chat: Chat, contact: Contact) {
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_videocam_filled),
|
||||
title = generalGetString(MR.strings.info_view_video_button),
|
||||
disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null,
|
||||
onClick = {
|
||||
startChatCall(chat, CallMediaType.Video)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, onClick: () -> Unit) {
|
||||
Surface(
|
||||
Modifier
|
||||
.width(96.dp)
|
||||
.height(66.dp),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
val modifier = if (disabled) Modifier else Modifier.clickable { onClick () }
|
||||
Column(
|
||||
modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = null,
|
||||
Modifier.size(26.dp),
|
||||
tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal),
|
||||
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
@@ -823,6 +1090,7 @@ fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
|
||||
fun PreviewChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
ChatInfoLayout(
|
||||
openedFromChatView = false,
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
@@ -847,6 +1115,7 @@ fun PreviewChatInfoLayout() {
|
||||
syncContactConnection = {},
|
||||
syncContactConnectionForce = {},
|
||||
verifyClicked = {},
|
||||
close = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-13
@@ -190,7 +190,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
ChatInfoView(chatModel, openedFromChatView = true, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
@@ -307,18 +307,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
startCall = out@{ media ->
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
startCall = out@{ media -> startChatCall(chat, media) },
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -522,6 +511,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
|
||||
fun startChatCall(chat: Chat, media: CallMediaType) {
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = chat.remoteHostId, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatLayout(
|
||||
chat: Chat,
|
||||
|
||||
+36
-17
@@ -246,10 +246,10 @@ fun GroupMemberInfoLayout(
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
val cStats = connStats.value
|
||||
fun knownDirectChat(contactId: Long): Chat? {
|
||||
fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? {
|
||||
val chat = getContactChat(contactId)
|
||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
chat
|
||||
chat to chat.chatInfo.contact
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -309,17 +309,37 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
val contactId = member.memberContactId
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
|
||||
if (knownChat != null) {
|
||||
val (chat, contact) = knownChat
|
||||
OpenChatButton(onClick = { openDirectChat(contact.contactId) })
|
||||
Spacer(Modifier.width(10.dp))
|
||||
CallButton(chat, contact)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
VideoButton(chat, contact)
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_call_filled), generalGetString(MR.strings.info_view_call_button), disabled = true, onClick = {})
|
||||
Spacer(Modifier.width(10.dp))
|
||||
InfoViewActionButton(painterResource(MR.images.ic_videocam_filled), generalGetString(MR.strings.info_view_video_button), disabled = true, onClick = {})
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (contactId != null && knownDirectChat(contactId) != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
@@ -513,12 +533,11 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_chat),
|
||||
stringResource(MR.strings.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
InfoViewActionButton(
|
||||
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||
title = generalGetString(MR.strings.info_view_message_button),
|
||||
disabled = false,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+81
-7
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -32,7 +33,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
@@ -47,6 +48,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
val showChatPreviews = chatModel.showChatPreviews.value
|
||||
val inProgress = remember { mutableStateOf(false) }
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(inProgress.value) {
|
||||
progressByTimeout = if (inProgress.value) {
|
||||
delay(1000)
|
||||
@@ -75,6 +77,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
@@ -94,6 +97,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.Local -> {
|
||||
ChatListNavLinkLayout(
|
||||
@@ -112,6 +116,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
@@ -131,6 +136,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.ContactConnection ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -151,6 +157,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
is ChatInfo.InvalidJSON ->
|
||||
ChatListNavLinkLayout(
|
||||
@@ -167,12 +174,13 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
oneHandUI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorChatListItem() {
|
||||
fun ErrorChatListItem() {
|
||||
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
||||
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
||||
}
|
||||
@@ -409,13 +417,73 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat, chatModel)
|
||||
deleteContactConversationDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContactConversationDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactDeletedByUser = chatInfo.contact.contactStatus == ContactStatus.DeletedByUser
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_question) else generalGetString(MR.strings.delete_contact_question),
|
||||
text = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_all_messages_deleted_cannot_undo_warning) else null,
|
||||
buttons = {
|
||||
Column {
|
||||
if (contactDeletedByUser) {
|
||||
// Delete conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Full(notify = false))
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
} else {
|
||||
// Delete contact
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
notifyDeleteContactDialog(chat, chatModel, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
// Only delete conversation
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages())
|
||||
if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) {
|
||||
showDeleteConversationNotice(chatInfo.contact)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDeleteConversationNotice(contact: Contact) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.conversation_deleted),
|
||||
text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showDeleteConversationNotice.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
@@ -594,7 +662,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSuccess: () -> Unit = {}) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.accept_connection_request__question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
|
||||
@@ -603,12 +671,14 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
onSuccess()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
onSuccess()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
@@ -845,6 +915,7 @@ expect fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>? = null
|
||||
)
|
||||
|
||||
@Preview/*(
|
||||
@@ -887,7 +958,8 @@ fun PreviewChatListNavLinkDirect() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -932,7 +1004,8 @@ fun PreviewChatListNavLinkGroup() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -954,7 +1027,8 @@ fun PreviewChatListNavLinkContactRequest() {
|
||||
showMenu = remember { mutableStateOf(false) },
|
||||
disabled = false,
|
||||
selectedChat = remember { mutableStateOf(false) },
|
||||
nextChatSelected = remember { mutableStateOf(false) }
|
||||
nextChatSelected = remember { mutableStateOf(false) },
|
||||
oneHandUI = remember { mutableStateOf(false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+105
-20
@@ -4,6 +4,7 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.BoxScope.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
@@ -32,7 +34,6 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.group.ProgressIndicator
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -41,7 +42,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URI
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
@@ -54,6 +54,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||
else newChatSheetState.value = AnimatedViewState.GONE
|
||||
}
|
||||
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
delay(1000L)
|
||||
@@ -76,7 +78,29 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} },
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!oneHandUI.state.value) {
|
||||
Box(Modifier.padding(end = endPadding)) {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
if (oneHandUI.state.value) {
|
||||
Box(Modifier.padding(end = endPadding)) {
|
||||
ChatListToolbar(
|
||||
scaffoldState.drawerState,
|
||||
userPickerState,
|
||||
stopped
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = {
|
||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||
@@ -89,13 +113,23 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||
floatingActionButton = {
|
||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
||||
var bottom = DEFAULT_PADDING
|
||||
if (oneHandUI.state.value) {
|
||||
bottom = DEFAULT_BOTTOM_PADDING
|
||||
} else {
|
||||
bottom -= 16.dp
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (!stopped) {
|
||||
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
||||
ModalManager.start.closeModals()
|
||||
ModalManager.start.showModalCloseable{
|
||||
NewChatView(rh = chatModel.currentRemoteHost.value)
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp).size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = bottom).size(AppBarHeight * fontSizeSqrtMultiplier),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
@@ -110,13 +144,18 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||
var modifier = Modifier.padding(it).padding(end = endPadding)
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
ChatList(chatModel, searchText = searchText)
|
||||
ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI)
|
||||
}
|
||||
if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||
Text(stringResource(
|
||||
@@ -142,7 +181,12 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
tryOrShowError("UserPicker", error = {}) {
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
|
||||
UserPicker(
|
||||
chatModel = chatModel,
|
||||
userPickerState = userPickerState,
|
||||
containerModifier = Modifier.padding(bottom = AppBarHeight),
|
||||
contentAlignment = if (oneHandUI.state.value) Alignment.BottomStart else Alignment.TopStart) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
@@ -341,6 +385,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
Text(
|
||||
@@ -390,8 +435,14 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>, oneHandUI: SharedPreference<Boolean>) {
|
||||
var modifier = Modifier.fillMaxWidth();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary)
|
||||
@@ -415,7 +466,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
} else {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterEnabledButton()
|
||||
ToggleFilterEnabledButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
@@ -480,9 +531,35 @@ private fun ErrorSettingsView() {
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
enum class ScrollDirection {
|
||||
Up, Down, Idle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>) {
|
||||
private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldValue>, oneHandUI: SharedPreference<Boolean>) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) }
|
||||
var previousIndex by remember { mutableStateOf(0) }
|
||||
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
val currentScrollOffset = listState.firstVisibleItemScrollOffset
|
||||
val threshold = 25
|
||||
|
||||
scrollDirection = when {
|
||||
currentIndex > previousIndex -> ScrollDirection.Down
|
||||
currentIndex < previousIndex -> ScrollDirection.Up
|
||||
currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down
|
||||
currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up
|
||||
currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle
|
||||
else -> scrollDirection
|
||||
}
|
||||
|
||||
previousIndex = currentIndex
|
||||
previousScrollOffset = currentScrollOffset
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
@@ -503,7 +580,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
Modifier
|
||||
.offset {
|
||||
val y = if (searchText.value.text.isEmpty()) {
|
||||
if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) {
|
||||
0
|
||||
} else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -511,7 +590,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
}
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, oneHandUI)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
@@ -519,11 +598,17 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
} }
|
||||
ChatListNavLinkView(chat, nextChatSelected)
|
||||
ChatListNavLinkView(chat, nextChatSelected, oneHandUI.state)
|
||||
}
|
||||
}
|
||||
if (chats.isEmpty() && chatModel.chats.isNotEmpty()) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
var modifier = Modifier.fillMaxSize();
|
||||
|
||||
if (oneHandUI.state.value) {
|
||||
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
}
|
||||
@@ -550,8 +635,8 @@ private fun filteredChats(
|
||||
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.contact.profile.displayName.lowercase().contains(s) ||
|
||||
cInfo.contact.fullName.lowercase().contains(s))
|
||||
}
|
||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||
@@ -570,8 +655,8 @@ private fun filteredChats(
|
||||
|
||||
private fun filtered(chat: Chat): Boolean =
|
||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ fun ChatPreviewView(
|
||||
} else {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) {
|
||||
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
|
||||
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
||||
} else if (!cInfo.ready && cInfo.contact.activeConn != null) {
|
||||
if (cInfo.contact.nextSendGrpInv) {
|
||||
|
||||
+7
-4
@@ -39,11 +39,13 @@ import kotlin.math.roundToInt
|
||||
fun UserPicker(
|
||||
chatModel: ChatModel,
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
containerModifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
showSettings: Boolean = true,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
useFromDesktopClicked: () -> Unit = {},
|
||||
settingsClicked: () -> Unit = {},
|
||||
settingsClicked: () -> Unit = {}
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
@@ -149,16 +151,17 @@ fun UserPicker(
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (animatedFloat.value - 1) * xOffset
|
||||
}
|
||||
},
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
containerModifier
|
||||
.widthIn(min = 260.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.clip(RoundedCornerShape(corner = CornerSize(25.dp))),
|
||||
) {
|
||||
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)
|
||||
LaunchedEffect(chat.id) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
}
|
||||
val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } }
|
||||
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel)
|
||||
ModalManager.start.closeModals()
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||
ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu)
|
||||
}
|
||||
},
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest -> {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) {
|
||||
ContactPreviewView(chat, disabled)
|
||||
}
|
||||
},
|
||||
click = {
|
||||
contactRequestAlertDialog(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo,
|
||||
chatModel,
|
||||
onSuccess = {
|
||||
ModalManager.start.closeModals()
|
||||
}
|
||||
)
|
||||
},
|
||||
dropdownMenuItems = {
|
||||
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
|
||||
ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu)
|
||||
}
|
||||
},
|
||||
showMenu,
|
||||
disabled,
|
||||
selectedChat,
|
||||
nextChatSelected,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
if (contact.activeConn != null) {
|
||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||
}
|
||||
DeleteContactAction(chat, chatModel, showMenu)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat),
|
||||
if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star),
|
||||
onClick = {
|
||||
toggleChatFavorite(chat, !favorite, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_SPACE_AFTER_ICON
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun ContactPreviewView(
|
||||
chat: Chat,
|
||||
disabled: Boolean,
|
||||
) {
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
@Composable
|
||||
fun VerifiedIcon() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun chatPreviewTitle() {
|
||||
val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) }
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (cInfo.contact.verified) {
|
||||
VerifiedIcon()
|
||||
}
|
||||
Text(
|
||||
cInfo.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (deleting) MaterialTheme.colors.secondary else Color.Unspecified
|
||||
)
|
||||
}
|
||||
is ChatInfo.ContactRequest ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
cInfo.chatViewName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = Color.Unspecified
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
ChatInfoImage(cInfo, size = 42.dp)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
|
||||
Box(modifier = Modifier.weight(10f, fill = true)) {
|
||||
chatPreviewTitle()
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
if (chat.chatInfo is ChatInfo.ContactRequest) {
|
||||
Text(
|
||||
text = generalGetString(MR.strings.contact_type_new).uppercase(),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 10.sp * fontSizeMultiplier,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 4.dp)
|
||||
.padding(vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (chat.chatInfo.chatSettings?.favorite == true) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_star_filled),
|
||||
contentDescription = generalGetString(MR.strings.favorite_chat),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(17.dp)
|
||||
)
|
||||
if (chat.chatInfo.incognito) {
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (chat.chatInfo.incognito) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_theater_comedy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(21.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
package chat.simplex.common.views.contacts
|
||||
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
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.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.Chat
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.ChatInfo
|
||||
import chat.simplex.common.model.ContactStatus
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.platform.getKeyboardState
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.helpers.AppBarTitle
|
||||
import chat.simplex.common.views.helpers.KeyChangeEffect
|
||||
import chat.simplex.common.views.helpers.KeyboardState
|
||||
import chat.simplex.common.views.helpers.ModalData
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import chat.simplex.common.views.helpers.ModalView
|
||||
import chat.simplex.common.views.helpers.SearchTextField
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.hostDevice
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
enum class ContactType {
|
||||
CARD, REQUEST, RECENT, REMOVED, UNKNOWN
|
||||
}
|
||||
|
||||
private fun contactChats(c: List<Chat>, contactTypes: List<ContactType>): List<Chat> {
|
||||
return c.filter { chat -> contactTypes.contains(getContactType(chat)) }
|
||||
}
|
||||
|
||||
private fun getContactType(chat: Chat): ContactType {
|
||||
return when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.ContactRequest -> ContactType.REQUEST
|
||||
is ChatInfo.Direct -> {
|
||||
val contact = cInfo.contact;
|
||||
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
|
||||
contact.chatDeleted -> ContactType.REMOVED
|
||||
contact.contactStatus != ContactStatus.DeletedByUser && contact.contactStatus != ContactStatus.Deleted -> ContactType.RECENT
|
||||
else -> ContactType.UNKNOWN
|
||||
}
|
||||
}
|
||||
else -> ContactType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private val chatsByTypeComparator = Comparator<Chat> { chat1, chat2 ->
|
||||
val chat1Type = getContactType(chat1)
|
||||
val chat2Type = getContactType(chat2)
|
||||
|
||||
when {
|
||||
chat1Type.ordinal < chat2Type.ordinal -> -1
|
||||
chat1Type.ordinal > chat2Type.ordinal -> 1
|
||||
|
||||
else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
ModalView(
|
||||
close = close
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.chat_deleted),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
|
||||
ContactsLayout(
|
||||
contactActions = {},
|
||||
contactTypes = listOf(ContactType.REMOVED),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactActionsSection(contactActions: @Composable () -> Unit, rh: RemoteHostInfo?) {
|
||||
contactActions()
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
|
||||
val archived = remember { contactChats(chatModel.chats, listOf(ContactType.REMOVED)) }
|
||||
|
||||
if (archived.isNotEmpty()) {
|
||||
SectionView {
|
||||
SectionItemView(
|
||||
click = {
|
||||
ModalManager.start.showCustomModal { close -> DeletedContactsView(
|
||||
rh = rh,
|
||||
close = close)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_folder_open),
|
||||
contentDescription = stringResource(MR.strings.chat_deleted),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
TextIconSpaced(extraPadding = true)
|
||||
Text(text = stringResource(MR.strings.chat_deleted), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactsLayout(
|
||||
contactActions: @Composable () -> Unit,
|
||||
contactTypes: List<ContactType>,
|
||||
contactListTitle: String? = null) {
|
||||
|
||||
SectionView {
|
||||
ContactsList(
|
||||
contactTypes = contactTypes,
|
||||
contactActions = contactActions,
|
||||
contactListTitle = contactListTitle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactsView(
|
||||
contactActions: @Composable () -> Unit,
|
||||
rh: RemoteHostInfo?
|
||||
) {
|
||||
ContactsLayout(
|
||||
contactActions = { ContactActionsSection(contactActions, rh) },
|
||||
contactTypes = listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST),
|
||||
contactListTitle = stringResource(MR.strings.contact_list_header_title).uppercase()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactsSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, focused: Boolean, onFocusChanged: (hasFocus: Boolean) -> Unit) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary)
|
||||
SearchTextField(
|
||||
Modifier.weight(1f).onFocusChanged { onFocusChanged(it.hasFocus) }.focusRequester(focusRequester),
|
||||
placeholder = stringResource(MR.strings.search_verb),
|
||||
alwaysVisible = true,
|
||||
searchText = searchText,
|
||||
trailingContent = null,
|
||||
) {
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
KeyChangeEffect(chatModel.currentRemoteHost.value) {
|
||||
hideSearchOnBack()
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||
if (chatModel.chats.size > 0) {
|
||||
ToggleFilterButton()
|
||||
}
|
||||
Spacer(Modifier.width(padding))
|
||||
}
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardState = getKeyboardState()
|
||||
LaunchedEffect(keyboardState.value) {
|
||||
if (keyboardState.value == KeyboardState.Closed && focused) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { searchText.value.text }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it.isNotEmpty()) {
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleFilterButton() {
|
||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
null,
|
||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||
.padding(3.dp)
|
||||
.size(sp16)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
private fun ContactsList(
|
||||
contactActions: @Composable () -> Unit,
|
||||
contactTypes: List<ContactType>,
|
||||
contactListTitle: String ? = null
|
||||
) {
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(
|
||||
TextFieldValue("")
|
||||
) }
|
||||
|
||||
var searchFocused by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
lazyListState =
|
||||
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
|
||||
}
|
||||
}
|
||||
val showUnreadAndFavorites =
|
||||
remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
|
||||
val allChats by remember(chatModel.chats, contactTypes) {
|
||||
derivedStateOf { contactChats(chatModel.chats, contactTypes) }
|
||||
}
|
||||
|
||||
val filteredContactChats = filteredContactChats(
|
||||
showUnreadAndFavorites = showUnreadAndFavorites,
|
||||
searchText = searchText.value.text,
|
||||
contactChats = allChats
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
item {
|
||||
SectionView {
|
||||
Divider()
|
||||
ContactsSearchBar(
|
||||
listState = listState,
|
||||
searchText = searchText,
|
||||
focused = searchFocused,
|
||||
onFocusChanged = {
|
||||
searchFocused = it
|
||||
}
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (!searchFocused) {
|
||||
contactActions()
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
|
||||
if (contactListTitle != null && filteredContactChats.isNotEmpty()) {
|
||||
Text(
|
||||
contactListTitle, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(filteredContactChats) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, filteredContactChats) {
|
||||
derivedStateOf {
|
||||
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
}
|
||||
}
|
||||
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
ContactListNavLinkView(chat, nextChatSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
generalGetString(MR.strings.no_filtered_contacts),
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean {
|
||||
var meetsPredicate = true;
|
||||
val s = searchText.trim().lowercase()
|
||||
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
|
||||
}
|
||||
|
||||
if (showUnreadAndFavorites) {
|
||||
meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false)
|
||||
}
|
||||
|
||||
return meetsPredicate;
|
||||
}
|
||||
|
||||
private fun filteredContactChats(
|
||||
showUnreadAndFavorites: Boolean,
|
||||
searchText: String,
|
||||
contactChats: List<Chat>
|
||||
): List<Chat> {
|
||||
return contactChats
|
||||
.filter { chat -> filterChat(
|
||||
chat = chat,
|
||||
searchText = searchText,
|
||||
showUnreadAndFavorites = showUnreadAndFavorites) }
|
||||
.sortedWith(chatsByTypeComparator)
|
||||
}
|
||||
|
||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||
+1
-1
@@ -16,7 +16,7 @@ import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun DefaultTopAppBar(
|
||||
navigationButton: @Composable RowScope.() -> Unit,
|
||||
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||
title: (@Composable () -> Unit)?,
|
||||
onTitleClick: (() -> Unit)? = null,
|
||||
showSearch: Boolean,
|
||||
|
||||
+68
-2
@@ -1,5 +1,8 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
@@ -10,6 +13,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
@@ -19,12 +23,16 @@ import androidx.compose.ui.platform.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.RemoteHostInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.contacts.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -32,10 +40,69 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ModalData.NewChatView(rh: RemoteHostInfo?) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val bottomPadding = DEFAULT_PADDING
|
||||
AppBarTitle(
|
||||
stringResource(MR.strings.new_chat),
|
||||
hostDevice(rh?.remoteHostId),
|
||||
bottomPadding = bottomPadding
|
||||
)
|
||||
}
|
||||
|
||||
ContactsView(
|
||||
contactActions = {
|
||||
NewChatOptions(
|
||||
addContact = {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) }
|
||||
},
|
||||
scanPaste = {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||
},
|
||||
createGroup = {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) }
|
||||
}
|
||||
)
|
||||
},
|
||||
rh = rh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewChatButton(icon: Painter, text: String, click: () -> Unit, textColor: Color = Color.Unspecified, iconColor: Color = MaterialTheme.colors.secondary, disabled: Boolean = false, extraPadding: Boolean = false) {
|
||||
SectionItemView(click, disabled = disabled) {
|
||||
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor)
|
||||
TextIconSpaced(extraPadding)
|
||||
Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewChatOptions(addContact: () -> Unit, scanPaste: () -> Unit, createGroup: () -> Unit) {
|
||||
val actions = remember { listOf(addContact, scanPaste, createGroup) }
|
||||
|
||||
Column {
|
||||
actions.forEachIndexed { index, _ ->
|
||||
NewChatButton(
|
||||
icon = painterResource(icons[index]),
|
||||
text = stringResource(titles[index]),
|
||||
click = actions[index],
|
||||
extraPadding = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
// TODO close new chat if remote host changes in model
|
||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
||||
NewChatSheetLayout(
|
||||
newChatSheetState,
|
||||
stopped,
|
||||
@@ -50,7 +117,6 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) }
|
||||
},
|
||||
|
||||
+4
@@ -43,6 +43,10 @@ fun DeveloperView(
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
|
||||
if (appPlatform.isAndroid) {
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_back_hand), stringResource(MR.strings.one_hand_ui), m.controller.appPrefs.oneHandUI)
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
|
||||
if (checked) {
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
<string name="welcome">Welcome!</string>
|
||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="toolbar_settings">Settings</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
<string name="member_contact_send_direct_message">send direct message</string>
|
||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||
@@ -436,10 +437,28 @@
|
||||
<string name="notifications">Notifications</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="info_view_connect_button">connect</string>
|
||||
<string name="info_view_open_button">open</string>
|
||||
<string name="info_view_message_button">message</string>
|
||||
<string name="info_view_call_button">call</string>
|
||||
<string name="info_view_video_button">video</string>
|
||||
<string name="delete_contact_question">Delete contact?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_contact_cannot_undo_warning">Contact will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_conversation_question">Delete conversation?</string>
|
||||
<string name="delete_conversation_all_messages_deleted_cannot_undo_warning">Conversation and all messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_conversation">Delete conversation</string>
|
||||
<string name="only_delete_conversation">Only delete conversation</string>
|
||||
<string name="delete_contact_keep_conversation">Delete contact, keep conversation</string>
|
||||
<string name="notify_delete_contact_question">Notify contact?</string>
|
||||
<string name="confirm_delete_contact_question">Confirm contact deletion?</string>
|
||||
<string name="delete_and_notify_contact">Delete and notify contact</string>
|
||||
<string name="delete_without_notification">Delete without notification</string>
|
||||
<string name="button_delete_contact">Delete contact</string>
|
||||
<string name="conversation_deleted">Conversation deleted!</string>
|
||||
<string name="you_can_still_send_messages_to_contact">You can still send messages to %1$s from the Contacts tab.</string>
|
||||
<string name="contact_deleted">Contact deleted!</string>
|
||||
<string name="you_can_still_view_conversation_with_contact">You can still view conversation with %1$s in the Chats tab.</string>
|
||||
<string name="text_field_set_contact_placeholder">Set contact name…</string>
|
||||
<string name="icon_descr_server_status_connected">Connected</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
@@ -1247,6 +1266,7 @@
|
||||
<string name="database_downgrade">Database downgrade</string>
|
||||
<string name="incompatible_database_version">Incompatible database version</string>
|
||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||
<string name="one_hand_ui">One-hand UI</string>
|
||||
<string name="terminal_always_visible">Show console in new window</string>
|
||||
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||
@@ -2194,4 +2214,13 @@
|
||||
<string name="download_errors">Download errors</string>
|
||||
<string name="server_address">Server address</string>
|
||||
<string name="open_server_settings_button">Open server settings</string>
|
||||
|
||||
<!-- ContactsView.kt -->
|
||||
<string name="contact_type_new">New</string>
|
||||
<string name="contact_type_deleted">Deleted contacts</string>
|
||||
<string name="chat_deleted">Deleted chats</string>
|
||||
<string name="contact_type_recent">Recent</string>
|
||||
<string name="no_filtered_contacts">No filtered contacts</string>
|
||||
<string name="contact_list_header_title">Your contacts</string>
|
||||
<string name="icon_descr_pending_contact_connection">Pending contact connection</string>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M241.78-244.5 134-136.5q-13.5 13.5-31.25 6.52Q85-136.97 85-156.5V-818q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h675q22.97 0 40.23 17.27Q875-840.97 875-818v516q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27H241.78Z"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
+1
@@ -35,6 +35,7 @@ actual fun ChatListNavLinkLayout(
|
||||
disabled: Boolean,
|
||||
selectedChat: State<Boolean>,
|
||||
nextChatSelected: State<Boolean>,
|
||||
oneHandUI: State<Boolean>?,
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!disabled) modifier = modifier
|
||||
|
||||
Reference in New Issue
Block a user