mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-01 00:56:05 +00:00
* ios: share extension (#4414) * ios: add share extension target * ios: Add UI * ios: send file from share-sheet * image utils * ShareError * error handling; ui-cleanup * progress bar; completion for direct chat * cleanup * cleanup * ios: unify filter and sort between forward and share sheets * ios: match share sheet styling with the main app * ios: fix text input stroke width * ios: align compose views * more of the same... * ShareAPI * remove combine * minor * Better error descriptions --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: enable file sending workers in share extension (#4474) * ios: align compose background, row height and fallback images for share-sheet (#4467) Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: coordinate database access between share extension, the app and notifications extension (#4472) * ios: database management proposal * Add SEState * Global event loop * minor * reset state * use apiCreateItem for local chats * simplify waiting for suspension * loading bar * Dismiss share sheet with error --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * send image message (#4481) Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: improve share extension completion handling (#4486) * improve completion handling * minor * show only spinner for group send * rework event loop, errorAlert * group chat timeout loading bar * state machine WIP * event loop actor * alert * errors text * default * file error --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: add remaining share types; process attachment in background on launch (#4510) * add remaining share types; process attachment in background on launch * cleanup diff * revert `makeVideoQualityLower` * reduce diff * reduce diff * iOS15 support * process events when sharing link and text * cleanup * remove video file on failure * cleanup CompletionHandler --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: share extension - additional alerts and media previews (#4521) * add remaining share types; process attachment in background on launch * cleanup diff * revert `makeVideoQualityLower` * reduce diff * reduce diff * iOS15 support * process events when sharing link and text * cleanup * remove video file on failure * cleanup CompletionHandler * media previews * network timeout alert * revert framework compiler optimisation flag * suspend chat after sheet dismiss * activate chat * update * fix search * sendMessageColor, file preview, chat deselect, simplify error action * cleanup * interupt database closing when sheet is reopened quickly * cleanup redundant alert check * restore package * refactor previews, remove link preview * show link preview when becomes available * comment * dont fail on invalid image * suspend --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: descriptive database errors (#4527) * ios: set share extension as inactive when suspending chat --------- Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com>
204 lines
6.2 KiB
Swift
204 lines
6.2 KiB
Swift
//
|
|
// SuspendChat.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by Evgeny on 26/06/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import SimpleXChat
|
|
import SwiftUI
|
|
|
|
private let suspendLockQueue = DispatchQueue(label: "chat.simplex.app.suspend.lock")
|
|
|
|
let bgSuspendTimeout: Int = 5 // seconds
|
|
|
|
let terminationTimeout: Int = 3 // seconds
|
|
|
|
let activationDelay: TimeInterval = 1.5
|
|
|
|
let nseSuspendTimeout: TimeInterval = 5
|
|
|
|
private func _suspendChat(timeout: Int) {
|
|
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
|
let state = AppChatState.shared.value
|
|
if !state.canSuspend {
|
|
logger.error("_suspendChat called, current state: \(state.rawValue)")
|
|
} else if ChatModel.ok {
|
|
AppChatState.shared.set(.suspending)
|
|
apiSuspendChat(timeoutMicroseconds: timeout * 1000000)
|
|
let endTask = beginBGTask(chatSuspended)
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: endTask)
|
|
} else {
|
|
AppChatState.shared.set(.suspended)
|
|
}
|
|
}
|
|
|
|
let seSubscriber = seMessageSubscriber {
|
|
switch $0 {
|
|
case let .state(state):
|
|
switch state {
|
|
case .inactive:
|
|
if AppChatState.shared.value.inactive { activateChat() }
|
|
case .sendingMessage:
|
|
if AppChatState.shared.value.canSuspend { suspendChat() }
|
|
}
|
|
}
|
|
}
|
|
|
|
func suspendChat() {
|
|
suspendLockQueue.sync {
|
|
_suspendChat(timeout: appSuspendTimeout)
|
|
}
|
|
}
|
|
|
|
func suspendBgRefresh() {
|
|
suspendLockQueue.sync {
|
|
if case .bgRefresh = AppChatState.shared.value {
|
|
_suspendChat(timeout: bgSuspendTimeout)
|
|
}
|
|
}
|
|
}
|
|
|
|
func terminateChat() {
|
|
logger.debug("terminateChat")
|
|
suspendLockQueue.sync {
|
|
switch AppChatState.shared.value {
|
|
case .suspending:
|
|
// suspend instantly if already suspending
|
|
_chatSuspended()
|
|
// when apiSuspendChat is called with timeout 0, it won't send any events on suspension
|
|
if ChatModel.ok { apiSuspendChat(timeoutMicroseconds: 0) }
|
|
chatCloseStore()
|
|
case .suspended:
|
|
chatCloseStore()
|
|
case .stopped:
|
|
chatCloseStore()
|
|
default:
|
|
// the store will be closed in _chatSuspended when event is received
|
|
_suspendChat(timeout: terminationTimeout)
|
|
}
|
|
}
|
|
}
|
|
|
|
func chatSuspended() {
|
|
suspendLockQueue.sync {
|
|
if case .suspending = AppChatState.shared.value {
|
|
_chatSuspended()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func _chatSuspended() {
|
|
logger.debug("_chatSuspended")
|
|
AppChatState.shared.set(.suspended)
|
|
if ChatModel.shared.chatRunning == true {
|
|
ChatReceiver.shared.stop()
|
|
}
|
|
chatCloseStore()
|
|
}
|
|
|
|
func setAppState(_ appState: AppState) {
|
|
suspendLockQueue.sync {
|
|
AppChatState.shared.set(appState)
|
|
}
|
|
}
|
|
|
|
func activateChat(appState: AppState = .active) {
|
|
logger.debug("DEBUGGING: activateChat")
|
|
suspendLockQueue.sync {
|
|
AppChatState.shared.set(appState)
|
|
if ChatModel.ok { apiActivateChat() }
|
|
logger.debug("DEBUGGING: activateChat: after apiActivateChat")
|
|
}
|
|
}
|
|
|
|
func initChatAndMigrate(refreshInvitations: Bool = true) {
|
|
let m = ChatModel.shared
|
|
if (!m.chatInitialized) {
|
|
m.v3DBMigration = v3DBMigrationDefault.get()
|
|
if AppChatState.shared.value == .stopped && storeDBPassphraseGroupDefault.get() && kcDatabasePassword.get() != nil {
|
|
initialize(start: true, confirmStart: true)
|
|
} else {
|
|
initialize(start: true)
|
|
}
|
|
}
|
|
|
|
func initialize(start: Bool, confirmStart: Bool = false) {
|
|
do {
|
|
try initializeChat(start: m.v3DBMigration.startChat && start, confirmStart: m.v3DBMigration.startChat && confirmStart, refreshInvitations: refreshInvitations)
|
|
} catch let error {
|
|
AlertManager.shared.showAlertMsg(
|
|
title: start ? "Error starting chat" : "Error opening chat",
|
|
message: "Please contact developers.\nError: \(responseError(error))"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func startChatForCall() {
|
|
logger.debug("DEBUGGING: startChatForCall")
|
|
if ChatModel.shared.chatRunning == true {
|
|
ChatReceiver.shared.start()
|
|
logger.debug("DEBUGGING: startChatForCall: after ChatReceiver.shared.start")
|
|
}
|
|
if .active != AppChatState.shared.value {
|
|
logger.debug("DEBUGGING: startChatForCall: before activateChat")
|
|
activateChat()
|
|
logger.debug("DEBUGGING: startChatForCall: after activateChat")
|
|
}
|
|
}
|
|
|
|
func startChatAndActivate(_ completion: @escaping () -> Void) {
|
|
logger.debug("DEBUGGING: startChatAndActivate")
|
|
if ChatModel.shared.chatRunning == true {
|
|
ChatReceiver.shared.start()
|
|
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
|
|
}
|
|
if case .active = AppChatState.shared.value {
|
|
completion()
|
|
} else if nseStateGroupDefault.get().inactive {
|
|
activate()
|
|
} else {
|
|
// setting app state to "activating" to notify NSE that it should suspend
|
|
setAppState(.activating)
|
|
waitNSESuspended(timeout: nseSuspendTimeout) { ok in
|
|
if !ok {
|
|
// if for some reason NSE failed to suspend,
|
|
// e.g., it crashed previously without setting its state to "suspended",
|
|
// set it to "suspended" state anyway, so that next time app
|
|
// does not have to wait when activating.
|
|
nseStateGroupDefault.set(.suspended)
|
|
}
|
|
if AppChatState.shared.value == .activating {
|
|
activate()
|
|
}
|
|
}
|
|
}
|
|
|
|
func activate() {
|
|
logger.debug("DEBUGGING: startChatAndActivate: before activateChat")
|
|
activateChat()
|
|
completion()
|
|
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
|
|
}
|
|
}
|
|
|
|
// appStateGroupDefault must not be used in the app directly, only via this singleton
|
|
class AppChatState {
|
|
static let shared = AppChatState()
|
|
private var value_ = appStateGroupDefault.get()
|
|
|
|
var value: AppState {
|
|
value_
|
|
}
|
|
|
|
func set(_ state: AppState) {
|
|
appStateGroupDefault.set(state)
|
|
sendAppState(state)
|
|
value_ = state
|
|
}
|
|
}
|