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

View File

@ -104,7 +104,7 @@ class SimplexService: Service() {
if (wakeLock != null || isStartingService) return
val self = this
isStartingService = true
withApi {
withBGApi {
val chatController = ChatController
waitDbMigrationEnds(chatController)
try {
@ -114,7 +114,7 @@ class SimplexService: Service() {
Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus")
showPassphraseNotification(chatDbStatus)
safeStopService()
return@withApi
return@withBGApi
}
saveServiceState(self, ServiceState.STARTED)
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.platform.FileChooserLauncher
import chat.simplex.common.platform.saveImage
import chat.simplex.common.views.helpers.SharedContent
import chat.simplex.common.views.helpers.withApi
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberPermissionState
import dev.icerock.moko.resources.compose.painterResource
@ -37,7 +36,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
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 -> {}
}
showMenu.value = false

View File

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

View File

@ -5,8 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.work.WorkManager
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.jakewharton.processphoenix.ProcessPhoenix
import dev.icerock.moko.resources.compose.painterResource
@ -15,7 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> 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 developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, 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 networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
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_DEVELOPER_TOOLS = "DeveloperTools"
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_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
@ -464,7 +466,7 @@ object ChatController {
suspend fun sendCmd(rhId: Long?, cmd: CC): CR {
val ctrl = ctrl ?: throw Exception("Controller is not initialized")
return withContext(Dispatchers.IO) {
//return withContext(Dispatchers.IO) {
val c = cmd.cmdString
chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated))
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
@ -475,8 +477,8 @@ object ChatController {
Log.d(TAG, "sendCmd response json $json")
}
chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp))
r.resp
}
return r.resp
//}
}
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.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (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)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
@ -1863,12 +1866,10 @@ object ChatController {
}
withCall(r, r.contact) { _ ->
chatModel.callCommand.add(WCallCommand.End)
withApi {
chatModel.activeCall.value = null
chatModel.showCallView.value = false
}
}
}
is CR.ContactSwitch ->
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
is CR.GroupMemberSwitch ->
@ -1977,7 +1978,7 @@ object ChatController {
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
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)
}
}
@ -2004,10 +2005,9 @@ object ChatController {
}
}
fun switchToLocalSession() {
suspend fun switchToLocalSession() {
val m = chatModel
m.remoteCtrlSession.value = null
withBGApi {
val users = listUsers(null)
m.users.clear()
m.users.addAll(users)
@ -2019,7 +2019,6 @@ object ChatController {
chatModel.networkStatuses.putAll(ss)
}
}
}
private fun activeUser(rhId: Long?, user: UserLike): Boolean =
rhId == chatModel.remoteHostId() && user.userId == chatModel.currentUser.value?.userId

View File

@ -42,7 +42,7 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() {
withBGApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert)
} else {
@ -57,7 +57,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (chatModel.ctrlInitInProgress.value) return
chatModel.ctrlInitInProgress.value = true
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 res: DBMigrationResult = kotlin.runCatching {
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))
composeState.value = ComposeState(useLinkPreviews = false)
} else {
withApi {
withBGApi {
// show "in progress"
// TODO show active remote host in chat console?
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) {
withApi {
val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withApi
withBGApi {
val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withBGApi
controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
chatModel.chatRunning.value = false
controller.startChat(user)
@ -186,11 +186,11 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) {
}
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi {
withBGApi {
val rhId = chatModel.remoteHostId()
val user = chatModel.controller.apiCreateActiveUser(
rhId, Profile(displayName.trim(), "", null)
) ?: return@withApi
) ?: return@withBGApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
@ -206,10 +206,10 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: ()
}
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi {
withBGApi {
chatModel.currentUser.value = chatModel.controller.apiCreateActiveUser(
null, Profile(displayName.trim(), "", null)
) ?: return@withApi
) ?: return@withBGApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) {
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.platform.*
import chat.simplex.common.views.helpers.withApi
import chat.simplex.common.views.helpers.withBGApi
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.minutes
@ -28,13 +28,13 @@ class CallManager(val chatModel: ChatModel) {
if (call == null) {
justAcceptIncomingCall(invitation = invitation)
} else {
withApi {
withBGApi {
chatModel.switchingCall.value = true
try {
endCall(call = call)
justAcceptIncomingCall(invitation = invitation)
} finally {
withApi { chatModel.switchingCall.value = false }
chatModel.switchingCall.value = false
}
}
}
@ -90,7 +90,7 @@ class CallManager(val chatModel: ChatModel) {
activeCallInvitation.value = null
ntfManager.cancelCallNotification()
}
withApi {
withBGApi {
if (!controller.apiRejectCall(invitation.remoteHostId, invitation.contact)) {
Log.e(TAG, "apiRejectCall error")
}

View File

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

View File

@ -164,7 +164,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
return@ChatLayout
}
hideKeyboard(view)
withApi {
withBGApi {
// 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 preloadedCode: String? = null
@ -205,7 +205,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withApi {
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
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 firstId = chatModel.chatItems.firstOrNull()?.id
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}")
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}")
@ -236,7 +236,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
deleteMessage = { itemId, mode ->
withApi {
withBGApi {
val cInfo = chat.chatInfo
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
@ -291,13 +291,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
receiveFile = { fileId, encrypted ->
withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
},
cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
},
joinGroup = { groupId, onComplete ->
withApi {
withBGApi {
chatModel.controller.apiJoinGroup(chatRh, groupId)
onComplete.invoke()
}
@ -314,11 +314,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
endCall = {
val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) }
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
},
acceptCall = { contact ->
hideKeyboard(view)
withApi {
withBGApi {
val invitation = chatModel.callInvitations.remove(contact.id)
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
if (invitation == null) {
@ -329,17 +329,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
acceptFeature = { contact, feature, param ->
withApi {
withBGApi {
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
}
},
openDirectChat = { contactId ->
withApi {
withBGApi {
openDirectChat(chatRh, contactId, chatModel)
}
},
updateContactStats = { contact ->
withApi {
withBGApi {
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
if (r != null) {
val contactStats = r.first
@ -349,7 +349,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
updateMemberStats = { groupInfo, member ->
withApi {
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
@ -360,7 +360,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
syncContactConnection = { contact ->
withApi {
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
@ -368,7 +368,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
syncMemberConnection = { groupInfo, member ->
withApi {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
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 }
},
setReaction = { cInfo, cItem, add, reaction ->
withApi {
withBGApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
rh = chatRh,
type = cInfo.chatType,
@ -397,7 +397,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
},
showItemDetails = { cInfo, cItem ->
withApi {
withBGApi {
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
if (chat.chatInfo is ChatInfo.Group) {
@ -416,7 +416,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
withBGApi {
setGroupMembers(chatRh, groupInfo, chatModel)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
@ -426,7 +426,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
openGroupLink = { groupInfo ->
hideKeyboard(view)
withApi {
withBGApi {
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) {
@ -451,7 +451,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
if (searchText.value == value) return@ChatLayout
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
withApi {
withBGApi {
apiFindMessages(c, chatModel, value)
searchText.value = value
}

View File

@ -267,7 +267,7 @@ fun ComposeView(
fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
withApi {
withBGApi {
if (wait != null) delay(wait)
val lp = getLinkPreview(url)
if (lp != null && pendingLinkUrl.value == url) {
@ -575,7 +575,7 @@ fun ComposeView(
fun allowVoiceToContact() {
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
withApi {
withBGApi {
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) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
withBGApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
if (toContact != null) {

View File

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

View File

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

View File

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

View File

@ -66,7 +66,7 @@ fun GroupMemberInfoView(
connectionCode,
getContactChat = { chatModel.getContactChat(it) },
openDirectChat = {
withApi {
withBGApi {
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
if (c != null) {
if (chatModel.getContactChat(it) == null) {
@ -81,7 +81,7 @@ fun GroupMemberInfoView(
}
},
createMemberContact = {
withApi {
withBGApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
@ -107,7 +107,7 @@ fun GroupMemberInfoView(
updateMemberRoleDialog(it, member, onDismiss = {
newRole.value = prevValue
}) {
withApi {
withBGApi {
kotlin.runCatching {
val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it)
chatModel.upsertGroupMember(rhId, groupInfo, mem)
@ -119,7 +119,7 @@ fun GroupMemberInfoView(
},
switchMemberAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
withBGApi {
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
@ -131,7 +131,7 @@ fun GroupMemberInfoView(
},
abortSwitchMemberAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
withBGApi {
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
@ -142,7 +142,7 @@ fun GroupMemberInfoView(
})
},
syncMemberConnection = {
withApi {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
@ -153,7 +153,7 @@ fun GroupMemberInfoView(
},
syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
if (r != null) {
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),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
withApi {
withBGApi {
val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId)
if (removedMember != null) {
chatModel.upsertGroupMember(rhId, groupInfo, removedMember)
@ -505,7 +505,7 @@ private fun updateMemberRoleDialog(
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
try {
val uri = URI(connReqUri)
withApi {
withBGApi {
planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
}
} 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) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
withBGApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
if (g != null) {

View File

@ -35,7 +35,7 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
close = close,
groupProfile = groupInfo.groupProfile,
saveProfile = { p ->
withApi {
withBGApi {
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
if (gInfo != null) {
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 ?: "") }
fun save(afterSave: () -> Unit = {}) {
withApi {
withBGApi {
var welcome: String? = welcomeText.value.trim('\n', ' ')
if (welcome?.length == 0) {
welcome = null

View File

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

View File

@ -394,7 +394,7 @@ fun JoinGroupAction(
inProgress: MutableState<Boolean>
) {
val joinGroup: () -> Unit = {
withApi {
withBGApi {
inProgress.value = true
chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId)
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) {
withApi {
withBGApi {
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
if (contact != null && isCurrentUser && contactRequest != null) {
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) {
withApi {
withBGApi {
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
chatModel.removeChat(rhId, contactRequest.id)
}
@ -606,7 +606,7 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti
),
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withApi {
withBGApi {
AlertManager.shared.hideAlert()
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
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),
confirmText = generalGetString(MR.strings.button_delete_contact),
onConfirm = {
withApi {
withBGApi {
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(rhId, chatInfo.id)
@ -654,7 +654,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
Column {
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
close?.invoke()
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false)
if (ok && openChat) {
@ -666,7 +666,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
}
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
close?.invoke()
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true)
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),
confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = {
withApi {
withBGApi {
inProgress?.value = true
chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId)
inProgress?.value = false
@ -728,7 +728,7 @@ fun cantInviteIncognitoAlert() {
}
fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
withApi {
withBGApi {
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
if (r) {
chatModel.removeChat(rhId, groupInfo.id)
@ -769,7 +769,7 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
}
else -> null
}
withApi {
withBGApi {
val res = when (newChatInfo) {
is ChatInfo.Direct -> with(newChatInfo) {
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) {
chatModel.appOpenUrl.value = rhId to uri
} else {
withApi {
withBGApi {
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.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
@ -117,15 +117,17 @@ fun UserPicker(
LaunchedEffect(Unit) {
// Controller.ctrl can be null when self-destructing activates
if (controller.ctrl != null && controller.ctrl != -1L) {
withBGApi {
controller.reloadRemoteHosts()
}
}
}
val UsersView: @Composable ColumnScope.() -> Unit = {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) {
userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) {
scope.launch {
withBGApi {
controller.showProgressIfNeeded {
ModalManager.closeAllModalsEverywhere()
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(
title,
archiveTime,
saveArchive = { withApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }},
saveArchive = { withBGApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }},
deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) }
)
}

View File

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

View File

@ -89,7 +89,7 @@ enum class MigrationConfirmation(val value: String) {
}
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
sealed class MigrationError {

View File

@ -45,7 +45,7 @@ class ModalData {
}
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 toRemove = mutableSetOf<Int>()
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")
val data = ModalData()
// 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
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)
// to prevent unneeded animation on different situations
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
if (placement == ModalPlacement.CENTER) {
@ -117,7 +118,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
fun showInView() {
// Without animation
if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) {
modalViews.lastOrNull()?.second?.invoke(::closeModal)
modalViews.lastOrNull()?.let { it.third(it.second, ::closeModal) }
return
}
AnimatedContent(targetState = modalCount.value,
@ -129,7 +130,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
}.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
if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) {
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }

View File

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

View File

@ -2,6 +2,7 @@ package chat.simplex.common.views.helpers
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
@ -21,15 +22,71 @@ import java.net.URI
import java.nio.file.Files
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
import kotlin.math.*
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action)
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 =
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 {
Opened, Closed
@ -422,11 +479,15 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}
val orientation = windowOrientation()
onDispose {
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()) {
whenGone()
}
}
}
}
}
@Composable

View File

@ -36,7 +36,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
val rhId = rh?.remoteHostId
AddGroupLayout(
createGroup = { incognito, groupProfile ->
withApi {
withBGApi {
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
if (groupInfo != null) {
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),
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),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup,
onDismissRequest = cleanup,
destructive = true,
@ -134,7 +134,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
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),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
destructive = true,
onDismiss = cleanup,
onDismissRequest = cleanup,
@ -157,7 +157,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
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),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup,
onDismissRequest = cleanup,
destructive = true,
@ -223,7 +223,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link),
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),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup,
onDismissRequest = cleanup,
hostDevice = hostDevice(rhId),
@ -254,7 +254,7 @@ suspend fun planAndConnect(
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,
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,
onDismissRequest = cleanup,
destructive = true,
@ -374,7 +374,7 @@ fun askCurrentOrIncognitoProfileAlert(
val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup)
}
}) {
@ -382,7 +382,7 @@ fun askCurrentOrIncognitoProfileAlert(
}
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
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) {
withApi {
withBGApi {
val c = chatModel.getContactChat(contact.contactId)
if (c != null) {
close?.invoke()
@ -439,7 +439,7 @@ fun ownGroupLinkConfirmConnect(
// Join incognito / Join with current profile
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup)
}
}) {
@ -452,7 +452,7 @@ fun ownGroupLinkConfirmConnect(
// Use current profile
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup)
}
}) {
@ -461,7 +461,7 @@ fun ownGroupLinkConfirmConnect(
// Use new incognito profile
SectionItemView({
AlertManager.privacySensitive.hideAlert()
withApi {
withBGApi {
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) {
withApi {
withBGApi {
val g = chatModel.getGroupChat(groupInfo.groupId)
if (g != null) {
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.updateContactConnection(rhId, it)
}

View File

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

View File

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

View File

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

View File

@ -38,7 +38,9 @@ import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
@Composable
fun ConnectMobileView() {
@ -46,8 +48,10 @@ fun ConnectMobileView() {
val remoteHosts = remember { chatModel.remoteHosts }
val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess
LaunchedEffect(Unit) {
withBGApi {
controller.reloadRemoteHosts()
}
}
ConnectMobileLayout(
deviceName = remember { deviceName.state },
remoteHosts = remoteHosts,

View File

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

View File

@ -131,7 +131,7 @@ object AppearanceScope {
SectionItemView({
val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme)
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)
}
@ -144,7 +144,7 @@ object AppearanceScope {
}
}
// 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)
}
}

View File

@ -1,6 +1,7 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.Column
@ -22,7 +23,7 @@ import chat.simplex.res.MR
@Composable
fun DeveloperView(
m: ChatModel,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) {
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
@ -30,12 +31,20 @@ fun DeveloperView(
AppBarTitle(stringResource(MR.strings.settings_developer_tools))
val developerTools = m.controller.appPrefs.developerTools
val devTools = remember { developerTools.state }
SectionView() {
SectionView {
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) })}
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
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_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 ->
if (checked) {
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)
}
}
}
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()
}
}

View File

@ -37,7 +37,7 @@ fun NetworkAndServersView(
chatModel: ChatModel,
showModal: (@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 }
// 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),
confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = {
withApi {
withBGApi {
val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get())
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
@ -84,7 +84,7 @@ fun NetworkAndServersView(
text = generalGetString(MR.strings.network_disable_socks_info),
confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = {
withApi {
withBGApi {
val conf = NetCfg.defaults
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
@ -111,7 +111,7 @@ fun NetworkAndServersView(
onionHosts.value = prevValue
}
) {
withApi {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
@ -136,7 +136,7 @@ fun NetworkAndServersView(
startsWith,
onDismiss = { sessionMode.value = prevValue }
) {
withApi {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
@ -160,7 +160,7 @@ fun NetworkAndServersView(
proxyPort: State<Int>,
showModal: (@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,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,

View File

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

View File

@ -96,7 +96,7 @@ fun PrivacySettingsView(
val currentUser = chatModel.currentUser.value
if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withApi {
withBGApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@ -119,7 +119,7 @@ fun PrivacySettingsView(
}
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withApi {
withBGApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
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.model.ChatModel
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
var testing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
ProtocolServerLayout(
testing,
server,
serverProtocol,
testServer = {
testing = true
scope.launch {
withLongRunningApi {
val res = testServerConnection(server, m)
if (isActive) {
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.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.views.usersettings.ScanProtocolServer
import chat.simplex.res.MR
import kotlinx.coroutines.launch
@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 servers by remember(rhId) {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
@ -56,6 +54,7 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
}
LaunchedEffect(rhId) {
withApi {
val res = m.controller.getUserProtoServers(rhId, serverProtocol)
if (res != null) {
currServers.value = res.protoServers
@ -65,7 +64,8 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
}
}
}
}
val testServersJob = CancellableOnGoneJob()
fun showServer(server: ServerCfg) {
ModalManager.start.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) }
@ -91,7 +91,6 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
})
}
}
val scope = rememberCoroutineScope()
ModalView(
close = {
if (saveDisabled.value) close()
@ -148,7 +147,7 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
)
},
testServers = {
scope.launch {
testServersJob.value = withLongRunningApi {
testServers(testing, servers, m) {
servers = it
m.userSMPServersUnsaved.value = servers
@ -338,6 +337,7 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
interruptIfCancelled()
val (updatedServer, f) = testServerConnection(server, m)
updatedServers.removeAt(index)
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 = {}) {
withApi {
withBGApi {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers
m.userSMPServersUnsaved.value = null

View File

@ -24,7 +24,7 @@ fun SetDeliveryReceiptsView(m: ChatModel) {
enableReceipts = {
val currentUser = m.currentUser.value
if (currentUser != null) {
withApi {
withBGApi {
try {
m.controller.apiSetAllContactReceipts(currentUser.remoteHostId, enable = 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) } } },
showVersion = {
withApi {
withBGApi {
val info = chatModel.controller.apiGetVersion()
if (info != null) {
ModalManager.start.showModal { VersionInfoView(info) }
@ -89,7 +89,7 @@ fun SettingsLayout(
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
drawerState: DrawerState,
@ -186,7 +186,7 @@ fun SettingsLayout(
@Composable
expect fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> 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(
painterResource(MR.images.ic_toggle_on),
stringResource(MR.strings.chat_preferences),
click = if (stopped) null else ({
withApi {
showCustomModal { m, close ->
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
}()
}
}),
disabled = stopped,
extraPadding = true

View File

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

View File

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

View File

@ -139,6 +139,10 @@
<string name="smp_server_test_delete_file">Delete file</string>
<string name="error_deleting_user">Error deleting user profile</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 -->
<string name="icon_descr_instant_notifications">Instant notifications</string>
@ -686,7 +690,9 @@
<string name="hide_dev_options">Hide:</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_section">Developer options</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_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
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) }
if (!hiddenUntilRestart) {
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.text.AnnotatedString
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.views.helpers.withApi
import chat.simplex.common.views.helpers.*
import java.io.File
import java.net.URI
import java.net.URLEncoder
@ -23,7 +22,7 @@ actual fun ClipboardManager.shareText(text: String) {
}
actual fun shareFile(text: String, fileSource: CryptoFile) {
withApi {
withBGApi {
FileChooserLauncher(false) { to: URI? ->
if (to != null) {
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 = {
val saveIfExists = {
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 -> {}
}
showMenu.value = false

View File

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

View File

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

View File

@ -45,7 +45,7 @@ actual fun GetImageBottomSheet(
}
val pickImageLauncher = rememberFileChooserLauncher(true, null, processPickedImage)
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") }
LangSelector(state) {
state.value = it
withApi {
withBGApi {
delay(200)
if (it == "system") {
languagePref.set(null)

View File

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