mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-android
This commit is contained in:
+25
-2
@@ -2660,6 +2660,7 @@ data class CIFile(
|
||||
is CIFileStatus.SndComplete -> true
|
||||
is CIFileStatus.SndCancelled -> true
|
||||
is CIFileStatus.SndError -> true
|
||||
is CIFileStatus.SndWarning -> true
|
||||
is CIFileStatus.RcvInvitation -> false
|
||||
is CIFileStatus.RcvAccepted -> false
|
||||
is CIFileStatus.RcvTransfer -> false
|
||||
@@ -2667,6 +2668,7 @@ data class CIFile(
|
||||
is CIFileStatus.RcvCancelled -> false
|
||||
is CIFileStatus.RcvComplete -> true
|
||||
is CIFileStatus.RcvError -> false
|
||||
is CIFileStatus.RcvWarning -> false
|
||||
is CIFileStatus.Invalid -> false
|
||||
}
|
||||
|
||||
@@ -2682,6 +2684,7 @@ data class CIFile(
|
||||
}
|
||||
is CIFileStatus.SndCancelled -> null
|
||||
is CIFileStatus.SndError -> null
|
||||
is CIFileStatus.SndWarning -> sndCancelAction
|
||||
is CIFileStatus.RcvInvitation -> null
|
||||
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
||||
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
||||
@@ -2689,6 +2692,7 @@ data class CIFile(
|
||||
is CIFileStatus.RcvCancelled -> null
|
||||
is CIFileStatus.RcvComplete -> null
|
||||
is CIFileStatus.RcvError -> null
|
||||
is CIFileStatus.RcvWarning -> rcvCancelAction
|
||||
is CIFileStatus.Invalid -> null
|
||||
}
|
||||
|
||||
@@ -2862,14 +2866,16 @@ sealed class CIFileStatus {
|
||||
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
|
||||
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
|
||||
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
|
||||
@Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus()
|
||||
@Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): CIFileStatus()
|
||||
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
|
||||
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
|
||||
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
|
||||
@Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus()
|
||||
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
|
||||
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
||||
@Serializable @SerialName("rcvError") class RcvError(val rcvFileError: FileError): CIFileStatus()
|
||||
@Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus()
|
||||
@Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus()
|
||||
|
||||
val sent: Boolean get() = when (this) {
|
||||
@@ -2878,6 +2884,7 @@ sealed class CIFileStatus {
|
||||
is SndComplete -> true
|
||||
is SndCancelled -> true
|
||||
is SndError -> true
|
||||
is SndWarning -> true
|
||||
is RcvInvitation -> false
|
||||
is RcvAccepted -> false
|
||||
is RcvTransfer -> false
|
||||
@@ -2885,10 +2892,26 @@ sealed class CIFileStatus {
|
||||
is RcvComplete -> false
|
||||
is RcvCancelled -> false
|
||||
is RcvError -> false
|
||||
is RcvWarning -> false
|
||||
is Invalid -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class FileError {
|
||||
@Serializable @SerialName("auth") class Auth: FileError()
|
||||
@Serializable @SerialName("noFile") class NoFile: FileError()
|
||||
@Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError()
|
||||
@Serializable @SerialName("other") class Other(val fileError: String): FileError()
|
||||
|
||||
val errorInfo: String get() = when (this) {
|
||||
is FileError.Auth -> generalGetString(MR.strings.file_error_auth)
|
||||
is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file)
|
||||
is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo)
|
||||
is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
@Serializable(with = MsgContentSerializer::class)
|
||||
sealed class MsgContent {
|
||||
|
||||
+65
-16
@@ -1,9 +1,18 @@
|
||||
package chat.simplex.common.model
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||
@@ -12,7 +21,6 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.group.toggleShowMemberMessages
|
||||
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -20,6 +28,7 @@ import com.charleskorn.kaml.Yaml
|
||||
import com.charleskorn.kaml.YamlConfiguration
|
||||
import chat.simplex.res.MR
|
||||
import com.russhwolf.settings.Settings
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -2026,6 +2035,11 @@ object ChatController {
|
||||
cleanupFile(r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.RcvFileWarning -> {
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.SndFileStart ->
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||
is CR.SndFileComplete -> {
|
||||
@@ -2052,6 +2066,11 @@ object ChatController {
|
||||
cleanupFile(r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.SndFileWarning -> {
|
||||
if (r.chatItem_ != null) {
|
||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||
}
|
||||
}
|
||||
is CR.CallInvitation -> {
|
||||
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
|
||||
}
|
||||
@@ -2184,15 +2203,43 @@ object ChatController {
|
||||
val sess = chatModel.remoteCtrlSession.value
|
||||
if (sess != null) {
|
||||
chatModel.remoteCtrlSession.value = null
|
||||
ModalManager.fullscreen.closeModals()
|
||||
fun showAlert(chatError: ChatError) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||
if (chatError is ChatError.ChatErrorRemoteCtrl) {
|
||||
chatError.remoteCtrlError.localizedString
|
||||
} else {
|
||||
generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string)
|
||||
}
|
||||
)
|
||||
when {
|
||||
r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed
|
||||
&& r.rcStopReason.chatError is ChatError.ChatErrorAgent
|
||||
&& r.rcStopReason.chatError.agentError is AgentErrorType.RCP
|
||||
&& r.rcStopReason.chatError.agentError.rcpErr is RCErrorType.IDENTITY ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||
text = generalGetString(MR.strings.remote_ctrl_connection_stopped_identity_desc)
|
||||
)
|
||||
else ->
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||
text = if (chatError is ChatError.ChatErrorRemoteCtrl) {
|
||||
chatError.remoteCtrlError.localizedString
|
||||
} else {
|
||||
generalGetString(MR.strings.remote_ctrl_connection_stopped_desc)
|
||||
},
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionItemView({
|
||||
clipboard.setText(AnnotatedString(json.encodeToString(r.rcStopReason)))
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.copy_error), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
when (r.rcStopReason) {
|
||||
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
|
||||
@@ -4322,6 +4369,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@Serializable @SerialName("rcvFileWarning") class RcvFileWarning(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
// sending file events
|
||||
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||
@@ -4334,7 +4382,8 @@ sealed class CR {
|
||||
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR()
|
||||
@Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
|
||||
@Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
|
||||
// call events
|
||||
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
|
||||
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
|
||||
@@ -4493,6 +4542,7 @@ sealed class CR {
|
||||
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
||||
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
|
||||
is RcvFileError -> "rcvFileError"
|
||||
is RcvFileWarning -> "rcvFileWarning"
|
||||
is SndFileStart -> "sndFileStart"
|
||||
is SndFileComplete -> "sndFileComplete"
|
||||
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
||||
@@ -4502,6 +4552,7 @@ sealed class CR {
|
||||
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
|
||||
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
|
||||
is SndFileError -> "sndFileError"
|
||||
is SndFileWarning -> "sndFileWarning"
|
||||
is CallInvitations -> "callInvitations"
|
||||
is CallInvitation -> "callInvitation"
|
||||
is CallOffer -> "callOffer"
|
||||
@@ -4652,6 +4703,7 @@ sealed class CR {
|
||||
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
||||
is RcvStandaloneFileComplete -> withUser(user, targetPath)
|
||||
is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
|
||||
is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
|
||||
is SndFileCancelled -> json.encodeToString(chatItem_)
|
||||
is SndStandaloneFileCreated -> noDetails()
|
||||
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||
@@ -4663,7 +4715,8 @@ sealed class CR {
|
||||
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
||||
is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileError -> withUser(user, json.encodeToString(chatItem_))
|
||||
is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||
is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
||||
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
|
||||
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
||||
@@ -4700,7 +4753,7 @@ sealed class CR {
|
||||
(if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) +
|
||||
"\nsessionCode: $sessionCode"
|
||||
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
|
||||
is RemoteCtrlStopped -> noDetails()
|
||||
is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason"
|
||||
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
|
||||
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
|
||||
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
||||
@@ -4982,8 +5035,6 @@ sealed class ChatErrorType {
|
||||
is FileImageSize -> "fileImageSize"
|
||||
is FileNotReceived -> "fileNotReceived"
|
||||
is FileNotApproved -> "fileNotApproved"
|
||||
// is XFTPRcvFile -> "xftpRcvFile"
|
||||
// is XFTPSndFile -> "xftpSndFile"
|
||||
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
|
||||
is InlineFileProhibited -> "inlineFileProhibited"
|
||||
is InvalidQuote -> "invalidQuote"
|
||||
@@ -5061,8 +5112,6 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
|
||||
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
|
||||
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
|
||||
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
|
||||
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType()
|
||||
@Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.currentUser
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -57,6 +58,9 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
try {
|
||||
if (chatModel.ctrlInitInProgress.value) return
|
||||
chatModel.ctrlInitInProgress.value = true
|
||||
if (!appPrefs.storeDBPassphrase.get() && !appPrefs.initialRandomDBPassphrase.get()) {
|
||||
ksDatabasePassword.remove()
|
||||
}
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
|
||||
|
||||
+2
@@ -224,6 +224,8 @@ object ThemeManager {
|
||||
s.length == 1 -> "#ff$s$s$s$s$s$s"
|
||||
s.length == 2 -> "#ff$s$s$s"
|
||||
s.length == 3 -> "#ff$s$s"
|
||||
s.length == 4 && this.alpha == 0f -> "#0000$s" // 000088ff treated as 88ff
|
||||
s.length == 4 -> "#ff00$s"
|
||||
s.length == 6 && this.alpha == 0f -> "#00$s"
|
||||
s.length == 6 -> "#ff$s"
|
||||
else -> "#$s"
|
||||
|
||||
+30
@@ -31,6 +31,7 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
sealed class CIInfoTab {
|
||||
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
|
||||
@@ -216,6 +217,27 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExpandableInfoRow(title: String, value: String) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.padding(PaddingValues(horizontal = DEFAULT_PADDING))
|
||||
.clickable { expanded.value = !expanded.value },
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(title, color = MaterialTheme.colors.onBackground)
|
||||
if (expanded.value) {
|
||||
Text(value, color = MaterialTheme.colors.secondary)
|
||||
} else {
|
||||
Text(value, color = MaterialTheme.colors.secondary, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Details() {
|
||||
AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message))
|
||||
@@ -244,6 +266,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
||||
if (devTools) {
|
||||
InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString())
|
||||
InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
|
||||
ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus))
|
||||
if (ci.file != null) {
|
||||
ExpandableInfoRow(stringResource(MR.strings.info_row_file_status), jsonShort.encodeToString(ci.file.fileStatus))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,6 +557,10 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem
|
||||
if (devTools) {
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId))
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt))
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_message_status), jsonShort.encodeToString(ci.meta.itemStatus)))
|
||||
if (ci.file != null) {
|
||||
shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus)))
|
||||
}
|
||||
}
|
||||
val qi = ci.quotedItem
|
||||
if (qi != null) {
|
||||
|
||||
+8
-11
@@ -962,17 +962,8 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||
) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false }
|
||||
val directions = setOf(DismissDirection.EndToStart)
|
||||
val swipeableModifier = SwipeToDismissModifier(
|
||||
state = dismissState,
|
||||
directions = directions,
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd))
|
||||
val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart))
|
||||
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
|
||||
LaunchedEffect(Unit) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
scope.launch {
|
||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) {
|
||||
if (composeState.value.editing) {
|
||||
@@ -983,7 +974,13 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
val swipeableModifier = SwipeToDismissModifier(
|
||||
state = dismissState,
|
||||
directions = setOf(DismissDirection.EndToStart),
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val provider = {
|
||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||
scope.launch {
|
||||
|
||||
+22
@@ -87,6 +87,26 @@ fun CIFileView(
|
||||
)
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.SndError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.SndWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
file.forwardingAllowed() -> {
|
||||
withLongRunningApi(slow = 600_000) {
|
||||
var filePath = getLoadedFilePath(file)
|
||||
@@ -157,6 +177,7 @@ fun CIFileView(
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid(file))
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary)
|
||||
@@ -174,6 +195,7 @@ fun CIFileView(
|
||||
is CIFileStatus.RcvComplete -> fileIcon()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||
is CIFileStatus.RcvWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||
is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark))
|
||||
}
|
||||
} else {
|
||||
|
||||
+29
-7
@@ -70,14 +70,16 @@ fun CIImageView(
|
||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
|
||||
is CIFileStatus.RcvTransfer -> progressIndicator()
|
||||
is CIFileStatus.RcvComplete -> {}
|
||||
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,8 +203,8 @@ fun CIImageView(
|
||||
} else {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
when {
|
||||
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted ->
|
||||
if (fileSizeValid()) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
@@ -211,7 +213,7 @@ fun CIImageView(
|
||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvAccepted ->
|
||||
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -225,9 +227,29 @@ fun CIImageView(
|
||||
)
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
|
||||
CIFileStatus.RcvComplete -> {} // ?
|
||||
CIFileStatus.RcvCancelled -> {} // TODO
|
||||
file.fileStatus is CIFileStatus.RcvError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.SndError ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.SndWarning ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
file.fileStatus is CIFileStatus.RcvTransfer -> {} // ?
|
||||
file.fileStatus is CIFileStatus.RcvComplete -> {} // ?
|
||||
file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-7
@@ -107,7 +107,7 @@ fun CIVideoView(
|
||||
}
|
||||
}
|
||||
}
|
||||
loadingIndicator(file)
|
||||
fileStatusIcon(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,11 +339,13 @@ private fun progressIndicator() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fileIcon(icon: Painter, stringId: StringResource) {
|
||||
private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) {
|
||||
var modifier = Modifier.fillMaxSize()
|
||||
modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier }
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(stringId),
|
||||
Modifier.fillMaxSize(),
|
||||
modifier,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
@@ -364,7 +366,7 @@ private fun progressCircle(progress: Long, total: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun loadingIndicator(file: CIFile?) {
|
||||
private fun fileStatusIcon(file: CIFile?) {
|
||||
if (file != null) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -387,7 +389,28 @@ private fun loadingIndicator(file: CIFile?) {
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.SndError ->
|
||||
fileIcon(
|
||||
painterResource(MR.images.ic_close),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.SndWarning ->
|
||||
fileIcon(
|
||||
painterResource(MR.images.ic_warning_filled),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video)
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
@@ -397,10 +420,31 @@ private fun loadingIndicator(file: CIFile?) {
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvComplete -> {}
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||
is CIFileStatus.RcvError ->
|
||||
fileIcon(
|
||||
painterResource(MR.images.ic_close),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.RcvWarning ->
|
||||
fileIcon(
|
||||
painterResource(MR.images.ic_warning_filled),
|
||||
MR.strings.icon_descr_file,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+138
-29
@@ -252,6 +252,81 @@ private fun PlayPauseButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlayablePlayPauseButton(
|
||||
audioPlaying: Boolean,
|
||||
sent: Boolean,
|
||||
hasText: Boolean,
|
||||
progress: State<Int>,
|
||||
duration: State<Int>,
|
||||
strokeWidth: Float,
|
||||
strokeColor: Color,
|
||||
error: Boolean,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
) {
|
||||
val angle = 360f * (progress.value.toDouble() / duration.value).toFloat()
|
||||
if (hasText) {
|
||||
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
|
||||
Icon(
|
||||
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
|
||||
contentDescription = null,
|
||||
Modifier.size(36.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceMsgLoadingProgressIndicator() {
|
||||
Box(
|
||||
Modifier
|
||||
.size(56.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileStatusIcon(
|
||||
sent: Boolean,
|
||||
icon: ImageResource,
|
||||
longClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
color = if (sent) sentColor else receivedColor,
|
||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 56.dp, minHeight = 56.dp)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = longClick
|
||||
)
|
||||
.onRightClick { longClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
contentDescription = null,
|
||||
Modifier.size(36.dp),
|
||||
tint = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceMsgIndicator(
|
||||
file: CIFile?,
|
||||
@@ -268,39 +343,73 @@ private fun VoiceMsgIndicator(
|
||||
) {
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = MaterialTheme.colors.primary
|
||||
if (file != null && file.loaded && progress != null && duration != null) {
|
||||
val angle = 360f * (progress.value.toDouble() / duration.value).toFloat()
|
||||
if (hasText) {
|
||||
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
|
||||
Icon(
|
||||
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
|
||||
contentDescription = null,
|
||||
Modifier.size(36.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
when {
|
||||
file?.fileStatus is CIFileStatus.SndStored ->
|
||||
if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) {
|
||||
PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick)
|
||||
} else {
|
||||
VoiceMsgLoadingProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
||||
}
|
||||
} else {
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
file?.fileStatus is CIFileStatus.SndTransfer ->
|
||||
VoiceMsgLoadingProgressIndicator()
|
||||
file != null && file.fileStatus is CIFileStatus.SndError ->
|
||||
FileStatusIcon(
|
||||
sent,
|
||||
MR.images.ic_close,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
file != null && file.fileStatus is CIFileStatus.SndWarning ->
|
||||
FileStatusIcon(
|
||||
sent,
|
||||
MR.images.ic_warning_filled,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.sndFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
file?.fileStatus is CIFileStatus.RcvInvitation ->
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
||||
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
|| file?.fileStatus is CIFileStatus.RcvAccepted
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(56.dp)
|
||||
.clip(RoundedCornerShape(4.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
} else if (file?.fileStatus is CIFileStatus.RcvAborted) {
|
||||
file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
VoiceMsgLoadingProgressIndicator()
|
||||
file?.fileStatus is CIFileStatus.RcvAborted ->
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem)
|
||||
} else {
|
||||
file != null && file.fileStatus is CIFileStatus.RcvError ->
|
||||
FileStatusIcon(
|
||||
sent,
|
||||
MR.images.ic_close,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
file != null && file.fileStatus is CIFileStatus.RcvWarning ->
|
||||
FileStatusIcon(
|
||||
sent,
|
||||
MR.images.ic_warning_filled,
|
||||
longClick,
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.temporary_file_error),
|
||||
file.fileStatus.rcvFileError.errorInfo
|
||||
)
|
||||
}
|
||||
)
|
||||
file != null && file.loaded && progress != null && duration != null ->
|
||||
PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick)
|
||||
else ->
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-7
@@ -189,13 +189,6 @@ fun FramedItemView(
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Box(Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(
|
||||
when {
|
||||
transparentBackground -> Color.Transparent
|
||||
sent -> MaterialTheme.colors.background
|
||||
else -> MaterialTheme.colors.background
|
||||
}
|
||||
)
|
||||
.background(
|
||||
when {
|
||||
transparentBackground -> Color.Transparent
|
||||
|
||||
+24
-19
@@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -40,9 +41,8 @@ import kotlin.math.log2
|
||||
@Composable
|
||||
fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||
val useKeychain = remember { mutableStateOf(appPrefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(appPrefs.initialRandomDBPassphrase.get()) }
|
||||
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
|
||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||
@@ -54,7 +54,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||
) {
|
||||
DatabaseEncryptionLayout(
|
||||
useKeychain,
|
||||
prefs,
|
||||
m.chatDbEncrypted.value,
|
||||
currentKey,
|
||||
newKey,
|
||||
@@ -65,7 +64,16 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||
migration,
|
||||
onConfirmEncrypt = {
|
||||
withLongRunningApi {
|
||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration)
|
||||
encryptDatabase(
|
||||
currentKey = currentKey,
|
||||
newKey = newKey,
|
||||
confirmNewKey = confirmNewKey,
|
||||
initialRandomDBPassphrase = initialRandomDBPassphrase,
|
||||
useKeychain = useKeychain,
|
||||
storedKey = storedKey,
|
||||
progressIndicator = progressIndicator,
|
||||
migration = migration
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -89,7 +97,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||
@Composable
|
||||
fun DatabaseEncryptionLayout(
|
||||
useKeychain: MutableState<Boolean>,
|
||||
prefs: AppPreferences,
|
||||
chatDbEncrypted: Boolean?,
|
||||
currentKey: MutableState<String>,
|
||||
newKey: MutableState<String>,
|
||||
@@ -119,14 +126,14 @@ fun DatabaseEncryptionLayout(
|
||||
enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration
|
||||
) { checked ->
|
||||
if (checked) {
|
||||
setUseKeychain(true, useKeychain, prefs, migration)
|
||||
setUseKeychain(true, useKeychain, migration)
|
||||
} else if (storedKey.value && !migration) {
|
||||
// Don't show in migration process since it will remove the key after successful encryption
|
||||
removePassphraseAlert {
|
||||
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false)
|
||||
removePassphraseFromKeyChain(useKeychain, storedKey, false)
|
||||
}
|
||||
} else {
|
||||
setUseKeychain(false, useKeychain, prefs, migration)
|
||||
setUseKeychain(false, useKeychain, migration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,17 +279,17 @@ fun resetFormAfterEncryption(
|
||||
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
||||
}
|
||||
|
||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences, migration: Boolean) {
|
||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, migration: Boolean) {
|
||||
useKeychain.value = value
|
||||
// Postpone it when migrating to the end of encryption process
|
||||
if (!migration) {
|
||||
prefs.storeDBPassphrase.set(value)
|
||||
appPrefs.storeDBPassphrase.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, prefs: AppPreferences, storedKey: MutableState<Boolean>, migration: Boolean) {
|
||||
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, storedKey: MutableState<Boolean>, migration: Boolean) {
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs, migration)
|
||||
setUseKeychain(false, useKeychain, migration)
|
||||
storedKey.value = false
|
||||
}
|
||||
|
||||
@@ -415,15 +422,14 @@ suspend fun encryptDatabase(
|
||||
migration: Boolean,
|
||||
): Boolean {
|
||||
val m = ChatModel
|
||||
val prefs = ChatController.appPrefs
|
||||
progressIndicator.value = true
|
||||
return try {
|
||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
||||
appPrefs.encryptionStartedAt.set(Clock.System.now())
|
||||
if (!m.chatDbChanged.value) {
|
||||
m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||
}
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
appPrefs.encryptionStartedAt.set(null)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
when {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
@@ -451,8 +457,8 @@ suspend fun encryptDatabase(
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.ksDatabasePassword.set(new)
|
||||
} else if (migration) {
|
||||
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true)
|
||||
} else {
|
||||
removePassphraseFromKeyChain(useKeychain, storedKey, migration)
|
||||
}
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||
@@ -523,7 +529,6 @@ fun PreviewDatabaseEncryptionLayout() {
|
||||
SimpleXTheme {
|
||||
DatabaseEncryptionLayout(
|
||||
useKeychain = remember { mutableStateOf(true) },
|
||||
prefs = AppPreferences(),
|
||||
chatDbEncrypted = true,
|
||||
currentKey = remember { mutableStateOf("") },
|
||||
newKey = remember { mutableStateOf("") },
|
||||
|
||||
+3
@@ -448,6 +448,9 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = n
|
||||
progressIndicator?.value = true
|
||||
stopChatAsync(m)
|
||||
platform.androidChatStopped()
|
||||
// close chat view for desktop
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
onStop?.invoke()
|
||||
} catch (e: Error) {
|
||||
m.chatRunning.value = true
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ fun SwipeToDismissModifier(
|
||||
return Modifier.swipeable(
|
||||
state = state,
|
||||
anchors = anchors,
|
||||
thresholds = { _, _ -> FractionalThreshold(0.5f) },
|
||||
thresholds = { _, _ -> FractionalThreshold(0.99f) },
|
||||
orientation = Orientation.Horizontal,
|
||||
reverseDirection = isRtl,
|
||||
).offset { IntOffset(state.offset.value.roundToInt(), 0) }
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
withBGApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
||||
if (groupInfo != null) {
|
||||
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
|
||||
chatModel.updateGroup(rhId = rhId, groupInfo)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
|
||||
+10
-2
@@ -31,7 +31,6 @@ import kotlinx.coroutines.delay
|
||||
fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||
@@ -58,7 +57,16 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
prefs.storeDBPassphrase.set(false)
|
||||
|
||||
val newKeyValue = newKey.value
|
||||
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false)
|
||||
val success = encryptDatabase(
|
||||
currentKey = currentKey,
|
||||
newKey = newKey,
|
||||
confirmNewKey = confirmNewKey,
|
||||
initialRandomDBPassphrase = mutableStateOf(true),
|
||||
useKeychain = mutableStateOf(false),
|
||||
storedKey = mutableStateOf(true),
|
||||
progressIndicator = progressIndicator,
|
||||
migration = false
|
||||
)
|
||||
if (success) {
|
||||
startChat(newKeyValue)
|
||||
nextStep()
|
||||
|
||||
+19
@@ -15,6 +15,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
@@ -166,6 +167,24 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
SectionView {
|
||||
DisconnectButton(onClick = ::disconnectDesktop)
|
||||
}
|
||||
|
||||
ProgressIndicator()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+29
-2
@@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.ClickableText
|
||||
@@ -58,6 +59,8 @@ fun NetworkAndServersView() {
|
||||
smpProxyFallback = smpProxyFallback,
|
||||
proxyPort = proxyPort,
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
if (enable) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.network_enable_socks),
|
||||
@@ -65,7 +68,19 @@ fun NetworkAndServersView() {
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get())
|
||||
var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get())
|
||||
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
|
||||
}
|
||||
if (conf.tcpTimeout == def.tcpTimeout) {
|
||||
conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout)
|
||||
}
|
||||
if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) {
|
||||
conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb)
|
||||
}
|
||||
if (conf.rcvConcurrency == def.rcvConcurrency) {
|
||||
conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency)
|
||||
}
|
||||
chatModel.controller.apiSetNetworkConfig(conf)
|
||||
chatModel.controller.setNetCfg(conf)
|
||||
networkUseSocksProxy.value = true
|
||||
@@ -80,7 +95,19 @@ fun NetworkAndServersView() {
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val conf = NetCfg.defaults
|
||||
var conf = controller.getNetCfg().copy(socksProxy = null)
|
||||
if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout)
|
||||
}
|
||||
if (conf.tcpTimeout == proxyDef.tcpTimeout) {
|
||||
conf = conf.copy(tcpTimeout = def.tcpTimeout)
|
||||
}
|
||||
if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) {
|
||||
conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb)
|
||||
}
|
||||
if (conf.rcvConcurrency == proxyDef.rcvConcurrency) {
|
||||
conf = conf.copy(rcvConcurrency = def.rcvConcurrency)
|
||||
}
|
||||
chatModel.controller.apiSetNetworkConfig(conf)
|
||||
chatModel.controller.setNetCfg(conf)
|
||||
networkUseSocksProxy.value = false
|
||||
|
||||
@@ -271,6 +271,11 @@
|
||||
<string name="srv_error_host">Server address is incompatible with network settings.</string>
|
||||
<string name="srv_error_version">Server version is incompatible with network settings.</string>
|
||||
|
||||
<!-- CIFileStatus errors -->
|
||||
<string name="file_error_auth">Wrong key or unknown file chunk address - most likely file is deleted.</string>
|
||||
<string name="file_error_no_file">File not found - most likely file was deleted or cancelled.</string>
|
||||
<string name="file_error_relay">File server error: %1$s</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Reply</string>
|
||||
<string name="share_verb">Share</string>
|
||||
@@ -408,6 +413,8 @@
|
||||
<string name="error_saving_file">Error saving file</string>
|
||||
<string name="loading_remote_file_title">Loading the file </string>
|
||||
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
||||
<string name="file_error">File error</string>
|
||||
<string name="temporary_file_error">Temporary file error</string>
|
||||
|
||||
<!-- Voice messages -->
|
||||
<string name="voice_message">Voice message</string>
|
||||
@@ -1403,6 +1410,8 @@
|
||||
<string name="info_row_database_id">Database ID</string>
|
||||
<string name="info_row_debug_delivery">Debug delivery</string>
|
||||
<string name="info_row_updated_at">Record updated at</string>
|
||||
<string name="info_row_message_status">Message status</string>
|
||||
<string name="info_row_file_status">File status</string>
|
||||
<string name="info_row_sent_at">Sent at</string>
|
||||
<string name="info_row_created_at">Created at</string>
|
||||
<string name="info_row_received_at">Received at</string>
|
||||
@@ -1411,6 +1420,8 @@
|
||||
<string name="info_row_disappears_at">Disappears at</string>
|
||||
<string name="share_text_database_id">Database ID: %d</string>
|
||||
<string name="share_text_updated_at">Record updated at: %s</string>
|
||||
<string name="share_text_message_status">Message status: %s</string>
|
||||
<string name="share_text_file_status">File status: %s</string>
|
||||
<string name="share_text_sent_at">Sent at: %s</string>
|
||||
<string name="share_text_created_at">Created at: %s</string>
|
||||
<string name="share_text_received_at">Received at: %s</string>
|
||||
@@ -1910,6 +1921,9 @@
|
||||
<string name="remote_ctrl_was_disconnected_title">Connection stopped</string>
|
||||
<string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string>
|
||||
<string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string>
|
||||
<string name="remote_ctrl_connection_stopped_desc">Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers.</string>
|
||||
<string name="remote_ctrl_connection_stopped_identity_desc">This link was used with another mobile device, please create a new link on the desktop.</string>
|
||||
<string name="copy_error">Copy error</string>
|
||||
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
||||
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
||||
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
|
||||
|
||||
+7
-2
@@ -14,8 +14,10 @@ import com.russhwolf.settings.*
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.desc.desc
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Composable
|
||||
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
|
||||
@@ -59,8 +61,11 @@ private val settingsThemesProps =
|
||||
Properties()
|
||||
.also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } }
|
||||
|
||||
actual val settings: Settings = PropertiesSettings(settingsProps) { withApi { settingsFile.writer().use { settingsProps.store(it, "") } } }
|
||||
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { withApi { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } }
|
||||
|
||||
private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } }
|
||||
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } }
|
||||
|
||||
actual fun windowOrientation(): WindowOrientation =
|
||||
if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {
|
||||
|
||||
Reference in New Issue
Block a user