android, desktop: moving to single thread in api calls (#3670)

* android, desktop: moving to single thread in api calls

* more places

* more changes

* seconds

* long running api into init function

* changes

* developer options

* progress indicator

* string

* rename

* progressIndicator for stop chat
This commit is contained in:
Stanislav Dmitrenko 2024-01-12 01:50:25 +07:00 committed by GitHub
parent bc8a6f4833
commit dad9716915
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 331 additions and 261 deletions

View File

@ -26,7 +26,6 @@ import kotlinx.coroutines.sync.withLock
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
const val TAG = "SIMPLEX" const val TAG = "SIMPLEX"
@ -72,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d(TAG, "onStateChanged: $event") Log.d(TAG, "onStateChanged: $event")
withApi { withBGApi {
when (event) { when (event) {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
isAppOnForeground = true isAppOnForeground = true

View File

@ -104,7 +104,7 @@ class SimplexService: Service() {
if (wakeLock != null || isStartingService) return if (wakeLock != null || isStartingService) return
val self = this val self = this
isStartingService = true isStartingService = true
withApi { withBGApi {
val chatController = ChatController val chatController = ChatController
waitDbMigrationEnds(chatController) waitDbMigrationEnds(chatController)
try { try {
@ -114,7 +114,7 @@ class SimplexService: Service() {
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus") Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
showPassphraseNotification(chatDbStatus) showPassphraseNotification(chatDbStatus)
safeStopService() safeStopService()
return@withApi return@withBGApi
} }
saveServiceState(self, ServiceState.STARTED) saveServiceState(self, ServiceState.STARTED)
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {

View File

@ -13,8 +13,7 @@ import chat.simplex.common.model.ChatItem
import chat.simplex.common.model.MsgContent import chat.simplex.common.model.MsgContent
import chat.simplex.common.platform.FileChooserLauncher import chat.simplex.common.platform.FileChooserLauncher
import chat.simplex.common.platform.saveImage import chat.simplex.common.platform.saveImage
import chat.simplex.common.views.helpers.SharedContent import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.withApi
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@ -37,7 +36,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
writePermissionState.launchPermissionRequest() writePermissionState.launchPermissionRequest()
} }
} }
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withBGApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
else -> {} else -> {}
} }
showMenu.value = false showMenu.value = false

View File

@ -102,7 +102,7 @@ fun AppearanceScope.AppearanceLayout(
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) { LangSelector(state) {
state.value = it state.value = it
withApi { withBGApi {
delay(200) delay(200)
val activity = context as? Activity val activity = context as? Activity
if (activity != null) { if (activity != null) {

View File

@ -5,8 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.work.WorkManager import androidx.work.WorkManager
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR import chat.simplex.res.MR
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@ -15,7 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
actual fun SettingsSectionApp( actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {

View File

@ -110,6 +110,7 @@ class AppPreferences {
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false) val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false) val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
@ -279,6 +280,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors" private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
@ -464,7 +466,7 @@ object ChatController {
suspend fun sendCmd(rhId: Long?, cmd: CC): CR { suspend fun sendCmd(rhId: Long?, cmd: CC): CR {
val ctrl = ctrl ?: throw Exception("Controller is not initialized") val ctrl = ctrl ?: throw Exception("Controller is not initialized")
return withContext(Dispatchers.IO) { //return withContext(Dispatchers.IO) {
val c = cmd.cmdString val c = cmd.cmdString
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
Log.d(TAG, "sendCmd: ${cmd.cmdType}") Log.d(TAG, "sendCmd: ${cmd.cmdType}")
@ -475,8 +477,8 @@ object ChatController {
Log.d(TAG, "sendCmd response json $json") Log.d(TAG, "sendCmd response json $json")
} }
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp)) chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
r.resp return r.resp
} //}
} }
private fun recvMsg(ctrl: ChatCtrl): APIResponse? { private fun recvMsg(ctrl: ChatCtrl): APIResponse? {
@ -1665,7 +1667,8 @@ object ChatController {
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
withApi { receiveFile(rhId, r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) } withBGApi { receiveFile(rhId, r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs
.privacyEncryptLocalFiles.get(), auto = true) }
} }
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem) ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
@ -1863,12 +1866,10 @@ object ChatController {
} }
withCall(r, r.contact) { _ -> withCall(r, r.contact) { _ ->
chatModel.callCommand.add(WCallCommand.End) chatModel.callCommand.add(WCallCommand.End)
withApi {
chatModel.activeCall.value = null chatModel.activeCall.value = null
chatModel.showCallView.value = false chatModel.showCallView.value = false
} }
} }
}
is CR.ContactSwitch -> is CR.ContactSwitch ->
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
is CR.GroupMemberSwitch -> is CR.GroupMemberSwitch ->
@ -1977,7 +1978,7 @@ object ChatController {
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart) chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart)
} }
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.showInternalErrors.get() -> { r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.INTERNAL && appPrefs.developerTools.get() && appPrefs.showInternalErrors.get() -> {
chatModel.processedInternalError.newError(r.chatError.agentError, false) chatModel.processedInternalError.newError(r.chatError.agentError, false)
} }
} }
@ -2004,10 +2005,9 @@ object ChatController {
} }
} }
fun switchToLocalSession() { suspend fun switchToLocalSession() {
val m = chatModel val m = chatModel
m.remoteCtrlSession.value = null m.remoteCtrlSession.value = null
withBGApi {
val users = listUsers(null) val users = listUsers(null)
m.users.clear() m.users.clear()
m.users.addAll(users) m.users.addAll(users)
@ -2019,7 +2019,6 @@ object ChatController {
chatModel.networkStatuses.putAll(ss) chatModel.networkStatuses.putAll(ss)
} }
} }
}
private fun activeUser(rhId: Long?, user: UserLike): Boolean = private fun activeUser(rhId: Long?, user: UserLike): Boolean =
rhId == chatModel.remoteHostId() && user.userId == chatModel.currentUser.value?.userId rhId == chatModel.remoteHostId() && user.userId == chatModel.currentUser.value?.userId

View File

@ -42,7 +42,7 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() { fun initChatControllerAndRunMigrations() {
withBGApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert) initChatController(startChat = ::showStartChatAfterRestartAlert)
} else { } else {
@ -57,7 +57,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (chatModel.ctrlInitInProgress.value) return if (chatModel.ctrlInitInProgress.value) return
chatModel.ctrlInitInProgress.value = true chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching { val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String) json.decodeFromString<DBMigrationResult>(migrated[0] as String)

View File

@ -50,7 +50,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
chatModel.addTerminalItem(TerminalItem.resp(null, resp)) chatModel.addTerminalItem(TerminalItem.resp(null, resp))
composeState.value = ComposeState(useLinkPreviews = false) composeState.value = ComposeState(useLinkPreviews = false)
} else { } else {
withApi { withBGApi {
// show "in progress" // show "in progress"
// TODO show active remote host in chat console? // TODO show active remote host in chat console?
chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s)) chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s))

View File

@ -175,8 +175,8 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
} }
fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) {
withApi { withBGApi {
val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withApi val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withBGApi
controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
chatModel.chatRunning.value = false chatModel.chatRunning.value = false
controller.startChat(user) controller.startChat(user)
@ -186,11 +186,11 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) {
} }
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withBGApi {
val rhId = chatModel.remoteHostId() val rhId = chatModel.remoteHostId()
val user = chatModel.controller.apiCreateActiveUser( val user = chatModel.controller.apiCreateActiveUser(
rhId, Profile(displayName.trim(), "", null) rhId, Profile(displayName.trim(), "", null)
) ?: return@withApi ) ?: return@withBGApi
chatModel.currentUser.value = user chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) { if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user) chatModel.controller.startChat(user)
@ -206,10 +206,10 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: ()
} }
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) { fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withBGApi {
chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser( chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser(
null, Profile(displayName.trim(), "", null) null, Profile(displayName.trim(), "", null)
) ?: return@withApi ) ?: return@withBGApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) { if (chatModel.users.isEmpty()) {
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) { onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) {

View File

@ -2,7 +2,7 @@ package chat.simplex.common.views.call
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withApi import chat.simplex.common.views.helpers.withBGApi
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@ -28,13 +28,13 @@ class CallManager(val chatModel: ChatModel) {
if (call == null) { if (call == null) {
justAcceptIncomingCall(invitation = invitation) justAcceptIncomingCall(invitation = invitation)
} else { } else {
withApi { withBGApi {
chatModel.switchingCall.value = true chatModel.switchingCall.value = true
try { try {
endCall(call = call) endCall(call = call)
justAcceptIncomingCall(invitation = invitation) justAcceptIncomingCall(invitation = invitation)
} finally { } finally {
withApi { chatModel.switchingCall.value = false } chatModel.switchingCall.value = false
} }
} }
} }
@ -90,7 +90,7 @@ class CallManager(val chatModel: ChatModel) {
activeCallInvitation.value = null activeCallInvitation.value = null
ntfManager.cancelCallNotification() ntfManager.cancelCallNotification()
} }
withApi { withBGApi {
if (!controller.apiRejectCall(invitation.remoteHostId, invitation.contact)) { if (!controller.apiRejectCall(invitation.remoteHostId, invitation.contact)) {
Log.e(TAG, "apiRejectCall error") Log.e(TAG, "apiRejectCall error")
} }

View File

@ -69,11 +69,9 @@ fun ChatInfoView(
currentUser, currentUser,
sendReceipts = sendReceipts, sendReceipts = sendReceipts,
setSendReceipts = { sendRcpts -> setSendReceipts = { sendRcpts ->
withApi {
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
updateChatSettings(chat, chatSettings, chatModel) updateChatSettings(chat, chatSettings, chatModel)
sendReceipts.value = sendRcpts sendReceipts.value = sendRcpts
}
}, },
connStats = connStats, connStats = connStats,
contactNetworkStatus.value, contactNetworkStatus.value,
@ -96,7 +94,7 @@ fun ChatInfoView(
clearChat = { clearChatDialog(chat, chatModel, close) }, clearChat = { clearChatDialog(chat, chatModel, close) },
switchContactAddress = { switchContactAddress = {
showSwitchAddressAlert(switchAddress = { showSwitchAddressAlert(switchAddress = {
withApi { withBGApi {
val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId)
connStats.value = cStats connStats.value = cStats
if (cStats != null) { if (cStats != null) {
@ -108,7 +106,7 @@ fun ChatInfoView(
}, },
abortSwitchContactAddress = { abortSwitchContactAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = { showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi { withBGApi {
val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId)
connStats.value = cStats connStats.value = cStats
if (cStats != null) { if (cStats != null) {
@ -118,7 +116,7 @@ fun ChatInfoView(
}) })
}, },
syncContactConnection = { syncContactConnection = {
withApi { withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
connStats.value = cStats connStats.value = cStats
if (cStats != null) { if (cStats != null) {
@ -129,7 +127,7 @@ fun ChatInfoView(
}, },
syncContactConnectionForce = { syncContactConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = {
withApi { withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
connStats.value = cStats connStats.value = cStats
if (cStats != null) { if (cStats != null) {
@ -208,18 +206,14 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
// Delete and notify contact // Delete and notify contact
SectionItemView({ SectionItemView({
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
withApi {
deleteContact(chat, chatModel, close, notify = true) deleteContact(chat, chatModel, close, notify = true)
}
}) { }) {
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
} }
// Delete // Delete
SectionItemView({ SectionItemView({
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
withApi {
deleteContact(chat, chatModel, close, notify = false) deleteContact(chat, chatModel, close, notify = false)
}
}) { }) {
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
} }
@ -227,9 +221,7 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
// Delete // Delete
SectionItemView({ SectionItemView({
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
withApi {
deleteContact(chat, chatModel, close) deleteContact(chat, chatModel, close)
}
}) { }) {
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
} }
@ -247,7 +239,7 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) { fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
val chatInfo = chat.chatInfo val chatInfo = chat.chatInfo
withApi { withBGApi {
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify) val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
if (r) { if (r) {
@ -269,7 +261,7 @@ fun clearChatDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = nul
text = generalGetString(MR.strings.clear_chat_warning), text = generalGetString(MR.strings.clear_chat_warning),
confirmText = generalGetString(MR.strings.clear_verb), confirmText = generalGetString(MR.strings.clear_verb),
onConfirm = { onConfirm = {
withApi { withBGApi {
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
val updatedChatInfo = chatModel.controller.apiClearChat(chatRh, chatInfo.chatType, chatInfo.apiId) val updatedChatInfo = chatModel.controller.apiClearChat(chatRh, chatInfo.chatType, chatInfo.apiId)
if (updatedChatInfo != null) { if (updatedChatInfo != null) {
@ -676,7 +668,7 @@ fun ShareAddressButton(onClick: () -> Unit) {
) )
} }
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withApi { private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
chatModel.updateContact(chatRh, it) chatModel.updateContact(chatRh, it)

View File

@ -164,7 +164,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
return@ChatLayout return@ChatLayout
} }
hideKeyboard(view) hideKeyboard(view)
withApi { withBGApi {
// The idea is to preload information before showing a modal because large groups can take time to load all members // The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null var preloadedCode: String? = null
@ -205,7 +205,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view) hideKeyboard(view)
withApi { withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val stats = r?.second val stats = r?.second
val (_, code) = if (member.memberActive) { val (_, code) = if (member.memberActive) {
@ -228,7 +228,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
val firstId = chatModel.chatItems.firstOrNull()?.id val firstId = chatModel.chatItems.firstOrNull()?.id
if (c != null && firstId != null) { if (c != null && firstId != null) {
withApi { withBGApi {
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}") Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
apiLoadPrevMessages(c, chatModel, firstId, searchText.value) apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}") Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
@ -236,7 +236,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
deleteMessage = { itemId, mode -> deleteMessage = { itemId, mode ->
withApi { withBGApi {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId } val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
@ -291,13 +291,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
receiveFile = { fileId, encrypted -> receiveFile = { fileId, encrypted ->
withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) } withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
}, },
cancelFile = { fileId -> cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(chatRh, user, fileId) } withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
}, },
joinGroup = { groupId, onComplete -> joinGroup = { groupId, onComplete ->
withApi { withBGApi {
chatModel.controller.apiJoinGroup(chatRh, groupId) chatModel.controller.apiJoinGroup(chatRh, groupId)
onComplete.invoke() onComplete.invoke()
} }
@ -314,11 +314,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
endCall = { endCall = {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) } if (call != null) withBGApi { chatModel.callManager.endCall(call) }
}, },
acceptCall = { contact -> acceptCall = { contact ->
hideKeyboard(view) hideKeyboard(view)
withApi { withBGApi {
val invitation = chatModel.callInvitations.remove(contact.id) val invitation = chatModel.callInvitations.remove(contact.id)
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
if (invitation == null) { if (invitation == null) {
@ -329,17 +329,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
acceptFeature = { contact, feature, param -> acceptFeature = { contact, feature, param ->
withApi { withBGApi {
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
} }
}, },
openDirectChat = { contactId -> openDirectChat = { contactId ->
withApi { withBGApi {
openDirectChat(chatRh, contactId, chatModel) openDirectChat(chatRh, contactId, chatModel)
} }
}, },
updateContactStats = { contact -> updateContactStats = { contact ->
withApi { withBGApi {
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
if (r != null) { if (r != null) {
val contactStats = r.first val contactStats = r.first
@ -349,7 +349,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
updateMemberStats = { groupInfo, member -> updateMemberStats = { groupInfo, member ->
withApi { withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
if (r != null) { if (r != null) {
val memStats = r.second val memStats = r.second
@ -360,7 +360,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
syncContactConnection = { contact -> syncContactConnection = { contact ->
withApi { withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
if (cStats != null) { if (cStats != null) {
chatModel.updateContactConnectionStats(chatRh, contact, cStats) chatModel.updateContactConnectionStats(chatRh, contact, cStats)
@ -368,7 +368,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
syncMemberConnection = { groupInfo, member -> syncMemberConnection = { groupInfo, member ->
withApi { withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) { if (r != null) {
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
@ -382,7 +382,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
chatModel.groupMembers.find { it.id == memberId } chatModel.groupMembers.find { it.id == memberId }
}, },
setReaction = { cInfo, cItem, add, reaction -> setReaction = { cInfo, cItem, add, reaction ->
withApi { withBGApi {
val updatedCI = chatModel.controller.apiChatItemReaction( val updatedCI = chatModel.controller.apiChatItemReaction(
rh = chatRh, rh = chatRh,
type = cInfo.chatType, type = cInfo.chatType,
@ -397,7 +397,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
}, },
showItemDetails = { cInfo, cItem -> showItemDetails = { cInfo, cItem ->
withApi { withBGApi {
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) { if (ciInfo != null) {
if (chat.chatInfo is ChatInfo.Group) { if (chat.chatInfo is ChatInfo.Group) {
@ -416,7 +416,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
addMembers = { groupInfo -> addMembers = { groupInfo ->
hideKeyboard(view) hideKeyboard(view)
withApi { withBGApi {
setGroupMembers(chatRh, groupInfo, chatModel) setGroupMembers(chatRh, groupInfo, chatModel)
ModalManager.end.closeModals() ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close -> ModalManager.end.showModalCloseable(true) { close ->
@ -426,7 +426,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
openGroupLink = { groupInfo -> openGroupLink = { groupInfo ->
hideKeyboard(view) hideKeyboard(view)
withApi { withBGApi {
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
ModalManager.end.closeModals() ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { ModalManager.end.showModalCloseable(true) {
@ -451,7 +451,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
if (searchText.value == value) return@ChatLayout if (searchText.value == value) return@ChatLayout
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
withApi { withBGApi {
apiFindMessages(c, chatModel, value) apiFindMessages(c, chatModel, value)
searchText.value = value searchText.value = value
} }

View File

@ -267,7 +267,7 @@ fun ComposeView(
fun loadLinkPreview(url: String, wait: Long? = null) { fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) { if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
withApi { withBGApi {
if (wait != null) delay(wait) if (wait != null) delay(wait)
val lp = getLinkPreview(url) val lp = getLinkPreview(url)
if (lp != null && pendingLinkUrl.value == url) { if (lp != null && pendingLinkUrl.value == url) {
@ -575,7 +575,7 @@ fun ComposeView(
fun allowVoiceToContact() { fun allowVoiceToContact() {
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
withApi { withBGApi {
chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Voice) chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Voice)
} }
} }

View File

@ -35,7 +35,7 @@ fun ContactPreferencesView(
var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) } var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
fun savePrefs(afterSave: () -> Unit = {}) { fun savePrefs(afterSave: () -> Unit = {}) {
withApi { withBGApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
if (toContact != null) { if (toContact != null) {

View File

@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
}, },
inviteMembers = { inviteMembers = {
allowModifyMembers = false allowModifyMembers = false
withApi { withBGApi {
for (contactId in selectedContacts) { for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
if (member != null) { if (member != null) {
@ -68,7 +68,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
}, },
clearSelection = { selectedContacts.clear() }, clearSelection = { selectedContacts.clear() },
addContact = { contactId -> if (contactId !in selectedContacts) selectedContacts.add(contactId) }, addContact = { contactId -> if (contactId !in selectedContacts) selectedContacts.add(contactId) },
removeContact = { contactId -> selectedContacts.removeIf { it == contactId } }, removeContact = { contactId -> selectedContacts.removeAll { it == contactId } },
close = close, close = close,
) )
KeyChangeEffect(chatModel.chatId.value) { KeyChangeEffect(chatModel.chatId.value) {

View File

@ -56,11 +56,9 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi
currentUser, currentUser,
sendReceipts = sendReceipts, sendReceipts = sendReceipts,
setSendReceipts = { sendRcpts -> setSendReceipts = { sendRcpts ->
withApi {
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
updateChatSettings(chat, chatSettings, chatModel) updateChatSettings(chat, chatSettings, chatModel)
sendReceipts.value = sendRcpts sendReceipts.value = sendRcpts
}
}, },
members = chatModel.groupMembers members = chatModel.groupMembers
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
@ -68,7 +66,7 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi
developerTools, developerTools,
groupLink, groupLink,
addMembers = { addMembers = {
withApi { withBGApi {
setGroupMembers(rhId, groupInfo, chatModel) setGroupMembers(rhId, groupInfo, chatModel)
ModalManager.end.showModalCloseable(true) { close -> ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(rhId, groupInfo, false, chatModel, close) AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
@ -76,7 +74,7 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi
} }
}, },
showMemberInfo = { member -> showMemberInfo = { member ->
withApi { withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId) val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
val stats = r?.second val stats = r?.second
val (_, code) = if (member.memberActive) { val (_, code) = if (member.memberActive) {
@ -131,7 +129,7 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
text = generalGetString(alertTextKey), text = generalGetString(alertTextKey),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = { onConfirm = {
withApi { withBGApi {
val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId)
if (r) { if (r) {
chatModel.removeChat(chat.remoteHostId, chatInfo.id) chatModel.removeChat(chat.remoteHostId, chatInfo.id)
@ -154,7 +152,7 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(MR.strings.leave_group_button), confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = { onConfirm = {
withApi { withBGApi {
chatModel.controller.leaveGroup(rhId, groupInfo.groupId) chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
close?.invoke() close?.invoke()
} }
@ -169,7 +167,7 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone), text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(MR.strings.remove_member_confirmation), confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = { onConfirm = {
withApi { withBGApi {
val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId)
if (updatedMember != null) { if (updatedMember != null) {
chatModel.upsertGroupMember(rhId, groupInfo, updatedMember) chatModel.upsertGroupMember(rhId, groupInfo, updatedMember)

View File

@ -38,7 +38,7 @@ fun GroupLinkView(
var creatingLink by rememberSaveable { mutableStateOf(false) } var creatingLink by rememberSaveable { mutableStateOf(false) }
fun createLink() { fun createLink() {
creatingLink = true creatingLink = true
withApi { withBGApi {
val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId) val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId)
if (link != null) { if (link != null) {
groupLink = link.first groupLink = link.first
@ -78,7 +78,7 @@ fun GroupLinkView(
text = generalGetString(MR.strings.all_group_members_will_remain_connected), text = generalGetString(MR.strings.all_group_members_will_remain_connected),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = { onConfirm = {
withApi { withBGApi {
val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId) val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId)
if (r) { if (r) {
groupLink = null groupLink = null

View File

@ -66,7 +66,7 @@ fun GroupMemberInfoView(
connectionCode, connectionCode,
getContactChat = { chatModel.getContactChat(it) }, getContactChat = { chatModel.getContactChat(it) },
openDirectChat = { openDirectChat = {
withApi { withBGApi {
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
if (c != null) { if (c != null) {
if (chatModel.getContactChat(it) == null) { if (chatModel.getContactChat(it) == null) {
@ -81,7 +81,7 @@ fun GroupMemberInfoView(
} }
}, },
createMemberContact = { createMemberContact = {
withApi { withBGApi {
progressIndicator = true progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) { if (memberContact != null) {
@ -107,7 +107,7 @@ fun GroupMemberInfoView(
updateMemberRoleDialog(it, member, onDismiss = { updateMemberRoleDialog(it, member, onDismiss = {
newRole.value = prevValue newRole.value = prevValue
}) { }) {
withApi { withBGApi {
kotlin.runCatching { kotlin.runCatching {
val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it) val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it)
chatModel.upsertGroupMember(rhId, groupInfo, mem) chatModel.upsertGroupMember(rhId, groupInfo, mem)
@ -119,7 +119,7 @@ fun GroupMemberInfoView(
}, },
switchMemberAddress = { switchMemberAddress = {
showSwitchAddressAlert(switchAddress = { showSwitchAddressAlert(switchAddress = {
withApi { withBGApi {
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
@ -131,7 +131,7 @@ fun GroupMemberInfoView(
}, },
abortSwitchMemberAddress = { abortSwitchMemberAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = { showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi { withBGApi {
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
@ -142,7 +142,7 @@ fun GroupMemberInfoView(
}) })
}, },
syncMemberConnection = { syncMemberConnection = {
withApi { withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
@ -153,7 +153,7 @@ fun GroupMemberInfoView(
}, },
syncMemberConnectionForce = { syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = {
withApi { withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
@ -204,7 +204,7 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone), text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(MR.strings.remove_member_confirmation), confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = { onConfirm = {
withApi { withBGApi {
val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId)
if (removedMember != null) { if (removedMember != null) {
chatModel.upsertGroupMember(rhId, groupInfo, removedMember) chatModel.upsertGroupMember(rhId, groupInfo, removedMember)
@ -505,7 +505,7 @@ private fun updateMemberRoleDialog(
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) { fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
try { try {
val uri = URI(connReqUri) val uri = URI(connReqUri)
withApi { withBGApi {
planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
} }
} catch (e: RuntimeException) { } catch (e: RuntimeException) {

View File

@ -32,7 +32,7 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) } var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) }
fun savePrefs(afterSave: () -> Unit = {}) { fun savePrefs(afterSave: () -> Unit = {}) {
withApi { withBGApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
if (g != null) { if (g != null) {

View File

@ -35,7 +35,7 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
close = close, close = close,
groupProfile = groupInfo.groupProfile, groupProfile = groupInfo.groupProfile,
saveProfile = { p -> saveProfile = { p ->
withApi { withBGApi {
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
if (gInfo != null) { if (gInfo != null) {
chatModel.updateGroup(rhId, gInfo) chatModel.updateGroup(rhId, gInfo)

View File

@ -36,7 +36,7 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
val welcomeText = remember { mutableStateOf(gInfo.groupProfile.description ?: "") } val welcomeText = remember { mutableStateOf(gInfo.groupProfile.description ?: "") }
fun save(afterSave: () -> Unit = {}) { fun save(afterSave: () -> Unit = {}) {
withApi { withBGApi {
var welcome: String? = welcomeText.value.trim('\n', ' ') var welcome: String? = welcomeText.value.trim('\n', ' ')
if (welcome?.length == 0) { if (welcome?.length == 0) {
welcome = null welcome = null

View File

@ -101,7 +101,7 @@ fun CIFileView(
filePath = getLoadedFilePath(file) filePath = getLoadedFilePath(file)
} }
if (filePath != null) { if (filePath != null) {
withApi { withBGApi {
saveFileLauncher.launch(file.fileName) saveFileLauncher.launch(file.fileName)
} }
} else { } else {

View File

@ -394,7 +394,7 @@ fun JoinGroupAction(
inProgress: MutableState<Boolean> inProgress: MutableState<Boolean>
) { ) {
val joinGroup: () -> Unit = { val joinGroup: () -> Unit = {
withApi { withBGApi {
inProgress.value = true inProgress.value = true
chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId) chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId)
inProgress.value = false inProgress.value = false
@ -581,7 +581,7 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
} }
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
withApi { withBGApi {
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
if (contact != null && isCurrentUser && contactRequest != null) { if (contact != null && isCurrentUser && contactRequest != null) {
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
@ -591,7 +591,7 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe
} }
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
withApi { withBGApi {
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
chatModel.removeChat(rhId, contactRequest.id) chatModel.removeChat(rhId, contactRequest.id)
} }
@ -606,7 +606,7 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti
), ),
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = { onConfirm = {
withApi { withBGApi {
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
chatModel.removeChat(rhId, connection.id) chatModel.removeChat(rhId, connection.id)
@ -625,7 +625,7 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo
text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry), text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry),
confirmText = generalGetString(MR.strings.button_delete_contact), confirmText = generalGetString(MR.strings.button_delete_contact),
onConfirm = { onConfirm = {
withApi { withBGApi {
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (r) { if (r) {
chatModel.removeChat(rhId, chatInfo.id) chatModel.removeChat(rhId, chatInfo.id)
@ -654,7 +654,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
Column { Column {
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
close?.invoke() close?.invoke()
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false)
if (ok && openChat) { if (ok && openChat) {
@ -666,7 +666,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
} }
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
close?.invoke() close?.invoke()
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true)
if (ok && openChat) { if (ok && openChat) {
@ -707,7 +707,7 @@ fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatMode
text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members), text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members),
confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { onConfirm = {
withApi { withBGApi {
inProgress?.value = true inProgress?.value = true
chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId) chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId)
inProgress?.value = false inProgress?.value = false
@ -728,7 +728,7 @@ fun cantInviteIncognitoAlert() {
} }
fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
withApi { withBGApi {
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
if (r) { if (r) {
chatModel.removeChat(rhId, groupInfo.id) chatModel.removeChat(rhId, groupInfo.id)
@ -769,7 +769,7 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
} }
else -> null else -> null
} }
withApi { withBGApi {
val res = when (newChatInfo) { val res = when (newChatInfo) {
is ChatInfo.Direct -> with(newChatInfo) { is ChatInfo.Direct -> with(newChatInfo) {
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings) chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings)

View File

@ -319,7 +319,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
if (chatModel.currentUser.value == null) { if (chatModel.currentUser.value == null) {
chatModel.appOpenUrl.value = rhId to uri chatModel.appOpenUrl.value = rhId to uri
} else { } else {
withApi { withBGApi {
planAndConnect(rhId, uri, incognito = null, close = null) planAndConnect(rhId, uri, incognito = null, close = null)
} }
} }

View File

@ -31,8 +31,8 @@ import chat.simplex.common.views.remote.*
import chat.simplex.common.views.usersettings.doWithAuth import chat.simplex.common.views.usersettings.doWithAuth
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
@ -117,15 +117,17 @@ fun UserPicker(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Controller.ctrl can be null when self-destructing activates // Controller.ctrl can be null when self-destructing activates
if (controller.ctrl != null && controller.ctrl != -1L) { if (controller.ctrl != null && controller.ctrl != -1L) {
withBGApi {
controller.reloadRemoteHosts() controller.reloadRemoteHosts()
} }
} }
}
val UsersView: @Composable ColumnScope.() -> Unit = { val UsersView: @Composable ColumnScope.() -> Unit = {
users.forEach { u -> users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) { UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) {
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) { if (!u.user.activeUser) {
scope.launch { withBGApi {
controller.showProgressIfNeeded { controller.showProgressIfNeeded {
ModalManager.closeAllModalsEverywhere() ModalManager.closeAllModalsEverywhere()
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)

View File

@ -34,7 +34,7 @@ fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTim
ChatArchiveLayout( ChatArchiveLayout(
title, title,
archiveTime, archiveTime,
saveArchive = { withApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }}, saveArchive = { withBGApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }},
deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) } deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) }
) )
} }

View File

@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
initialRandomDBPassphrase, initialRandomDBPassphrase,
progressIndicator, progressIndicator,
onConfirmEncrypt = { onConfirmEncrypt = {
withApi { withBGApi {
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator) encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
} }
} }

View File

@ -27,6 +27,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.* import kotlinx.datetime.*
import java.io.* import java.io.*
@ -88,8 +89,8 @@ fun DatabaseView(
chatItemTTL, chatItemTTL,
user, user,
m.users, m.users,
startChat = { startChat(m, chatLastStart, m.chatDbChanged) }, startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) },
stopChatAlert = { stopChatAlert(m) }, stopChatAlert = { stopChatAlert(m, progressIndicator) },
exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, deleteChatAlert = { deleteChatAlert(m, progressIndicator) },
deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) }, deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) },
@ -187,7 +188,7 @@ fun DatabaseLayout(
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
} }
} }
RunChatSetting(runChat, stopped, toggleEnabled, startChat, stopChatAlert) RunChatSetting(runChat, stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert)
} }
SectionTextFooter( SectionTextFooter(
if (stopped) { if (stopped) {
@ -239,7 +240,7 @@ fun DatabaseLayout(
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_download), painterResource(MR.images.ic_download),
stringResource(MR.strings.import_database), stringResource(MR.strings.import_database),
{ withApi { importArchiveLauncher.launch("application/zip") } }, { withBGApi { importArchiveLauncher.launch("application/zip") } },
textColor = Color.Red, textColor = Color.Red,
iconColor = Color.Red, iconColor = Color.Red,
disabled = operationsDisabled disabled = operationsDisabled
@ -366,9 +367,10 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive)
} }
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) { fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
withApi { withBGApi {
try { try {
progressIndicator?.value = true
if (chatDbChanged.value) { if (chatDbChanged.value) {
initChatController() initChatController()
chatDbChanged.value = false chatDbChanged.value = false
@ -376,12 +378,12 @@ fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged
if (m.chatDbStatus.value !is DBMigrationResult.OK) { if (m.chatDbStatus.value !is DBMigrationResult.OK) {
/** Hide current view and show [DatabaseErrorView] */ /** Hide current view and show [DatabaseErrorView] */
ModalManager.closeAllModalsEverywhere() ModalManager.closeAllModalsEverywhere()
return@withApi return@withBGApi
} }
val user = m.currentUser.value val user = m.currentUser.value
if (user == null) { if (user == null) {
ModalManager.closeAllModalsEverywhere() ModalManager.closeAllModalsEverywhere()
return@withApi return@withBGApi
} else { } else {
m.controller.startChat(user) m.controller.startChat(user)
} }
@ -392,16 +394,18 @@ fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged
} catch (e: Error) { } catch (e: Error) {
m.chatRunning.value = false m.chatRunning.value = false
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.toString()) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.toString())
} finally {
progressIndicator?.value = false
} }
} }
} }
private fun stopChatAlert(m: ChatModel) { private fun stopChatAlert(m: ChatModel, progressIndicator: MutableState<Boolean>? = null) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.stop_chat_question), title = generalGetString(MR.strings.stop_chat_question),
text = generalGetString(MR.strings.stop_chat_to_export_import_or_delete_chat_database), text = generalGetString(MR.strings.stop_chat_to_export_import_or_delete_chat_database),
confirmText = generalGetString(MR.strings.stop_chat_confirmation), confirmText = generalGetString(MR.strings.stop_chat_confirmation),
onConfirm = { authStopChat(m) }, onConfirm = { authStopChat(m, progressIndicator = progressIndicator) },
onDismiss = { m.chatRunning.value = true } onDismiss = { m.chatRunning.value = true }
) )
} }
@ -415,7 +419,7 @@ private fun exportProhibitedAlert() {
) )
} }
fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) { fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null, onStop: (() -> Unit)? = null) {
if (m.controller.appPrefs.performLA.get()) { if (m.controller.appPrefs.performLA.get()) {
authenticate( authenticate(
generalGetString(MR.strings.auth_stop_chat), generalGetString(MR.strings.auth_stop_chat),
@ -423,7 +427,7 @@ fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
completed = { laResult -> completed = { laResult ->
when (laResult) { when (laResult) {
LAResult.Success, is LAResult.Unavailable -> { LAResult.Success, is LAResult.Unavailable -> {
stopChat(m, onStop) stopChat(m, progressIndicator, onStop)
} }
is LAResult.Error -> { is LAResult.Error -> {
m.chatRunning.value = true m.chatRunning.value = true
@ -436,19 +440,22 @@ fun authStopChat(m: ChatModel, onStop: (() -> Unit)? = null) {
} }
) )
} else { } else {
stopChat(m, onStop) stopChat(m, progressIndicator, onStop)
} }
} }
private fun stopChat(m: ChatModel, onStop: (() -> Unit)? = null) { private fun stopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null, onStop: (() -> Unit)? = null) {
withApi { withBGApi {
try { try {
progressIndicator?.value = true
stopChatAsync(m) stopChatAsync(m)
platform.androidChatStopped() platform.androidChatStopped()
onStop?.invoke() onStop?.invoke()
} catch (e: Error) { } catch (e: Error) {
m.chatRunning.value = true m.chatRunning.value = true
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString()) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString())
} finally {
progressIndicator?.value = false
} }
} }
} }
@ -493,7 +500,7 @@ private fun exportArchive(
saveArchiveLauncher: FileChooserLauncher saveArchiveLauncher: FileChooserLauncher
) { ) {
progressIndicator.value = true progressIndicator.value = true
withApi { withBGApi {
try { try {
val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile) val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile)
chatArchiveFile.value = archiveFile chatArchiveFile.value = archiveFile
@ -567,7 +574,7 @@ private fun importArchive(
progressIndicator.value = true progressIndicator.value = true
val archivePath = saveArchiveFromURI(importedArchiveURI) val archivePath = saveArchiveFromURI(importedArchiveURI)
if (archivePath != null) { if (archivePath != null) {
withApi { withBGApi {
try { try {
m.controller.apiDeleteStorage() m.controller.apiDeleteStorage()
try { try {
@ -635,7 +642,7 @@ private fun deleteChatAlert(m: ChatModel, progressIndicator: MutableState<Boolea
private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) { private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
progressIndicator.value = true progressIndicator.value = true
withApi { withBGApi {
try { try {
deleteChatAsync(m) deleteChatAsync(m)
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
@ -658,7 +665,7 @@ private fun setCiTTL(
) { ) {
Log.d(TAG, "DatabaseView setChatItemTTL ${chatItemTTL.value.seconds ?: -1}") Log.d(TAG, "DatabaseView setChatItemTTL ${chatItemTTL.value.seconds ?: -1}")
progressIndicator.value = true progressIndicator.value = true
withApi { withBGApi {
try { try {
m.controller.setChatItemTTL(rhId, chatItemTTL.value) m.controller.setChatItemTTL(rhId, chatItemTTL.value)
// Update model on success // Update model on success

View File

@ -89,7 +89,7 @@ enum class MigrationConfirmation(val value: String) {
} }
fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation = fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation =
if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp if (appPrefs.developerTools.get() && appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
@Serializable @Serializable
sealed class MigrationError { sealed class MigrationError {

View File

@ -45,7 +45,7 @@ class ModalData {
} }
class ModalManager(private val placement: ModalPlacement? = null) { class ModalManager(private val placement: ModalPlacement? = null) {
private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>() private val modalViews = arrayListOf<Triple<Boolean, ModalData, (@Composable ModalData.(close: () -> Unit) -> Unit)>>()
private val modalCount = mutableStateOf(0) private val modalCount = mutableStateOf(0)
private val toRemove = mutableSetOf<Int>() private val toRemove = mutableSetOf<Int>()
private var oldViewChanging = AtomicBoolean(false) private var oldViewChanging = AtomicBoolean(false)
@ -65,8 +65,9 @@ class ModalManager(private val placement: ModalPlacement? = null) {
} }
} }
fun showCustomModal(animated: Boolean = true, modal: @Composable (close: () -> Unit) -> Unit) { fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showCustomModal") Log.d(TAG, "ModalManager.showCustomModal")
val data = ModalData()
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view // This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
@ -75,7 +76,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
// Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0) // Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0)
// to prevent unneeded animation on different situations // to prevent unneeded animation on different situations
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START) val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
modalViews.add(anim to modal) modalViews.add(Triple(anim, data, modal))
modalCount.value = modalViews.size - toRemove.size modalCount.value = modalViews.size - toRemove.size
if (placement == ModalPlacement.CENTER) { if (placement == ModalPlacement.CENTER) {
@ -117,7 +118,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
fun showInView() { fun showInView() {
// Without animation // Without animation
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
modalViews.lastOrNull()?.second?.invoke(::closeModal) modalViews.lastOrNull()?.let { it.third(it.second, ::closeModal) }
return return
} }
AnimatedContent(targetState = modalCount.value, AnimatedContent(targetState = modalCount.value,
@ -129,7 +130,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
}.using(SizeTransform(clip = false)) }.using(SizeTransform(clip = false))
} }
) { ) {
modalViews.getOrNull(it - 1)?.second?.invoke(::closeModal) modalViews.getOrNull(it - 1)?.let { it.third(it.second, ::closeModal) }
// This is needed because if we delete from modalViews immediately on request, animation will be bad // This is needed because if we delete from modalViews immediately on request, animation will be bad
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) { if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } } runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }

View File

@ -43,9 +43,8 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
title = title, title = title,
text = text, text = text,
confirmText = generalGetString(MR.strings.restart_chat_button), confirmText = generalGetString(MR.strings.restart_chat_button),
onConfirm = { onConfirm = ::restartChatOrApp
withApi { restartChatOrApp() } )
})
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = title, title = title,

View File

@ -2,6 +2,7 @@ package chat.simplex.common.views.helpers
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
@ -21,15 +22,71 @@ import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.Executors
import kotlin.math.* import kotlin.math.*
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action) fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action)
fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job = fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job =
scope.launch { withContext(Dispatchers.Main, action) } Exception().let {
scope.launch { withContext(Dispatchers.Main, block = { wrapWithLogging(action, it) }) }
}
fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job = fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
CoroutineScope(Dispatchers.Default).launch(block = action) Exception().let {
CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) })
}
fun withLongRunningApi(slow: Long = 120_000, deadlock: Long = 240_000, action: suspend CoroutineScope.() -> Unit): Job =
Exception().let {
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow, deadlock = deadlock) })
}
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 10_000, deadlock: Long = 60_000) = coroutineScope {
val start = System.currentTimeMillis()
val job = launch {
delay(deadlock)
Log.e(TAG, "Possible deadlock of the thread, not finished after ${deadlock / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_deadlock_title),
text = generalGetString(MR.strings.possible_deadlock_desc).format(deadlock / 1000, exception.stackTraceToString()),
)
}
action()
job.cancel()
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
val end = System.currentTimeMillis()
if (end - start > slow) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()),
)
}
}
}
@OptIn(InternalCoroutinesApi::class)
suspend fun interruptIfCancelled() = coroutineScope {
if (!isActive) {
Log.d(TAG, "Coroutine was cancelled and interrupted: ${Exception().stackTraceToString()}")
throw coroutineContext.job.getCancellationException()
}
}
/**
* This coroutine helper makes possible to cancel coroutine scope when a user goes back but not when the user rotates a screen
* */
@Composable
fun ModalData.CancellableOnGoneJob(key: String = rememberSaveable { UUID.randomUUID().toString() }): MutableState<Job> {
val job = remember { stateGetOrPut<Job>(key) { Job() } }
DisposableEffectOnGone {
job.value.cancel()
}
return job
}
enum class KeyboardState { enum class KeyboardState {
Opened, Closed Opened, Closed
@ -422,11 +479,15 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}
val orientation = windowOrientation() val orientation = windowOrientation()
onDispose { onDispose {
whenDispose() whenDispose()
withApi {
// It needs some delay before check orientation again because it can still be not updated to actual value
delay(300)
if (orientation == windowOrientation()) { if (orientation == windowOrientation()) {
whenGone() whenGone()
} }
} }
} }
}
} }
@Composable @Composable

View File

@ -36,7 +36,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
val rhId = rh?.remoteHostId val rhId = rh?.remoteHostId
AddGroupLayout( AddGroupLayout(
createGroup = { incognito, groupProfile -> createGroup = { incognito, groupProfile ->
withApi { withBGApi {
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
if (groupInfo != null) { if (groupInfo != null) {
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf())) chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))

View File

@ -56,7 +56,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself), title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
destructive = true, destructive = true,
@ -134,7 +134,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself), title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
destructive = true, destructive = true,
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
@ -157,7 +157,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_connection_request), title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
destructive = true, destructive = true,
@ -223,7 +223,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link), title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group) + linkText, text = generalGetString(MR.strings.you_will_join_group) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
@ -254,7 +254,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_join_request), title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
destructive = true, destructive = true,
@ -374,7 +374,7 @@ fun askCurrentOrIncognitoProfileAlert(
val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup)
} }
}) { }) {
@ -382,7 +382,7 @@ fun askCurrentOrIncognitoProfileAlert(
} }
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup)
} }
}) { }) {
@ -402,7 +402,7 @@ fun askCurrentOrIncognitoProfileAlert(
} }
fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) { fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) {
withApi { withBGApi {
val c = chatModel.getContactChat(contact.contactId) val c = chatModel.getContactChat(contact.contactId)
if (c != null) { if (c != null) {
close?.invoke() close?.invoke()
@ -439,7 +439,7 @@ fun ownGroupLinkConfirmConnect(
// Join incognito / Join with current profile // Join incognito / Join with current profile
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup)
} }
}) { }) {
@ -452,7 +452,7 @@ fun ownGroupLinkConfirmConnect(
// Use current profile // Use current profile
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup)
} }
}) { }) {
@ -461,7 +461,7 @@ fun ownGroupLinkConfirmConnect(
// Use new incognito profile // Use new incognito profile
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup)
} }
}) { }) {
@ -483,7 +483,7 @@ fun ownGroupLinkConfirmConnect(
} }
fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) { fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) {
withApi { withBGApi {
val g = chatModel.getGroupChat(groupInfo.groupId) val g = chatModel.getGroupChat(groupInfo.groupId)
if (g != null) { if (g != null) {
close?.invoke() close?.invoke()

View File

@ -192,7 +192,7 @@ fun DeleteButton(onClick: () -> Unit) {
) )
} }
private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi { private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi {
chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let {
chatModel.updateContactConnection(rhId, it) chatModel.updateContactConnection(rhId, it)
} }

View File

@ -371,7 +371,7 @@ private fun createInvitation(
) { ) {
if (connReqInvitation.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return if (connReqInvitation.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return
creatingConnReq.value = true creatingConnReq.value = true
withApi { withBGApi {
val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get())
if (r != null) { if (r != null) {
chatModel.updateContactConnection(rhId, r.second) chatModel.updateContactConnection(rhId, r.second)

View File

@ -43,7 +43,7 @@ fun CreateSimpleXAddress(m: ChatModel, rhId: Long?) {
) )
}, },
createAddress = { createAddress = {
withApi { withBGApi {
progressIndicator = true progressIndicator = true
val connReqContact = m.controller.apiCreateUserAddress(rhId) val connReqContact = m.controller.apiCreateUserAddress(rhId)
if (connReqContact != null) { if (connReqContact != null) {
@ -170,8 +170,8 @@ private fun ProgressIndicator() {
private fun prepareChatBeforeAddressCreation(rhId: Long?) { private fun prepareChatBeforeAddressCreation(rhId: Long?) {
if (chatModel.users.isNotEmpty()) return if (chatModel.users.isNotEmpty()) return
withApi { withBGApi {
val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withBGApi
chatModel.currentUser.value = user chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) { if (chatModel.users.isEmpty()) {
if (appPlatform.isDesktop) { if (appPlatform.isDesktop) {

View File

@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
confirmNewKey, confirmNewKey,
progressIndicator, progressIndicator,
onConfirmEncrypt = { onConfirmEncrypt = {
withApi { withBGApi {
if (m.chatRunning.value == true) { if (m.chatRunning.value == true) {
// Stop chat if it's started before doing anything // Stop chat if it's started before doing anything
stopChatAsync(m) stopChatAsync(m)

View File

@ -38,7 +38,9 @@ import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
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 kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
@Composable @Composable
fun ConnectMobileView() { fun ConnectMobileView() {
@ -46,8 +48,10 @@ fun ConnectMobileView() {
val remoteHosts = remember { chatModel.remoteHosts } val remoteHosts = remember { chatModel.remoteHosts }
val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
withBGApi {
controller.reloadRemoteHosts() controller.reloadRemoteHosts()
} }
}
ConnectMobileLayout( ConnectMobileLayout(
deviceName = remember { deviceName.state }, deviceName = remember { deviceName.state },
remoteHosts = remoteHosts, remoteHosts = remoteHosts,

View File

@ -92,7 +92,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
} }
fun saveCfg(cfg: NetCfg) { fun saveCfg(cfg: NetCfg) {
withApi { withBGApi {
chatModel.controller.apiSetNetworkConfig(cfg) chatModel.controller.apiSetNetworkConfig(cfg)
currentCfg.value = cfg currentCfg.value = cfg
chatModel.controller.setNetCfg(cfg) chatModel.controller.setNetCfg(cfg)

View File

@ -131,7 +131,7 @@ object AppearanceScope {
SectionItemView({ SectionItemView({
val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme)
theme.value = yaml.encodeToString<ThemeOverrides>(overrides) theme.value = yaml.encodeToString<ThemeOverrides>(overrides)
withApi { exportThemeLauncher.launch("simplex.theme")} withBGApi { exportThemeLauncher.launch("simplex.theme")}
}) { }) {
Text(generalGetString(MR.strings.export_theme), color = colors.primary) Text(generalGetString(MR.strings.export_theme), color = colors.primary)
} }
@ -144,7 +144,7 @@ object AppearanceScope {
} }
} }
// Can not limit to YAML mime type since it's unsupported by Android // Can not limit to YAML mime type since it's unsupported by Android
SectionItemView({ withApi { importThemeLauncher.launch("*/*") } }) { SectionItemView({ withBGApi { importThemeLauncher.launch("*/*") } }) {
Text(generalGetString(MR.strings.import_theme), color = colors.primary) Text(generalGetString(MR.strings.import_theme), color = colors.primary)
} }
} }

View File

@ -1,6 +1,7 @@
package chat.simplex.common.views.usersettings package chat.simplex.common.views.usersettings
import SectionBottomSpacer import SectionBottomSpacer
import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -22,7 +23,7 @@ import chat.simplex.res.MR
@Composable @Composable
fun DeveloperView( fun DeveloperView(
m: ChatModel, m: ChatModel,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
@ -30,12 +31,20 @@ fun DeveloperView(
AppBarTitle(stringResource(MR.strings.settings_developer_tools)) AppBarTitle(stringResource(MR.strings.settings_developer_tools))
val developerTools = m.controller.appPrefs.developerTools val developerTools = m.controller.appPrefs.developerTools
val devTools = remember { developerTools.state } val devTools = remember { developerTools.state }
SectionView() { SectionView {
InstallTerminalAppItem(uriHandler) InstallTerminalAppItem(uriHandler)
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) })} ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) }) }
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools)
if (appPlatform.isDesktop && devTools.value) { SectionTextFooter(
generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " +
generalGetString(MR.strings.developer_options)
)
}
if (devTools.value) {
SectionSpacer()
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
if (appPlatform.isDesktop) {
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked -> TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
if (checked) { if (checked) {
withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) {
@ -45,13 +54,11 @@ fun DeveloperView(
m.controller.appPrefs.terminalAlwaysVisible.set(false) m.controller.appPrefs.terminalAlwaysVisible.set(false)
} }
} }
}
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors) SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls)
} }
} }
SectionTextFooter(
generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " +
generalGetString(MR.strings.developer_options)
)
SectionBottomSpacer() SectionBottomSpacer()
} }
} }

View File

@ -37,7 +37,7 @@ fun NetworkAndServersView(
chatModel: ChatModel, chatModel: ChatModel,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
) { ) {
val currentRemoteHost by remember { chatModel.currentRemoteHost } val currentRemoteHost by remember { chatModel.currentRemoteHost }
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations // It's not a state, just a one-time value. Shouldn't be used in any state-related situations
@ -69,7 +69,7 @@ fun NetworkAndServersView(
text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value), text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value),
confirmText = generalGetString(MR.strings.confirm_verb), confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = { onConfirm = {
withApi { withBGApi {
val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get()) val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get())
chatModel.controller.apiSetNetworkConfig(conf) chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf) chatModel.controller.setNetCfg(conf)
@ -84,7 +84,7 @@ fun NetworkAndServersView(
text = generalGetString(MR.strings.network_disable_socks_info), text = generalGetString(MR.strings.network_disable_socks_info),
confirmText = generalGetString(MR.strings.confirm_verb), confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = { onConfirm = {
withApi { withBGApi {
val conf = NetCfg.defaults val conf = NetCfg.defaults
chatModel.controller.apiSetNetworkConfig(conf) chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf) chatModel.controller.setNetCfg(conf)
@ -111,7 +111,7 @@ fun NetworkAndServersView(
onionHosts.value = prevValue onionHosts.value = prevValue
} }
) { ) {
withApi { withBGApi {
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it) val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg) val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) { if (res) {
@ -136,7 +136,7 @@ fun NetworkAndServersView(
startsWith, startsWith,
onDismiss = { sessionMode.value = prevValue } onDismiss = { sessionMode.value = prevValue }
) { ) {
withApi { withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it) val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg) val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) { if (res) {
@ -160,7 +160,7 @@ fun NetworkAndServersView(
proxyPort: State<Int>, proxyPort: State<Int>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
toggleSocksProxy: (Boolean) -> Unit, toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit, useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit,

View File

@ -26,7 +26,7 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
close() close()
} }
fun savePrefs(afterSave: () -> Unit = {}) { fun savePrefs(afterSave: () -> Unit = {}) {
withApi { withBGApi {
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences()) val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
val updated = m.controller.apiUpdateProfile(user.remoteHostId, newProfile) val updated = m.controller.apiUpdateProfile(user.remoteHostId, newProfile)
if (updated != null) { if (updated != null) {

View File

@ -96,7 +96,7 @@ fun PrivacySettingsView(
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withApi { withBGApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs) chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@ -119,7 +119,7 @@ fun PrivacySettingsView(
} }
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withApi { withBGApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs) chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)

View File

@ -28,21 +28,19 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable @Composable
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) { fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
var testing by remember { mutableStateOf(false) } var testing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
ProtocolServerLayout( ProtocolServerLayout(
testing, testing,
server, server,
serverProtocol, serverProtocol,
testServer = { testServer = {
testing = true testing = true
scope.launch { withLongRunningApi {
val res = testServerConnection(server, m) val res = testServerConnection(server, m)
if (isActive) { if (isActive) {
onUpdate(res.first) onUpdate(res.first)

View File

@ -23,12 +23,10 @@ import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform import chat.simplex.common.platform.appPlatform
import chat.simplex.common.views.usersettings.ScanProtocolServer
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.launch
@Composable @Composable
fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) } var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var servers by remember(rhId) { var servers by remember(rhId) {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
@ -56,6 +54,7 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
} }
LaunchedEffect(rhId) { LaunchedEffect(rhId) {
withApi {
val res = m.controller.getUserProtoServers(rhId, serverProtocol) val res = m.controller.getUserProtoServers(rhId, serverProtocol)
if (res != null) { if (res != null) {
currServers.value = res.protoServers currServers.value = res.protoServers
@ -65,7 +64,8 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
} }
} }
} }
}
val testServersJob = CancellableOnGoneJob()
fun showServer(server: ServerCfg) { fun showServer(server: ServerCfg) {
ModalManager.start.showModalCloseable(true) { close -> ModalManager.start.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) } var old by remember { mutableStateOf(server) }
@ -91,7 +91,6 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
}) })
} }
} }
val scope = rememberCoroutineScope()
ModalView( ModalView(
close = { close = {
if (saveDisabled.value) close() if (saveDisabled.value) close()
@ -148,7 +147,7 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
) )
}, },
testServers = { testServers = {
scope.launch { testServersJob.value = withLongRunningApi {
testServers(testing, servers, m) { testServers(testing, servers, m) {
servers = it servers = it
m.userSMPServersUnsaved.value = servers m.userSMPServersUnsaved.value = servers
@ -338,6 +337,7 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
val updatedServers = ArrayList<ServerCfg>(servers) val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) { for ((index, server) in servers.withIndex()) {
if (server.enabled) { if (server.enabled) {
interruptIfCancelled()
val (updatedServer, f) = testServerConnection(server, m) val (updatedServer, f) = testServerConnection(server, m)
updatedServers.removeAt(index) updatedServers.removeAt(index)
updatedServers.add(index, updatedServer) updatedServers.add(index, updatedServer)
@ -352,7 +352,7 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
} }
private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) { private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
withApi { withBGApi {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) { if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers currServers.value = servers
m.userSMPServersUnsaved.value = null m.userSMPServersUnsaved.value = null

View File

@ -24,7 +24,7 @@ fun SetDeliveryReceiptsView(m: ChatModel) {
enableReceipts = { enableReceipts = {
val currentUser = m.currentUser.value val currentUser = m.currentUser.value
if (currentUser != null) { if (currentUser != null) {
withApi { withBGApi {
try { try {
m.controller.apiSetAllContactReceipts(currentUser.remoteHostId, enable = true) m.controller.apiSetAllContactReceipts(currentUser.remoteHostId, enable = true)
m.currentUser.value = currentUser.copy(sendRcptsContacts = true) m.currentUser.value = currentUser.copy(sendRcptsContacts = true)

View File

@ -62,7 +62,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
}, },
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
showVersion = { showVersion = {
withApi { withBGApi {
val info = chatModel.controller.apiGetVersion() val info = chatModel.controller.apiGetVersion()
if (info != null) { if (info != null) {
ModalManager.start.showModal { VersionInfoView(info) } ModalManager.start.showModal { VersionInfoView(info) }
@ -89,7 +89,7 @@ fun SettingsLayout(
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit, showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
drawerState: DrawerState, drawerState: DrawerState,
@ -186,7 +186,7 @@ fun SettingsLayout(
@Composable @Composable
expect fun SettingsSectionApp( expect fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) )
@ -218,16 +218,14 @@ expect fun SettingsSectionApp(
} }
} }
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) { @Composable fun ChatPreferencesItem(showCustomModal: ((@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) {
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_toggle_on), painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.chat_preferences), stringResource(MR.strings.chat_preferences),
click = if (stopped) null else ({ click = if (stopped) null else ({
withApi {
showCustomModal { m, close -> showCustomModal { m, close ->
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
}() }()
}
}), }),
disabled = stopped, disabled = stopped,
extraPadding = true extraPadding = true

View File

@ -70,7 +70,7 @@ fun UserAddressView(
shareViaProfile, shareViaProfile,
onCloseHandler, onCloseHandler,
createAddress = { createAddress = {
withApi { withBGApi {
progressIndicator = true progressIndicator = true
val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId)
if (connReqContact != null) { if (connReqContact != null) {
@ -116,7 +116,7 @@ fun UserAddressView(
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = { onConfirm = {
progressIndicator = true progressIndicator = true
withApi { withBGApi {
val u = chatModel.controller.apiDeleteUserAddress(user?.value?.remoteHostId) val u = chatModel.controller.apiDeleteUserAddress(user?.value?.remoteHostId)
if (u != null) { if (u != null) {
chatModel.userAddress.value = null chatModel.userAddress.value = null

View File

@ -40,7 +40,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
profile = profile, profile = profile,
close, close,
saveProfile = { displayName, fullName, image -> saveProfile = { displayName, fullName, image ->
withApi { withBGApi {
val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, profile.copy(displayName = displayName.trim(), fullName = fullName, image = image)) val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, profile.copy(displayName = displayName.trim(), fullName = fullName, image = image))
if (updated != null) { if (updated != null) {
val (newProfile, _) = updated val (newProfile, _) = updated

View File

@ -139,6 +139,10 @@
<string name="smp_server_test_delete_file">Delete file</string> <string name="smp_server_test_delete_file">Delete file</string>
<string name="error_deleting_user">Error deleting user profile</string> <string name="error_deleting_user">Error deleting user profile</string>
<string name="error_updating_user_privacy">Error updating user privacy</string> <string name="error_updating_user_privacy">Error updating user privacy</string>
<string name="possible_deadlock_title">Deadlock</string>
<string name="possible_deadlock_desc">Execution of code takes too long time: %1$d seconds. Probably, the app is frozen: %2$s</string>
<string name="possible_slow_function_title">Slow function</string>
<string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string>
<!-- background service notice - SimpleXAPI.kt --> <!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Instant notifications</string> <string name="icon_descr_instant_notifications">Instant notifications</string>
@ -686,7 +690,9 @@
<string name="hide_dev_options">Hide:</string> <string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</string> <string name="show_developer_options">Show developer options</string>
<string name="developer_options">Database IDs and Transport isolation option.</string> <string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string>
<string name="show_internal_errors">Show internal errors</string> <string name="show_internal_errors">Show internal errors</string>
<string name="show_slow_api_calls">Show slow API calls</string>
<string name="shutdown_alert_question">Shutdown?</string> <string name="shutdown_alert_question">Shutdown?</string>
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string> <string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M609.824-771.5q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm0 660q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm160-520q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm0 380q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25Zm60-190q-15.824 0-27.074-11.426-11.25-11.426-11.25-27.25t11.426-27.324q11.426-11.5 27.25-11.5t27.324 11.676q11.5 11.676 11.5 27.5t-11.676 27.074q-11.676 11.25-27.5 11.25ZM480-81.5q-82.481 0-155.275-31.304-72.794-31.305-126.706-85.219-53.913-53.915-85.216-126.711Q81.5-397.531 81.5-480.016q0-82.484 31.303-155.273 31.303-72.79 85.216-126.699 53.912-53.909 126.706-85.461Q397.519-879 480-879v60q-141.5 0-240 98.562Q141.5-621.875 141.5-480t98.312 240.188Q338.125-141.5 480-141.5v60Zm-.111-330q-28.389 0-48.389-20.078-20-20.078-20-48.422 0-5.938.75-12.441.75-6.503 3.25-11.808L336-584l40-40.5 81.023 79.5q4.477-2 22.977-4 28.344 0 48.672 20.361Q549-508.279 549-479.889q0 28.389-20.361 48.389-20.36 20-48.75 20Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -186,7 +186,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
} }
} }
// Reload all strings in all @Composable's after language change at runtime // Reload all strings in all @Composable's after language change at runtime
if (remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") { if (remember { ChatController.appPrefs.developerTools.state }.value && remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") {
var hiddenUntilRestart by remember { mutableStateOf(false) } var hiddenUntilRestart by remember { mutableStateOf(false) }
if (!hiddenUntilRestart) { if (!hiddenUntilRestart) {
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp) val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp)

View File

@ -4,8 +4,7 @@ import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.generalGetString import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.withApi
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.net.URLEncoder import java.net.URLEncoder
@ -23,7 +22,7 @@ actual fun ClipboardManager.shareText(text: String) {
} }
actual fun shareFile(text: String, fileSource: CryptoFile) { actual fun shareFile(text: String, fileSource: CryptoFile) {
withApi { withBGApi {
FileChooserLauncher(false) { to: URI? -> FileChooserLauncher(false) { to: URI? ->
if (to != null) { if (to != null) {
val absolutePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) val absolutePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath)

View File

@ -35,7 +35,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
val saveIfExists = { val saveIfExists = {
when (cItem.content.msgContent) { when (cItem.content.msgContent) {
is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withBGApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
else -> {} else -> {}
} }
showMenu.value = false showMenu.value = false

View File

@ -43,7 +43,7 @@ actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<An
.combinedClickable(onClick = { .combinedClickable(onClick = {
val chat = chatModel.getChat(call.contact.id) val chat = chatModel.getChat(call.contact.id)
if (chat != null) { if (chat != null) {
withApi { withBGApi {
openChat(chat.remoteHostId, chat.chatInfo, chatModel) openChat(chat.remoteHostId, chat.chatInfo, chatModel)
} }
} }

View File

@ -2,7 +2,7 @@ package chat.simplex.common.views.database
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.withApi import chat.simplex.common.views.helpers.withBGApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@ -12,7 +12,7 @@ actual fun restartChatOrApp() {
startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged)
} else { } else {
authStopChat(chatModel) { authStopChat(chatModel) {
withApi { withBGApi {
// adding delay in order to prevent locked database by previous initialization // adding delay in order to prevent locked database by previous initialization
delay(1000) delay(1000)
chatModel.chatDbChanged.value = true chatModel.chatDbChanged.value = true

View File

@ -45,7 +45,7 @@ actual fun GetImageBottomSheet(
} }
val pickImageLauncher = rememberFileChooserLauncher(true, null, processPickedImage) val pickImageLauncher = rememberFileChooserLauncher(true, null, processPickedImage)
ActionButton(null, stringResource(MR.strings.from_gallery_button), icon = painterResource(MR.images.ic_image)) { ActionButton(null, stringResource(MR.strings.from_gallery_button), icon = painterResource(MR.images.ic_image)) {
withApi { pickImageLauncher.launch("image/*") } withBGApi { pickImageLauncher.launch("image/*") }
} }
} }
} }

View File

@ -51,7 +51,7 @@ fun AppearanceScope.AppearanceLayout(
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) { LangSelector(state) {
state.value = it state.value = it
withApi { withBGApi {
delay(200) delay(200)
if (it == "system") { if (it == "system") {
languagePref.set(null) languagePref.set(null)

View File

@ -3,6 +3,7 @@ package chat.simplex.common.views.usersettings
import SectionView import SectionView
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.views.helpers.ModalData
import chat.simplex.res.MR import chat.simplex.res.MR
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
@ -10,7 +11,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
actual fun SettingsSectionApp( actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {