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.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.platform.onRightClick
|
import chat.simplex.common.platform.onRightClick
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout(
|
|||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
selectedChat: State<Boolean>,
|
selectedChat: State<Boolean>,
|
||||||
nextChatSelected: State<Boolean>,
|
nextChatSelected: State<Boolean>,
|
||||||
|
oneHandUI: State<Boolean>?
|
||||||
) {
|
) {
|
||||||
var modifier = Modifier.fillMaxWidth()
|
var modifier = Modifier.fillMaxWidth()
|
||||||
|
|
||||||
|
if (oneHandUI != null && oneHandUI.value) {
|
||||||
|
modifier = modifier.scale(scaleX = 1f, scaleY = -1f)
|
||||||
|
}
|
||||||
|
|
||||||
if (!disabled) modifier = modifier
|
if (!disabled) modifier = modifier
|
||||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||||
.onRightClick { showMenu.value = true }
|
.onRightClick { showMenu.value = true }
|
||||||
|
|||||||
+15
-1
@@ -802,6 +802,7 @@ interface SomeChat {
|
|||||||
val id: ChatId
|
val id: ChatId
|
||||||
val apiId: Long
|
val apiId: Long
|
||||||
val ready: Boolean
|
val ready: Boolean
|
||||||
|
val chatDeleted: Boolean
|
||||||
val sendMsgEnabled: Boolean
|
val sendMsgEnabled: Boolean
|
||||||
val ntfsEnabled: Boolean
|
val ntfsEnabled: Boolean
|
||||||
val incognito: Boolean
|
val incognito: Boolean
|
||||||
@@ -882,6 +883,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = contact.id
|
override val id get() = contact.id
|
||||||
override val apiId get() = contact.apiId
|
override val apiId get() = contact.apiId
|
||||||
override val ready get() = contact.ready
|
override val ready get() = contact.ready
|
||||||
|
override val chatDeleted get() = contact.chatDeleted
|
||||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||||
override val incognito get() = contact.incognito
|
override val incognito get() = contact.incognito
|
||||||
@@ -906,6 +908,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = groupInfo.id
|
override val id get() = groupInfo.id
|
||||||
override val apiId get() = groupInfo.apiId
|
override val apiId get() = groupInfo.apiId
|
||||||
override val ready get() = groupInfo.ready
|
override val ready get() = groupInfo.ready
|
||||||
|
override val chatDeleted get() = groupInfo.chatDeleted
|
||||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||||
override val incognito get() = groupInfo.incognito
|
override val incognito get() = groupInfo.incognito
|
||||||
@@ -930,6 +933,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = noteFolder.id
|
override val id get() = noteFolder.id
|
||||||
override val apiId get() = noteFolder.apiId
|
override val apiId get() = noteFolder.apiId
|
||||||
override val ready get() = noteFolder.ready
|
override val ready get() = noteFolder.ready
|
||||||
|
override val chatDeleted get() = noteFolder.chatDeleted
|
||||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
override val ntfsEnabled get() = noteFolder.ntfsEnabled
|
||||||
override val incognito get() = noteFolder.incognito
|
override val incognito get() = noteFolder.incognito
|
||||||
@@ -954,6 +958,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = contactRequest.id
|
override val id get() = contactRequest.id
|
||||||
override val apiId get() = contactRequest.apiId
|
override val apiId get() = contactRequest.apiId
|
||||||
override val ready get() = contactRequest.ready
|
override val ready get() = contactRequest.ready
|
||||||
|
override val chatDeleted get() = contactRequest.chatDeleted
|
||||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||||
override val incognito get() = contactRequest.incognito
|
override val incognito get() = contactRequest.incognito
|
||||||
@@ -978,6 +983,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = contactConnection.id
|
override val id get() = contactConnection.id
|
||||||
override val apiId get() = contactConnection.apiId
|
override val apiId get() = contactConnection.apiId
|
||||||
override val ready get() = contactConnection.ready
|
override val ready get() = contactConnection.ready
|
||||||
|
override val chatDeleted get() = contactConnection.chatDeleted
|
||||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
override val incognito get() = contactConnection.incognito
|
override val incognito get() = contactConnection.incognito
|
||||||
@@ -1003,6 +1009,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
override val id get() = ""
|
override val id get() = ""
|
||||||
override val apiId get() = 0L
|
override val apiId get() = 0L
|
||||||
override val ready get() = false
|
override val ready get() = false
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val sendMsgEnabled get() = false
|
override val sendMsgEnabled get() = false
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
override val incognito get() = false
|
override val incognito get() = false
|
||||||
@@ -1079,6 +1086,7 @@ data class Contact(
|
|||||||
val chatTs: Instant?,
|
val chatTs: Instant?,
|
||||||
val contactGroupMemberId: Long? = null,
|
val contactGroupMemberId: Long? = null,
|
||||||
val contactGrpInvSent: Boolean,
|
val contactGrpInvSent: Boolean,
|
||||||
|
override val chatDeleted: Boolean,
|
||||||
val uiThemes: ThemeModeOverrides? = null,
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Direct
|
override val chatType get() = ChatType.Direct
|
||||||
@@ -1152,6 +1160,7 @@ data class Contact(
|
|||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
chatTs = Clock.System.now(),
|
chatTs = Clock.System.now(),
|
||||||
contactGrpInvSent = false,
|
contactGrpInvSent = false,
|
||||||
|
chatDeleted = false,
|
||||||
uiThemes = null,
|
uiThemes = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1160,7 +1169,8 @@ data class Contact(
|
|||||||
@Serializable
|
@Serializable
|
||||||
enum class ContactStatus {
|
enum class ContactStatus {
|
||||||
@SerialName("active") Active,
|
@SerialName("active") Active,
|
||||||
@SerialName("deleted") Deleted;
|
@SerialName("deleted") Deleted,
|
||||||
|
@SerialName("deletedByUser") DeletedByUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -1303,6 +1313,7 @@ data class GroupInfo (
|
|||||||
override val id get() = "#$groupId"
|
override val id get() = "#$groupId"
|
||||||
override val apiId get() = groupId
|
override val apiId get() = groupId
|
||||||
override val ready get() = membership.memberActive
|
override val ready get() = membership.memberActive
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val sendMsgEnabled get() = membership.memberActive
|
override val sendMsgEnabled get() = membership.memberActive
|
||||||
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
|
||||||
override val incognito get() = membership.memberIncognito
|
override val incognito get() = membership.memberIncognito
|
||||||
@@ -1608,6 +1619,7 @@ class NoteFolder(
|
|||||||
override val chatType get() = ChatType.Local
|
override val chatType get() = ChatType.Local
|
||||||
override val id get() = "*$noteFolderId"
|
override val id get() = "*$noteFolderId"
|
||||||
override val apiId get() = noteFolderId
|
override val apiId get() = noteFolderId
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val ready get() = true
|
override val ready get() = true
|
||||||
override val sendMsgEnabled get() = true
|
override val sendMsgEnabled get() = true
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
@@ -1644,6 +1656,7 @@ class UserContactRequest (
|
|||||||
override val chatType get() = ChatType.ContactRequest
|
override val chatType get() = ChatType.ContactRequest
|
||||||
override val id get() = "<@$contactRequestId"
|
override val id get() = "<@$contactRequestId"
|
||||||
override val apiId get() = contactRequestId
|
override val apiId get() = contactRequestId
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val ready get() = true
|
override val ready get() = true
|
||||||
override val sendMsgEnabled get() = false
|
override val sendMsgEnabled get() = false
|
||||||
override val ntfsEnabled get() = false
|
override val ntfsEnabled get() = false
|
||||||
@@ -1683,6 +1696,7 @@ class PendingContactConnection(
|
|||||||
override val chatType get() = ChatType.ContactConnection
|
override val chatType get() = ChatType.ContactConnection
|
||||||
override val id get () = ":$pccConnId"
|
override val id get () = ":$pccConnId"
|
||||||
override val apiId get() = pccConnId
|
override val apiId get() = pccConnId
|
||||||
|
override val chatDeleted get() = false
|
||||||
override val ready get() = false
|
override val ready get() = false
|
||||||
override val sendMsgEnabled get() = false
|
override val sendMsgEnabled get() = false
|
||||||
override val ntfsEnabled 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 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 showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
|
||||||
|
|
||||||
|
|
||||||
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
|
||||||
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
|
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) =
|
private fun mkIntPreference(prefName: String, default: Int) =
|
||||||
SharedPreference(
|
SharedPreference(
|
||||||
get = fun() = settings.getInt(prefName, default),
|
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_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||||
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
|
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
|
||||||
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
|
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 = "LocalAuthenticationSelfDestruct"
|
||||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
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_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||||
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
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_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
|
||||||
|
|
||||||
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
|
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
|
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)
|
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
|
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 {
|
val success = when {
|
||||||
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
r is CR.ContactDeleted && type == ChatType.Direct -> true
|
||||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true
|
||||||
@@ -1185,6 +1191,22 @@ object ChatController {
|
|||||||
return success
|
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) {
|
fun clearChat(chat: Chat, close: (() -> Unit)? = null) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
|
||||||
@@ -1997,6 +2019,10 @@ object ChatController {
|
|||||||
val cInfo = r.chatItem.chatInfo
|
val cInfo = r.chatItem.chatInfo
|
||||||
val cItem = r.chatItem.chatItem
|
val cItem = r.chatItem.chatItem
|
||||||
if (active(r.user)) {
|
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)
|
chatModel.addChatItem(rhId, cInfo, cItem)
|
||||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
|
||||||
chatModel.increaseUnreadCounter(rhId, r.user)
|
chatModel.increaseUnreadCounter(rhId, r.user)
|
||||||
@@ -2710,7 +2736,7 @@ sealed class CC {
|
|||||||
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
class APIConnectPlan(val userId: Long, val connReq: String): CC()
|
||||||
class APIConnect(val userId: Long, val incognito: Boolean, 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 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 ApiClearChat(val type: ChatType, val id: Long): CC()
|
||||||
class ApiListContacts(val userId: Long): CC()
|
class ApiListContacts(val userId: Long): CC()
|
||||||
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
class ApiUpdateProfile(val userId: Long, val profile: Profile): CC()
|
||||||
@@ -2861,11 +2887,7 @@ sealed class CC {
|
|||||||
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
is APIConnectPlan -> "/_connect plan $userId $connReq"
|
||||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
|
||||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||||
is ApiDeleteChat -> if (notify != null) {
|
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||||
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
|
|
||||||
} else {
|
|
||||||
"/_delete ${chatRef(type, id)}"
|
|
||||||
}
|
|
||||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||||
is ApiListContacts -> "/_contacts $userId"
|
is ApiListContacts -> "/_contacts $userId"
|
||||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||||
@@ -3084,8 +3106,6 @@ sealed class CC {
|
|||||||
null
|
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)
|
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -3095,6 +3115,8 @@ sealed class CC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onOff(b: Boolean): String = if (b) "on" else "off"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NewUser(
|
data class NewUser(
|
||||||
val profile: Profile?,
|
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
|
@Serializable
|
||||||
sealed class ConnectionPlan {
|
sealed class ConnectionPlan {
|
||||||
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
|
||||||
@@ -5953,6 +5988,7 @@ data class AppSettings(
|
|||||||
var uiDarkColorScheme: String? = null,
|
var uiDarkColorScheme: String? = null,
|
||||||
var uiCurrentThemeIds: Map<String, String>? = null,
|
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||||
var uiThemes: List<ThemeOverrides>? = null,
|
var uiThemes: List<ThemeOverrides>? = null,
|
||||||
|
var oneHandUI: Boolean? = null
|
||||||
) {
|
) {
|
||||||
fun prepareForExport(): AppSettings {
|
fun prepareForExport(): AppSettings {
|
||||||
val empty = AppSettings()
|
val empty = AppSettings()
|
||||||
@@ -5982,6 +6018,7 @@ data class AppSettings(
|
|||||||
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||||
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||||
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||||
|
if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI }
|
||||||
return empty
|
return empty
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6019,6 +6056,7 @@ data class AppSettings(
|
|||||||
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||||
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||||
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||||
|
oneHandUI?.let { def.oneHandUI.set(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -6049,6 +6087,7 @@ data class AppSettings(
|
|||||||
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||||
uiCurrentThemeIds = null,
|
uiCurrentThemeIds = null,
|
||||||
uiThemes = null,
|
uiThemes = null,
|
||||||
|
oneHandUI = false
|
||||||
)
|
)
|
||||||
|
|
||||||
val current: AppSettings
|
val current: AppSettings
|
||||||
@@ -6080,6 +6119,7 @@ data class AppSettings(
|
|||||||
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||||
uiCurrentThemeIds = def.currentThemeIds.get(),
|
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||||
uiThemes = def.themeOverrides.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.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.*
|
import androidx.compose.foundation.text.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
|
import chat.simplex.common.views.call.CallMediaType
|
||||||
|
import chat.simplex.common.views.chatlist.*
|
||||||
import androidx.compose.ui.text.*
|
import androidx.compose.ui.text.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
@@ -45,9 +49,21 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import java.io.File
|
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
|
@Composable
|
||||||
fun ChatInfoView(
|
fun ChatInfoView(
|
||||||
chatModel: ChatModel,
|
chatModel: ChatModel,
|
||||||
|
openedFromChatView: Boolean,
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
connectionStats: ConnectionStats?,
|
connectionStats: ConnectionStats?,
|
||||||
customUserProfile: Profile?,
|
customUserProfile: Profile?,
|
||||||
@@ -68,6 +84,7 @@ fun ChatInfoView(
|
|||||||
val chatRh = chat.remoteHostId
|
val chatRh = chat.remoteHostId
|
||||||
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||||
ChatInfoLayout(
|
ChatInfoLayout(
|
||||||
|
openedFromChatView = openedFromChatView,
|
||||||
chat,
|
chat,
|
||||||
contact,
|
contact,
|
||||||
currentUser,
|
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) {
|
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||||
val chatInfo = chat.chatInfo
|
val chatInfo = chat.chatInfo
|
||||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
if (chatInfo is ChatInfo.Direct) {
|
||||||
title = generalGetString(MR.strings.delete_contact_question),
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
title = generalGetString(MR.strings.delete_contact_question),
|
||||||
buttons = {
|
buttons = {
|
||||||
Column {
|
Column {
|
||||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) {
|
// Delete contact
|
||||||
// Delete and notify contact
|
|
||||||
SectionItemView({
|
SectionItemView({
|
||||||
AlertManager.shared.hideAlert()
|
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({
|
SectionItemView({
|
||||||
AlertManager.shared.hideAlert()
|
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
|
val chatInfo = chat.chatInfo
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val chatRh = chat.remoteHostId
|
val chatRh = chat.remoteHostId
|
||||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
|
||||||
if (r) {
|
if (ct != null) {
|
||||||
chatModel.removeChat(chatRh, chatInfo.id)
|
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) {
|
if (chatModel.chatId.value == chatInfo.id) {
|
||||||
chatModel.chatId.value = null
|
chatModel.chatId.value = null
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
@@ -280,6 +372,7 @@ fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoLayout(
|
fun ChatInfoLayout(
|
||||||
|
openedFromChatView: Boolean,
|
||||||
chat: Chat,
|
chat: Chat,
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
@@ -300,6 +393,7 @@ fun ChatInfoLayout(
|
|||||||
syncContactConnection: () -> Unit,
|
syncContactConnection: () -> Unit,
|
||||||
syncContactConnectionForce: () -> Unit,
|
syncContactConnectionForce: () -> Unit,
|
||||||
verifyClicked: () -> Unit,
|
verifyClicked: () -> Unit,
|
||||||
|
close: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val cStats = connStats.value
|
val cStats = connStats.value
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@@ -319,7 +413,31 @@ fun ChatInfoLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged)
|
||||||
|
|
||||||
SectionSpacer()
|
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) {
|
if (customUserProfile != null) {
|
||||||
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
SectionView(generalGetString(MR.strings.incognito).uppercase()) {
|
||||||
SectionItemViewSpaceBetween {
|
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
|
@Composable
|
||||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||||
Row(
|
Row(
|
||||||
@@ -823,6 +1090,7 @@ fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
|
|||||||
fun PreviewChatInfoLayout() {
|
fun PreviewChatInfoLayout() {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
ChatInfoLayout(
|
ChatInfoLayout(
|
||||||
|
openedFromChatView = false,
|
||||||
chat = Chat(
|
chat = Chat(
|
||||||
remoteHostId = null,
|
remoteHostId = null,
|
||||||
chatInfo = ChatInfo.Direct.sampleData,
|
chatInfo = ChatInfo.Direct.sampleData,
|
||||||
@@ -847,6 +1115,7 @@ fun PreviewChatInfoLayout() {
|
|||||||
syncContactConnection = {},
|
syncContactConnection = {},
|
||||||
syncContactConnectionForce = {},
|
syncContactConnectionForce = {},
|
||||||
verifyClicked = {},
|
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
|
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||||
preloadedCode = code
|
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) {
|
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||||
KeyChangeEffect(chat.id) {
|
KeyChangeEffect(chat.id) {
|
||||||
@@ -307,18 +307,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
onComplete.invoke()
|
onComplete.invoke()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startCall = out@{ media ->
|
startCall = out@{ media -> startChatCall(chat, 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
endCall = {
|
endCall = {
|
||||||
val call = chatModel.activeCall.value
|
val call = chatModel.activeCall.value
|
||||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
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
|
@Composable
|
||||||
fun ChatLayout(
|
fun ChatLayout(
|
||||||
chat: Chat,
|
chat: Chat,
|
||||||
|
|||||||
+36
-17
@@ -246,10 +246,10 @@ fun GroupMemberInfoLayout(
|
|||||||
verifyClicked: () -> Unit,
|
verifyClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val cStats = connStats.value
|
val cStats = connStats.value
|
||||||
fun knownDirectChat(contactId: Long): Chat? {
|
fun knownDirectChat(contactId: Long): Pair<Chat, Contact>? {
|
||||||
val chat = getContactChat(contactId)
|
val chat = getContactChat(contactId)
|
||||||
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||||
chat
|
chat to chat.chatInfo.contact
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -309,17 +309,37 @@ fun GroupMemberInfoLayout(
|
|||||||
|
|
||||||
val contactId = member.memberContactId
|
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) {
|
if (member.memberActive) {
|
||||||
SectionView {
|
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) {
|
if (connectionCode != null) {
|
||||||
VerifyCodeButton(member.verified, verifyClicked)
|
VerifyCodeButton(member.verified, verifyClicked)
|
||||||
}
|
}
|
||||||
@@ -513,12 +533,11 @@ fun RemoveMemberButton(onClick: () -> Unit) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenChatButton(onClick: () -> Unit) {
|
fun OpenChatButton(onClick: () -> Unit) {
|
||||||
SettingsActionItem(
|
InfoViewActionButton(
|
||||||
painterResource(MR.images.ic_chat),
|
icon = painterResource(MR.images.ic_chat_bubble_filled),
|
||||||
stringResource(MR.strings.button_send_direct_message),
|
title = generalGetString(MR.strings.info_view_message_button),
|
||||||
click = onClick,
|
disabled = false,
|
||||||
textColor = MaterialTheme.colors.primary,
|
onClick = onClick
|
||||||
iconColor = MaterialTheme.colors.primary,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+81
-7
@@ -1,6 +1,7 @@
|
|||||||
package chat.simplex.common.views.chatlist
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -32,7 +33,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, oneHandUI: State<Boolean>) {
|
||||||
val showMenu = remember { mutableStateOf(false) }
|
val showMenu = remember { mutableStateOf(false) }
|
||||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||||
chat.chatStats.unreadCount > 0 || 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 showChatPreviews = chatModel.showChatPreviews.value
|
||||||
val inProgress = remember { mutableStateOf(false) }
|
val inProgress = remember { mutableStateOf(false) }
|
||||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(inProgress.value) {
|
LaunchedEffect(inProgress.value) {
|
||||||
progressByTimeout = if (inProgress.value) {
|
progressByTimeout = if (inProgress.value) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
@@ -75,6 +77,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
disabled,
|
disabled,
|
||||||
selectedChat,
|
selectedChat,
|
||||||
nextChatSelected,
|
nextChatSelected,
|
||||||
|
oneHandUI
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ChatInfo.Group ->
|
is ChatInfo.Group ->
|
||||||
@@ -94,6 +97,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
disabled,
|
disabled,
|
||||||
selectedChat,
|
selectedChat,
|
||||||
nextChatSelected,
|
nextChatSelected,
|
||||||
|
oneHandUI
|
||||||
)
|
)
|
||||||
is ChatInfo.Local -> {
|
is ChatInfo.Local -> {
|
||||||
ChatListNavLinkLayout(
|
ChatListNavLinkLayout(
|
||||||
@@ -112,6 +116,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
disabled,
|
disabled,
|
||||||
selectedChat,
|
selectedChat,
|
||||||
nextChatSelected,
|
nextChatSelected,
|
||||||
|
oneHandUI
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ChatInfo.ContactRequest ->
|
is ChatInfo.ContactRequest ->
|
||||||
@@ -131,6 +136,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
disabled,
|
disabled,
|
||||||
selectedChat,
|
selectedChat,
|
||||||
nextChatSelected,
|
nextChatSelected,
|
||||||
|
oneHandUI
|
||||||
)
|
)
|
||||||
is ChatInfo.ContactConnection ->
|
is ChatInfo.ContactConnection ->
|
||||||
ChatListNavLinkLayout(
|
ChatListNavLinkLayout(
|
||||||
@@ -151,6 +157,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
disabled,
|
disabled,
|
||||||
selectedChat,
|
selectedChat,
|
||||||
nextChatSelected,
|
nextChatSelected,
|
||||||
|
oneHandUI
|
||||||
)
|
)
|
||||||
is ChatInfo.InvalidJSON ->
|
is ChatInfo.InvalidJSON ->
|
||||||
ChatListNavLinkLayout(
|
ChatListNavLinkLayout(
|
||||||
@@ -167,12 +174,13 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
|||||||
disabled,
|
disabled,
|
||||||
selectedChat,
|
selectedChat,
|
||||||
nextChatSelected,
|
nextChatSelected,
|
||||||
|
oneHandUI
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ErrorChatListItem() {
|
fun ErrorChatListItem() {
|
||||||
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) {
|
||||||
Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic)
|
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),
|
stringResource(MR.strings.delete_contact_menu_action),
|
||||||
painterResource(MR.images.ic_delete),
|
painterResource(MR.images.ic_delete),
|
||||||
onClick = {
|
onClick = {
|
||||||
deleteContactDialog(chat, chatModel)
|
deleteContactConversationDialog(chat, chatModel)
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
},
|
},
|
||||||
color = Color.Red
|
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
|
@Composable
|
||||||
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||||
ItemAction(
|
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(
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
title = generalGetString(MR.strings.accept_connection_request__question),
|
title = generalGetString(MR.strings.accept_connection_request__question),
|
||||||
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
|
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({
|
SectionItemView({
|
||||||
AlertManager.shared.hideAlert()
|
AlertManager.shared.hideAlert()
|
||||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
|
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)
|
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
}
|
}
|
||||||
SectionItemView({
|
SectionItemView({
|
||||||
AlertManager.shared.hideAlert()
|
AlertManager.shared.hideAlert()
|
||||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
|
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)
|
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,
|
disabled: Boolean,
|
||||||
selectedChat: State<Boolean>,
|
selectedChat: State<Boolean>,
|
||||||
nextChatSelected: State<Boolean>,
|
nextChatSelected: State<Boolean>,
|
||||||
|
oneHandUI: State<Boolean>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@Preview/*(
|
@Preview/*(
|
||||||
@@ -887,7 +958,8 @@ fun PreviewChatListNavLinkDirect() {
|
|||||||
showMenu = remember { mutableStateOf(false) },
|
showMenu = remember { mutableStateOf(false) },
|
||||||
disabled = false,
|
disabled = false,
|
||||||
selectedChat = remember { mutableStateOf(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) },
|
showMenu = remember { mutableStateOf(false) },
|
||||||
disabled = false,
|
disabled = false,
|
||||||
selectedChat = remember { mutableStateOf(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) },
|
showMenu = remember { mutableStateOf(false) },
|
||||||
disabled = false,
|
disabled = false,
|
||||||
selectedChat = remember { mutableStateOf(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.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.layout.BoxScope.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -12,6 +13,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.focus.*
|
import androidx.compose.ui.focus.*
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
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.views.usersettings.SettingsView
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.call.Call
|
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.chat.item.CIFileViewScope
|
||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -41,7 +42,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -54,6 +54,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||||
else newChatSheetState.value = AnimatedViewState.GONE
|
else newChatSheetState.value = AnimatedViewState.GONE
|
||||||
}
|
}
|
||||||
|
val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (shouldShowWhatsNew(chatModel)) {
|
if (shouldShowWhatsNew(chatModel)) {
|
||||||
delay(1000L)
|
delay(1000L)
|
||||||
@@ -76,7 +78,29 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val (userPickerState, scaffoldState ) = settingsState
|
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,
|
scaffoldState = scaffoldState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
tryOrShowError("Settings", error = { ErrorSettingsView() }) {
|
||||||
@@ -89,13 +113,23 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) {
|
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(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!stopped) {
|
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(
|
elevation = FloatingActionButtonDefaults.elevation(
|
||||||
defaultElevation = 0.dp,
|
defaultElevation = 0.dp,
|
||||||
pressedElevation = 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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (!chatModel.desktopNoUserNoRemote) {
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
ChatList(chatModel, searchText = searchText)
|
ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI)
|
||||||
}
|
}
|
||||||
if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||||
Text(stringResource(
|
Text(stringResource(
|
||||||
@@ -142,7 +181,12 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
}
|
}
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
tryOrShowError("UserPicker", error = {}) {
|
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() }
|
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||||
userPickerState.value = AnimatedViewState.GONE
|
userPickerState.value = AnimatedViewState.GONE
|
||||||
}
|
}
|
||||||
@@ -341,6 +385,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||||
Text(
|
Text(
|
||||||
@@ -390,8 +435,14 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) {
|
private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>, oneHandUI: SharedPreference<Boolean>) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
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() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
var focused by remember { mutableStateOf(false) }
|
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)
|
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 {
|
} else {
|
||||||
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
val padding = if (appPlatform.isDesktop) 0.dp else 7.dp
|
||||||
if (chatModel.chats.size > 0) {
|
if (chatModel.chats.size > 0) {
|
||||||
ToggleFilterEnabledButton()
|
ToggleFilterEnabledButton()
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(padding))
|
Spacer(Modifier.width(padding))
|
||||||
}
|
}
|
||||||
@@ -480,9 +531,35 @@ private fun ErrorSettingsView() {
|
|||||||
|
|
||||||
private var lazyListState = 0 to 0
|
private var lazyListState = 0 to 0
|
||||||
|
|
||||||
|
enum class ScrollDirection {
|
||||||
|
Up, Down, Idle
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@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)
|
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) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
}
|
}
|
||||||
@@ -503,7 +580,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
|||||||
Modifier
|
Modifier
|
||||||
.offset {
|
.offset {
|
||||||
val y = if (searchText.value.text.isEmpty()) {
|
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 {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -511,7 +590,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
|||||||
}
|
}
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
) {
|
) {
|
||||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, oneHandUI)
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,11 +598,17 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
|
|||||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
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()) {
|
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)
|
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)
|
chat.id == chatModel.chatId.value || filtered(chat)
|
||||||
} else {
|
} else {
|
||||||
(viewNameContains(cInfo, s) ||
|
(viewNameContains(cInfo, s) ||
|
||||||
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
cInfo.contact.profile.displayName.lowercase().contains(s) ||
|
||||||
cInfo.contact.fullName.lowercase().contains(s))
|
cInfo.contact.fullName.lowercase().contains(s))
|
||||||
}
|
}
|
||||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
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 =
|
private fun filtered(chat: Chat): Boolean =
|
||||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||||
chat.chatStats.unreadChat ||
|
chat.chatStats.unreadChat ||
|
||||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||||
|
|
||||||
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
|
||||||
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
cInfo.chatViewName.lowercase().contains(s.lowercase())
|
||||||
|
|||||||
+1
-1
@@ -201,7 +201,7 @@ fun ChatPreviewView(
|
|||||||
} else {
|
} else {
|
||||||
when (cInfo) {
|
when (cInfo) {
|
||||||
is ChatInfo.Direct ->
|
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)
|
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
|
||||||
} else if (!cInfo.ready && cInfo.contact.activeConn != null) {
|
} else if (!cInfo.ready && cInfo.contact.activeConn != null) {
|
||||||
if (cInfo.contact.nextSendGrpInv) {
|
if (cInfo.contact.nextSendGrpInv) {
|
||||||
|
|||||||
+7
-4
@@ -39,11 +39,13 @@ import kotlin.math.roundToInt
|
|||||||
fun UserPicker(
|
fun UserPicker(
|
||||||
chatModel: ChatModel,
|
chatModel: ChatModel,
|
||||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||||
|
containerModifier: Modifier = Modifier,
|
||||||
|
contentAlignment: Alignment = Alignment.TopStart,
|
||||||
showSettings: Boolean = true,
|
showSettings: Boolean = true,
|
||||||
showCancel: Boolean = false,
|
showCancel: Boolean = false,
|
||||||
cancelClicked: () -> Unit = {},
|
cancelClicked: () -> Unit = {},
|
||||||
useFromDesktopClicked: () -> Unit = {},
|
useFromDesktopClicked: () -> Unit = {},
|
||||||
settingsClicked: () -> Unit = {},
|
settingsClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||||
@@ -149,16 +151,17 @@ fun UserPicker(
|
|||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
alpha = animatedFloat.value
|
alpha = animatedFloat.value
|
||||||
translationY = (animatedFloat.value - 1) * xOffset
|
translationY = (animatedFloat.value - 1) * xOffset
|
||||||
}
|
},
|
||||||
|
contentAlignment = contentAlignment
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
containerModifier
|
||||||
.widthIn(min = 260.dp)
|
.widthIn(min = 260.dp)
|
||||||
.width(IntrinsicSize.Min)
|
.width(IntrinsicSize.Min)
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
|
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
|
||||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
|
.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
|
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
||||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
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
|
@Composable
|
||||||
fun DefaultTopAppBar(
|
fun DefaultTopAppBar(
|
||||||
navigationButton: @Composable RowScope.() -> Unit,
|
navigationButton: (@Composable RowScope.() -> Unit)? = null,
|
||||||
title: (@Composable () -> Unit)?,
|
title: (@Composable () -> Unit)?,
|
||||||
onTitleClick: (() -> Unit)? = null,
|
onTitleClick: (() -> Unit)? = null,
|
||||||
showSearch: Boolean,
|
showSearch: Boolean,
|
||||||
|
|||||||
+68
-2
@@ -1,5 +1,8 @@
|
|||||||
package chat.simplex.common.views.newchat
|
package chat.simplex.common.views.newchat
|
||||||
|
|
||||||
|
import SectionItemView
|
||||||
|
import SectionView
|
||||||
|
import TextIconSpaced
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
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.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
|
import chat.simplex.common.model.RemoteHostInfo
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.contacts.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -32,10 +40,69 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
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
|
@Composable
|
||||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||||
// TODO close new chat if remote host changes in model
|
// TODO close new chat if remote host changes in model
|
||||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
|
||||||
NewChatSheetLayout(
|
NewChatSheetLayout(
|
||||||
newChatSheetState,
|
newChatSheetState,
|
||||||
stopped,
|
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) }
|
ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) }
|
||||||
},
|
},
|
||||||
createGroup = {
|
createGroup = {
|
||||||
closeNewChatSheet(false)
|
|
||||||
ModalManager.center.closeModals()
|
ModalManager.center.closeModals()
|
||||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) }
|
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) }
|
||||||
},
|
},
|
||||||
|
|||||||
+4
@@ -43,6 +43,10 @@ fun DeveloperView(
|
|||||||
SectionSpacer()
|
SectionSpacer()
|
||||||
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
|
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)
|
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) {
|
if (appPlatform.isDesktop) {
|
||||||
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
|
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
|
|||||||
@@ -345,6 +345,7 @@
|
|||||||
<string name="welcome">Welcome!</string>
|
<string name="welcome">Welcome!</string>
|
||||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||||
<string name="your_chats">Chats</string>
|
<string name="your_chats">Chats</string>
|
||||||
|
<string name="toolbar_settings">Settings</string>
|
||||||
<string name="contact_connection_pending">connecting…</string>
|
<string name="contact_connection_pending">connecting…</string>
|
||||||
<string name="member_contact_send_direct_message">send direct message</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>
|
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||||
@@ -436,10 +437,28 @@
|
|||||||
<string name="notifications">Notifications</string>
|
<string name="notifications">Notifications</string>
|
||||||
|
|
||||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
<!-- 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_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_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_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="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="text_field_set_contact_placeholder">Set contact name…</string>
|
||||||
<string name="icon_descr_server_status_connected">Connected</string>
|
<string name="icon_descr_server_status_connected">Connected</string>
|
||||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||||
@@ -1247,6 +1266,7 @@
|
|||||||
<string name="database_downgrade">Database downgrade</string>
|
<string name="database_downgrade">Database downgrade</string>
|
||||||
<string name="incompatible_database_version">Incompatible database version</string>
|
<string name="incompatible_database_version">Incompatible database version</string>
|
||||||
<string name="confirm_database_upgrades">Confirm database upgrades</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="terminal_always_visible">Show console in new window</string>
|
||||||
<string name="chat_list_always_visible">Show chat list 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>
|
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||||
@@ -2194,4 +2214,13 @@
|
|||||||
<string name="download_errors">Download errors</string>
|
<string name="download_errors">Download errors</string>
|
||||||
<string name="server_address">Server address</string>
|
<string name="server_address">Server address</string>
|
||||||
<string name="open_server_settings_button">Open server settings</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>
|
</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,
|
disabled: Boolean,
|
||||||
selectedChat: State<Boolean>,
|
selectedChat: State<Boolean>,
|
||||||
nextChatSelected: State<Boolean>,
|
nextChatSelected: State<Boolean>,
|
||||||
|
oneHandUI: State<Boolean>?,
|
||||||
) {
|
) {
|
||||||
var modifier = Modifier.fillMaxWidth()
|
var modifier = Modifier.fillMaxWidth()
|
||||||
if (!disabled) modifier = modifier
|
if (!disabled) modifier = modifier
|
||||||
|
|||||||
Reference in New Issue
Block a user