ios: UI to export/import/delete chat database (#743)
* ios: UI to export/import/delete chat database * move files * ui for database migration * migration screen layout * ios: export archive and delete chat database * import archive * refactor, update texts * database migration (almost works) * fix missing import * delete legacy database * update migration errors
This commit is contained in:
committed by
GitHub
parent
4d9e446489
commit
6a2f2a512f
@@ -14,6 +14,7 @@ struct ContentView: View {
|
||||
@Binding var doAuthenticate: Bool
|
||||
@Binding var userAuthorized: Bool?
|
||||
@State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
@State private var v3DBMigration = v3DBMigrationDefault.get()
|
||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@@ -26,35 +27,43 @@ struct ContentView: View {
|
||||
if let step = chatModel.onboardingStage {
|
||||
if case .onboardingComplete = step,
|
||||
chatModel.currentUser != nil {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
NtfManager.shared.requestAuthorization(onDeny: {
|
||||
alertManager.showAlert(notificationAlert())
|
||||
})
|
||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||
if (!prefLANoticeShown && prefShowLANotice) {
|
||||
prefLANoticeShown = true
|
||||
alertManager.showAlert(laNoticeAlert())
|
||||
}
|
||||
prefShowLANotice = true
|
||||
}
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
ActiveCallView(call: call)
|
||||
}
|
||||
IncomingCallView()
|
||||
}
|
||||
mainView()
|
||||
} else {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
} else if !v3DBMigrationDefault.get().startChat {
|
||||
MigrateToAppGroupView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { if doAuthenticate { runAuthenticate() } }
|
||||
.onAppear {
|
||||
if doAuthenticate { runAuthenticate() }
|
||||
}
|
||||
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
}
|
||||
|
||||
private func mainView() -> some View {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
NtfManager.shared.requestAuthorization(onDeny: {
|
||||
alertManager.showAlert(notificationAlert())
|
||||
})
|
||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||
if (!prefLANoticeShown && prefShowLANotice) {
|
||||
prefLANoticeShown = true
|
||||
alertManager.showAlert(laNoticeAlert())
|
||||
}
|
||||
prefShowLANotice = true
|
||||
}
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
ActiveCallView(call: call)
|
||||
}
|
||||
IncomingCallView()
|
||||
}
|
||||
}
|
||||
|
||||
private func runAuthenticate() {
|
||||
if !prefPerformLA {
|
||||
userAuthorized = true
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import BackgroundTasks
|
||||
import SimpleXChat
|
||||
|
||||
private let receiveTaskId = "chat.simplex.app.receive"
|
||||
|
||||
@@ -71,7 +72,11 @@ class BGManager {
|
||||
}
|
||||
self.completed = false
|
||||
DispatchQueue.main.async {
|
||||
initializeChat()
|
||||
do {
|
||||
try initializeChat(start: true)
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
if ChatModel.shared.currentUser == nil {
|
||||
completeReceiving("no current user")
|
||||
return
|
||||
|
||||
@@ -15,6 +15,8 @@ import SimpleXChat
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var currentUser: User?
|
||||
@Published var chatRunning: Bool?
|
||||
@Published var chatDbChanged = false
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// current chat
|
||||
|
||||
@@ -72,7 +72,7 @@ func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
|
||||
let msgDelay: Double = 7.5
|
||||
let maxTaskDuration: Double = 15
|
||||
|
||||
private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> ChatResponse) -> ChatResponse {
|
||||
private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
|
||||
let endTask = beginBGTask()
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
|
||||
let r = f()
|
||||
@@ -93,11 +93,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
if case .apiParseMarkdown = cmd {} else {
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
|
||||
ChatModel.shared.terminalItems.append(.resp(.now, resp))
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
|
||||
ChatModel.shared.terminalItems.append(.resp(.now, resp))
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -108,10 +106,10 @@ func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil
|
||||
}
|
||||
}
|
||||
|
||||
func chatRecvMsg() async -> ChatResponse {
|
||||
func chatRecvMsg() async -> ChatResponse? {
|
||||
await withCheckedContinuation { cont in
|
||||
_ = withBGTask(bgDelay: msgDelay) {
|
||||
let resp = chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||
_ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in
|
||||
let resp = recvSimpleXMsg()
|
||||
cont.resume(returning: resp)
|
||||
return resp
|
||||
}
|
||||
@@ -143,8 +141,8 @@ func apiStartChat() throws -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func apiStopChat() throws {
|
||||
let r = chatSendCmdSync(.apiStopChat)
|
||||
func apiStopChat() async throws {
|
||||
let r = await chatSendCmd(.apiStopChat)
|
||||
switch r {
|
||||
case .chatStopped: return
|
||||
default: throw r
|
||||
@@ -163,6 +161,18 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiExportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||
}
|
||||
|
||||
func apiImportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiImportArchive(config: config))
|
||||
}
|
||||
|
||||
func apiDeleteStorage() async throws {
|
||||
try await sendCommandOkResp(.apiDeleteStorage)
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [Chat] {
|
||||
let r = chatSendCmdSync(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
|
||||
@@ -326,12 +336,6 @@ func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiParseMarkdown(text: String) throws -> [FormattedText]? {
|
||||
let r = chatSendCmdSync(.apiParseMarkdown(text: text))
|
||||
if case let .apiParsedMarkdown(formattedText) = r { return formattedText }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
let r = await chatSendCmd(.createMyAddress)
|
||||
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
||||
@@ -468,42 +472,42 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func initializeChat() {
|
||||
func initializeChat(start: Bool) throws {
|
||||
logger.debug("initializeChat")
|
||||
do {
|
||||
let m = ChatModel.shared
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
} else if start {
|
||||
try startChat()
|
||||
} else {
|
||||
startChat()
|
||||
m.chatRunning = false
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to initialize chat controller or database: \(error)")
|
||||
fatalError("Failed to initialize chat controller or database: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func startChat() {
|
||||
func startChat() throws {
|
||||
logger.debug("startChat")
|
||||
do {
|
||||
let m = ChatModel.shared
|
||||
// TODO set file folder once, before chat is started
|
||||
let justStarted = try apiStartChat()
|
||||
if justStarted {
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
m.userSMPServers = try getUserSMPServers()
|
||||
m.chats = try apiGetChats()
|
||||
withAnimation {
|
||||
m.onboardingStage = m.chats.isEmpty
|
||||
? .step3_MakeConnection
|
||||
: .onboardingComplete
|
||||
}
|
||||
let m = ChatModel.shared
|
||||
// TODO set file folder once, before chat is started
|
||||
let justStarted = try apiStartChat()
|
||||
if justStarted {
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
m.userSMPServers = try getUserSMPServers()
|
||||
m.chats = try apiGetChats()
|
||||
withAnimation {
|
||||
m.onboardingStage = m.chats.isEmpty
|
||||
? .step3_MakeConnection
|
||||
: .onboardingComplete
|
||||
}
|
||||
ChatReceiver.shared.start()
|
||||
} catch {
|
||||
fatalError("Failed to start or load chats: \(error)")
|
||||
}
|
||||
ChatReceiver.shared.start()
|
||||
m.chatRunning = true
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
class ChatReceiver {
|
||||
@@ -524,9 +528,11 @@ class ChatReceiver {
|
||||
}
|
||||
|
||||
func receiveMsgLoop() async {
|
||||
let msg = await chatRecvMsg()
|
||||
self._lastMsgTime = .now
|
||||
await processReceivedMsg(msg)
|
||||
// TODO use function that has timeout
|
||||
if let msg = await chatRecvMsg() {
|
||||
self._lastMsgTime = .now
|
||||
await processReceivedMsg(msg)
|
||||
}
|
||||
if self.receiveMessages {
|
||||
do { try await Task.sleep(nanoseconds: 7_500_000) }
|
||||
catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") }
|
||||
@@ -697,7 +703,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
// CallController.shared.reportCallRemoteEnded(call: call)
|
||||
}
|
||||
case let .appPhase(appPhase):
|
||||
setAppState(AppState(appPhase: appPhase))
|
||||
appStateGroupDefault.set(AppState(appPhase: appPhase))
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ struct SimpleXApp: App {
|
||||
init() {
|
||||
hs_init(0, nil)
|
||||
UserDefaults.standard.register(defaults: appDefaults)
|
||||
setDbContainer()
|
||||
BGManager.shared.register()
|
||||
NtfManager.shared.registerCategories()
|
||||
}
|
||||
@@ -38,7 +39,11 @@ struct SimpleXApp: App {
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
initializeChat()
|
||||
do {
|
||||
try initializeChat(start: v3DBMigrationDefault.get().startChat)
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
logger.debug("scenePhase \(String(describing: scenePhase))")
|
||||
@@ -51,7 +56,7 @@ struct SimpleXApp: App {
|
||||
}
|
||||
doAuthenticate = false
|
||||
case .active:
|
||||
setAppState(.active)
|
||||
appStateGroupDefault.set(.active)
|
||||
apiSetAppPhase(appPhase: .active)
|
||||
doAuthenticate = authenticationExpired()
|
||||
default:
|
||||
@@ -61,12 +66,34 @@ struct SimpleXApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
private func setDbContainer() {
|
||||
// Uncomment and run once to open DB in app documents folder:
|
||||
// dbContainerGroupDefault.set(.documents)
|
||||
// v3DBMigrationDefault.set(.offer)
|
||||
// to create database in app documents folder also uncomment:
|
||||
// let legacyDatabase = true
|
||||
let legacyDatabase = hasLegacyDatabase()
|
||||
if legacyDatabase, case .documents = dbContainerGroupDefault.get() {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
switch v3DBMigrationDefault.get() {
|
||||
case .migrated: ()
|
||||
default: v3DBMigrationDefault.set(.offer)
|
||||
}
|
||||
logger.debug("SimpleXApp init: using legacy DB in documents folder: \(getAppDatabasePath(), privacy: .public)*.db")
|
||||
} else {
|
||||
dbContainerGroupDefault.set(.group)
|
||||
v3DBMigrationDefault.set(.ready)
|
||||
logger.debug("SimpleXApp init: using DB in app group container: \(getAppDatabasePath(), privacy: .public)*.db")
|
||||
logger.debug("SimpleXApp init: legacy DB\(legacyDatabase ? "" : " not", privacy: .public) present")
|
||||
}
|
||||
}
|
||||
|
||||
private func pauseApp() {
|
||||
setAppState(.pausing)
|
||||
appStateGroupDefault.set(.pausing)
|
||||
apiSetAppPhase(appPhase: .paused)
|
||||
let endTask = beginBGTask {
|
||||
if getAppState() != .active {
|
||||
setAppState(.suspending)
|
||||
if appStateGroupDefault.get() != .active {
|
||||
appStateGroupDefault.set(.suspending)
|
||||
apiSetAppPhase(appPhase: .suspended)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,9 +210,8 @@ struct ComposeView: View {
|
||||
allowedContentTypes: [.data],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
if case .success = result {
|
||||
if case let .success(files) = result, let fileURL = files.first {
|
||||
do {
|
||||
let fileURL: URL = try result.get().first!
|
||||
var fileSize: Int? = nil
|
||||
if fileURL.startAccessingSecurityScopedResource() {
|
||||
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
||||
@@ -392,17 +391,12 @@ struct ComposeView: View {
|
||||
}
|
||||
|
||||
private func parseMessage(_ msg: String) -> URL? {
|
||||
do {
|
||||
let parsedMsg = try apiParseMarkdown(text: msg)
|
||||
let uri = parsedMsg?.first(where: { ft in
|
||||
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
||||
})
|
||||
if let uri = uri { return URL(string: uri.text) }
|
||||
else { return nil }
|
||||
} catch {
|
||||
logger.error("apiParseMarkdown error: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
let parsedMsg = parseSimpleXMarkdown(msg)
|
||||
let uri = parsedMsg?.first(where: { ft in
|
||||
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
||||
})
|
||||
if let uri = uri { return URL(string: uri.text) }
|
||||
else { return nil }
|
||||
}
|
||||
|
||||
private func isSimplexLink(_ link: String) -> Bool {
|
||||
|
||||
@@ -23,6 +23,7 @@ struct ChatListView: View {
|
||||
ForEach(filteredChats()) { chat in
|
||||
ChatListNavLink(chat: chat, showChatInfo: $showChatInfo)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
@@ -46,7 +47,11 @@ struct ChatListView: View {
|
||||
SettingsButton()
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
NewChatButton()
|
||||
switch chatModel.chatRunning {
|
||||
case .some(true): NewChatButton()
|
||||
case .some(false): chatStoppedIcon()
|
||||
case .none: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +79,17 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func chatStoppedIcon() -> some View {
|
||||
Button {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Chat is stopped",
|
||||
message: "You can start chat via app Settings / Database or by restarting the app"
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
|
||||
65
apps/ios/Shared/Views/Database/ChatArchiveView.swift
Normal file
65
apps/ios/Shared/Views/Database/ChatArchiveView.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// ChatArchiveView.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 23/06/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatArchiveView: View {
|
||||
var archiveName: String
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
@State private var showDeleteAlert = false
|
||||
|
||||
var body: some View {
|
||||
let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName)
|
||||
let fileTs = chatArchiveTimeDefault.get()
|
||||
List {
|
||||
Section {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Button {
|
||||
showShareSheet(items: [fileUrl])
|
||||
} label: {
|
||||
Text("Save archive")
|
||||
}
|
||||
}
|
||||
settingsRow("trash") {
|
||||
Button {
|
||||
showDeleteAlert = true
|
||||
} label: {
|
||||
Text("Delete archive").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Chat archive")
|
||||
} footer: {
|
||||
Text("Created on \(fileTs)")
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showDeleteAlert) {
|
||||
Alert(
|
||||
title: Text("Delete chat archive?"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: fileUrl.path)
|
||||
chatArchiveName = nil
|
||||
chatArchiveTime = 0
|
||||
} catch let error {
|
||||
logger.error("removeItem error \(String(describing: error))")
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatArchiveView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatArchiveView(archiveName: "")
|
||||
}
|
||||
}
|
||||
331
apps/ios/Shared/Views/Database/DatabaseView.swift
Normal file
331
apps/ios/Shared/Views/Database/DatabaseView.swift
Normal file
@@ -0,0 +1,331 @@
|
||||
//
|
||||
// DatabaseView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 19/06/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
enum DatabaseAlert: Identifiable {
|
||||
case stopChat
|
||||
case importArchive
|
||||
case archiveImported
|
||||
case deleteChat
|
||||
case chatDeleted
|
||||
case deleteLegacyDatabase
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .stopChat: return "stopChat"
|
||||
case .importArchive: return "importArchive"
|
||||
case .archiveImported: return "archiveImported"
|
||||
case .deleteChat: return "deleteChat"
|
||||
case .chatDeleted: return "chatDeleted"
|
||||
case .deleteLegacyDatabase: return "deleteLegacyDatabase"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Binding var showSettings: Bool
|
||||
@State private var runChat = false
|
||||
@State private var alert: DatabaseAlert? = nil
|
||||
@State private var showFileImporter = false
|
||||
@State private var importedArchivePath: URL?
|
||||
@State private var progressIndicator = false
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
@State private var dbContainer = dbContainerGroupDefault.get()
|
||||
@State private var legacyDatabase = hasLegacyDatabase()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
chatDatabaseView()
|
||||
if progressIndicator {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func chatDatabaseView() -> some View {
|
||||
List {
|
||||
let stopped = m.chatRunning == false
|
||||
Section {
|
||||
settingsRow(
|
||||
stopped ? "exclamationmark.octagon.fill" : "play.fill",
|
||||
color: stopped ? .red : .green
|
||||
) {
|
||||
Toggle(
|
||||
stopped ? "Chat is stopped" : "Chat is running",
|
||||
isOn: $runChat
|
||||
)
|
||||
.onChange(of: runChat) { _ in
|
||||
if (runChat) {
|
||||
startChat()
|
||||
} else {
|
||||
alert = .stopChat
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Run chat")
|
||||
} footer: {
|
||||
if case .documents = dbContainer {
|
||||
Text("Database will be migrated when the app restarts")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow("square.and.arrow.up") {
|
||||
Button {
|
||||
exportArchive()
|
||||
} label: {
|
||||
Text("Export database")
|
||||
}
|
||||
}
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Button(role: .destructive) {
|
||||
showFileImporter = true
|
||||
} label: {
|
||||
Text("Import database")
|
||||
}
|
||||
}
|
||||
if let archiveName = chatArchiveName {
|
||||
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
|
||||
? "Old database archive"
|
||||
: "New database archive"
|
||||
settingsRow("archivebox") {
|
||||
NavigationLink {
|
||||
ChatArchiveView(archiveName: archiveName)
|
||||
.navigationTitle(title)
|
||||
} label: {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("trash.slash") {
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteChat
|
||||
} label: {
|
||||
Text("Delete database")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Chat database")
|
||||
} footer: {
|
||||
Text(
|
||||
stopped
|
||||
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
|
||||
: "Stop chat to enable database actions"
|
||||
)
|
||||
}
|
||||
.disabled(!stopped)
|
||||
|
||||
if case .group = dbContainer, legacyDatabase {
|
||||
Section("Old database") {
|
||||
settingsRow("trash") {
|
||||
Button {
|
||||
alert = .deleteLegacyDatabase
|
||||
} label: {
|
||||
Text("Delete old database")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { runChat = m.chatRunning ?? true }
|
||||
.alert(item: $alert) { item in databaseAlert(item) }
|
||||
.fileImporter(
|
||||
isPresented: $showFileImporter,
|
||||
allowedContentTypes: [.zip],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
if case let .success(files) = result, let fileURL = files.first {
|
||||
importedArchivePath = fileURL
|
||||
alert = .importArchive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func databaseAlert(_ alertItem: DatabaseAlert) -> Alert {
|
||||
switch alertItem {
|
||||
case .stopChat:
|
||||
return Alert(
|
||||
title: Text("Stop chat?"),
|
||||
message: Text("Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped."),
|
||||
primaryButton: .destructive(Text("Stop")) {
|
||||
stopChat()
|
||||
},
|
||||
secondaryButton: .cancel {
|
||||
withAnimation { runChat = true }
|
||||
}
|
||||
)
|
||||
case .importArchive:
|
||||
if let fileURL = importedArchivePath {
|
||||
return Alert(
|
||||
title: Text("Import chat database?"),
|
||||
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.\n") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Import")) {
|
||||
importArchive(fileURL)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
} else {
|
||||
return Alert(title: Text("Error: no database file"))
|
||||
}
|
||||
case .archiveImported:
|
||||
return Alert(
|
||||
title: Text("Chat database imported"),
|
||||
message: Text("Restart the app to use imported chat database"),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
|
||||
case .deleteChat:
|
||||
return Alert(
|
||||
title: Text("Delete chat profile?"),
|
||||
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deleteChat()
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .chatDeleted:
|
||||
return Alert(
|
||||
title: Text("Chat database deleted"),
|
||||
message: Text("Restart the app to create a new chat profile"),
|
||||
primaryButton: .default(Text("Ok")),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .deleteLegacyDatabase:
|
||||
return Alert(
|
||||
title: Text("Delete old database?"),
|
||||
message: Text("The old database was not removed during the migration, it can be deleted."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deleteLegacyDatabase()
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text("\(error)"))
|
||||
}
|
||||
}
|
||||
|
||||
private func stopChat() {
|
||||
Task {
|
||||
do {
|
||||
try await apiStopChat()
|
||||
await MainActor.run { m.chatRunning = false }
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
runChat = true
|
||||
alert = .error(title: "Error stopping chat", error: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let archivePath = try await exportChatArchive()
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func importArchive(_ archivePath: URL) {
|
||||
if archivePath.startAccessingSecurityScopedResource() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
try await apiImportArchive(config: config)
|
||||
await operationEnded(.archiveImported)
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)))
|
||||
}
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
} else {
|
||||
alert = .error(title: "Error accessing database file")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteChat() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
await operationEnded(.chatDeleted)
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteLegacyDatabase() {
|
||||
if removeLegacyDatabaseAndFiles() {
|
||||
legacyDatabase = false
|
||||
} else {
|
||||
alert = .error(title: "Error deleting old database")
|
||||
}
|
||||
}
|
||||
|
||||
private func operationEnded(_ dbAlert: DatabaseAlert) async {
|
||||
await MainActor.run {
|
||||
m.chatDbChanged = true
|
||||
progressIndicator = false
|
||||
alert = dbAlert
|
||||
}
|
||||
}
|
||||
|
||||
private func startChat() {
|
||||
if m.chatDbChanged {
|
||||
showSettings = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
_ = try apiStartChat()
|
||||
runChat = true
|
||||
m.chatRunning = true
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
} catch let error {
|
||||
runChat = false
|
||||
alert = .error(title: "Error starting chat", error: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseView(showSettings: Binding.constant(false))
|
||||
}
|
||||
}
|
||||
248
apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift
Normal file
248
apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// MigrateToGroupView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 20/06/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
enum V3DBMigrationState: String {
|
||||
case offer
|
||||
case postponed
|
||||
case exporting
|
||||
case export_error
|
||||
case exported
|
||||
case migrating
|
||||
case migration_error
|
||||
case migrated
|
||||
case ready
|
||||
|
||||
var startChat: Bool {
|
||||
switch self {
|
||||
case .postponed: return true
|
||||
case .ready: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let v3DBMigrationDefault = EnumDefault<V3DBMigrationState>(
|
||||
defaults: UserDefaults.standard,
|
||||
forKey: DEFAULT_CHAT_V3_DB_MIGRATION,
|
||||
withDefault: .offer
|
||||
)
|
||||
|
||||
struct MigrateToAppGroupView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var v3DBMigration = v3DBMigrationDefault.get()
|
||||
@State private var migrationError = ""
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
Text("Database migration").font(.largeTitle)
|
||||
|
||||
switch v3DBMigration {
|
||||
case .offer:
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("To support instant push notifications the chat database has to be migrated.")
|
||||
Text("If you need to use the chat now tap **Skip** below (you will be offered to migrate the database when you restart the app).")
|
||||
}
|
||||
.padding(.top, 56)
|
||||
center {
|
||||
Button {
|
||||
migrateDatabaseToV3()
|
||||
} label: {
|
||||
Text("Start migration")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
skipMigration()
|
||||
case .exporting:
|
||||
center {
|
||||
ProgressView(value: 0.33)
|
||||
Text("Exporting database archive...")
|
||||
}
|
||||
migrationProgress()
|
||||
case .export_error:
|
||||
migrationFailed().padding(.top, 56)
|
||||
center {
|
||||
Text("Export error:").font(.headline)
|
||||
Text(migrationError)
|
||||
}
|
||||
skipMigration()
|
||||
case .exported:
|
||||
center {
|
||||
Text("Exported database archive.")
|
||||
}
|
||||
case .migrating:
|
||||
center {
|
||||
ProgressView(value: 0.67)
|
||||
Text("Migrating database archive...")
|
||||
}
|
||||
migrationProgress()
|
||||
case .migration_error:
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
migrationFailed()
|
||||
Text("The created archive is available via app Settings / Database / Old database archive.")
|
||||
}
|
||||
.padding(.top, 56)
|
||||
center {
|
||||
Text("Migration error:").font(.headline)
|
||||
Text(migrationError)
|
||||
}
|
||||
skipMigration()
|
||||
case .migrated:
|
||||
center {
|
||||
ProgressView(value: 1.0)
|
||||
Text("Migration is completed")
|
||||
}
|
||||
VStack {
|
||||
Spacer()
|
||||
Spacer()
|
||||
Spacer()
|
||||
Button {
|
||||
do {
|
||||
resetChatCtrl()
|
||||
try initializeChat(start: true)
|
||||
setV3DBMigration(.ready)
|
||||
} catch let error {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
setV3DBMigration(.migration_error)
|
||||
migrationError = "Error starting chat: \(responseError(error))"
|
||||
}
|
||||
deleteOldArchive()
|
||||
} label: {
|
||||
Text("Start using chat")
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
default:
|
||||
Spacer()
|
||||
Text("Unexpected migration state")
|
||||
Text("\(v3DBMigration.rawValue)")
|
||||
Spacer()
|
||||
skipMigration()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func center<Content>(@ViewBuilder c: @escaping () -> Content) -> some View where Content: View {
|
||||
VStack(alignment: .leading, spacing: 8) { c() }
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private func migrationProgress() -> some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
ProgressView().scaleEffect(2)
|
||||
Spacer()
|
||||
Spacer()
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func migrationFailed() -> some View {
|
||||
Text("Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).")
|
||||
}
|
||||
|
||||
private func skipMigration() -> some View {
|
||||
ZStack {
|
||||
Button {
|
||||
setV3DBMigration(.postponed)
|
||||
do {
|
||||
try startChat()
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
} label: {
|
||||
Text("Skip and start using chat")
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
||||
}
|
||||
|
||||
private func setV3DBMigration(_ value: V3DBMigrationState) {
|
||||
v3DBMigration = value
|
||||
v3DBMigrationDefault.set(value)
|
||||
}
|
||||
|
||||
func migrateDatabaseToV3() {
|
||||
setV3DBMigration(.exporting)
|
||||
let archiveTime = Date.now
|
||||
let archiveName = "simplex-chat.\(archiveTime.ISO8601Format()).zip"
|
||||
chatArchiveTime = archiveTime.timeIntervalSince1970
|
||||
chatArchiveName = archiveName
|
||||
let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
|
||||
Task {
|
||||
do {
|
||||
try await apiExportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.exported) }
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
setV3DBMigration(.export_error)
|
||||
migrationError = responseError(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
await MainActor.run { setV3DBMigration(.migrating) }
|
||||
dbContainerGroupDefault.set(.group)
|
||||
resetChatCtrl()
|
||||
try initializeChat(start: false)
|
||||
try await apiImportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.migrated) }
|
||||
} catch let error {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
await MainActor.run {
|
||||
setV3DBMigration(.migration_error)
|
||||
migrationError = responseError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exportChatArchive() async throws -> URL {
|
||||
let archiveTime = Date.now
|
||||
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
||||
let archiveName = "simplex-chat.\(ts).zip"
|
||||
let archivePath = getDocumentsDirectory().appendingPathComponent(archiveName)
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
try await apiExportArchive(config: config)
|
||||
deleteOldArchive()
|
||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
chatArchiveTimeDefault.set(archiveTime)
|
||||
return archivePath
|
||||
}
|
||||
|
||||
func deleteOldArchive() {
|
||||
let d = UserDefaults.standard
|
||||
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
|
||||
d.set(nil, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
d.set(0, forKey: DEFAULT_CHAT_ARCHIVE_TIME)
|
||||
} catch let error {
|
||||
logger.error("removeItem error \(String(describing: error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MigrateToGroupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MigrateToAppGroupView()
|
||||
}
|
||||
}
|
||||
@@ -95,11 +95,11 @@ struct CreateProfile: View {
|
||||
)
|
||||
do {
|
||||
m.currentUser = try apiCreateActiveUser(profile)
|
||||
startChat()
|
||||
try startChat()
|
||||
withAnimation { m.onboardingStage = .step3_MakeConnection }
|
||||
|
||||
} catch {
|
||||
fatalError("Failed to create user: \(error)")
|
||||
fatalError("Failed to create user or start chat: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,14 @@ struct MakeConnection: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SettingsButton().padding(.bottom, 1)
|
||||
HStack {
|
||||
SettingsButton()
|
||||
if m.chatRunning == false {
|
||||
Spacer()
|
||||
chatStoppedIcon()
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 1)
|
||||
|
||||
if let user = m.currentUser {
|
||||
Text("Welcome \(user.displayName)!")
|
||||
@@ -66,6 +73,7 @@ struct MakeConnection: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(m.chatRunning != true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -24,6 +24,9 @@ let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
|
||||
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
||||
let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
|
||||
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
||||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_LA_NOTICE: false,
|
||||
@@ -34,11 +37,14 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_WEBRTC_POLICY_RELAY: true,
|
||||
DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
|
||||
DEFAULT_EXPERIMENTAL_CALLS: false
|
||||
DEFAULT_EXPERIMENTAL_CALLS: false,
|
||||
DEFAULT_CHAT_V3_DB_MIGRATION: "offer"
|
||||
]
|
||||
|
||||
private var indent: CGFloat = 36
|
||||
|
||||
let chatArchiveTimeDefault = DateDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CHAT_ARCHIVE_TIME)
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -52,7 +58,7 @@ struct SettingsView: View {
|
||||
var body: some View {
|
||||
let user: User = chatModel.currentUser!
|
||||
|
||||
return NavigationView {
|
||||
NavigationView {
|
||||
List {
|
||||
Section("You") {
|
||||
NavigationLink {
|
||||
@@ -62,12 +68,30 @@ struct SettingsView: View {
|
||||
ProfilePreview(profileOf: user)
|
||||
.padding(.leading, -8)
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
UserAddress()
|
||||
.navigationTitle("Your chat address")
|
||||
} label: {
|
||||
settingsRow("qrcode") { Text("Your SimpleX contact address") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
DatabaseView(showSettings: $showSettings)
|
||||
.navigationTitle("Your chat database")
|
||||
} label: {
|
||||
settingsRow("internaldrive") {
|
||||
HStack {
|
||||
Text("Database export & import")
|
||||
Spacer()
|
||||
if chatModel.chatRunning == false {
|
||||
Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Settings") {
|
||||
@@ -76,7 +100,9 @@ struct SettingsView: View {
|
||||
CallSettings()
|
||||
.navigationTitle("Your calls")
|
||||
} label: {
|
||||
settingsRow("video") { Text("Audio & video calls") }
|
||||
settingsRow("video") {
|
||||
Text("Audio & video calls")
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigationLink {
|
||||
@@ -95,6 +121,7 @@ struct SettingsView: View {
|
||||
settingsRow("server.rack") { Text("SMP servers") }
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
Section("Help") {
|
||||
NavigationLink {
|
||||
@@ -128,6 +155,7 @@ struct SettingsView: View {
|
||||
Text("Chat with the developers")
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") }
|
||||
}
|
||||
|
||||
@@ -137,6 +165,7 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
settingsRow("terminal") { Text("Chat console") }
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
ZStack(alignment: .leading) {
|
||||
Image(colorScheme == .dark ? "github_light" : "github")
|
||||
.resizable()
|
||||
@@ -156,6 +185,7 @@ struct SettingsView: View {
|
||||
notificationsIcon()
|
||||
notificationsToggle(token)
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
}
|
||||
Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))")
|
||||
}
|
||||
@@ -261,9 +291,9 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func settingsRow<Content : View>(_ icon: String, content: @escaping () -> Content) -> some View {
|
||||
func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.secondary)
|
||||
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(color)
|
||||
content().padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
logger.debug("NotificationService.didReceive")
|
||||
print("*** userInfo", request.content.userInfo)
|
||||
let appState = getAppState()
|
||||
let appState = appStateGroupDefault.get()
|
||||
if appState.running {
|
||||
print("userInfo", request.content.userInfo)
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
@@ -33,9 +33,10 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let encNtfInfo = ntfData["message"] as? String,
|
||||
let _ = startChat() {
|
||||
apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)
|
||||
let content = receiveMessages()
|
||||
contentHandler (content)
|
||||
return
|
||||
if let content = receiveMessages() {
|
||||
contentHandler(content)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
@@ -68,6 +69,7 @@ func startChat() -> User? {
|
||||
do {
|
||||
try apiStartChat()
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
return user
|
||||
} catch {
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
@@ -78,40 +80,43 @@ func startChat() -> User? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func receiveMessages() -> UNNotificationContent {
|
||||
func receiveMessages() -> UNNotificationContent? {
|
||||
logger.debug("NotificationService receiveMessages started")
|
||||
while true {
|
||||
let res = chatResponse(chat_recv_msg(getChatCtrl())!)
|
||||
logger.debug("NotificationService receiveMessages: \(res.responseType)")
|
||||
switch res {
|
||||
// case let .newContactConnection(connection):
|
||||
// case let .contactConnectionDeleted(connection):
|
||||
case let .contactConnected(contact):
|
||||
return createContactConnectedNtf(contact)
|
||||
// case let .contactConnecting(contact):
|
||||
// TODO profile update
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
return createContactRequestNtf(contactRequest)
|
||||
// case let .contactUpdated(toContact):
|
||||
// TODO profile updated
|
||||
case let .newChatItem(aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
return createMessageReceivedNtf(cInfo, cItem)
|
||||
// case let .chatItemUpdated(aChatItem):
|
||||
// TODO message updated
|
||||
// let cInfo = aChatItem.chatInfo
|
||||
// let cItem = aChatItem.chatItem
|
||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
// case let .chatItemDeleted(_, toChatItem):
|
||||
// TODO message updated
|
||||
// case let .rcvFileComplete(aChatItem):
|
||||
// TODO file received?
|
||||
// let cInfo = aChatItem.chatInfo
|
||||
// let cItem = aChatItem.chatItem
|
||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
default:
|
||||
logger.debug("NotificationService ignored event: \(res.responseType)")
|
||||
if let res = recvSimpleXMsg() {
|
||||
logger.debug("NotificationService receiveMessages: \(res.responseType)")
|
||||
switch res {
|
||||
// case let .newContactConnection(connection):
|
||||
// case let .contactConnectionDeleted(connection):
|
||||
case let .contactConnected(contact):
|
||||
return createContactConnectedNtf(contact)
|
||||
// case let .contactConnecting(contact):
|
||||
// TODO profile update
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
return createContactRequestNtf(contactRequest)
|
||||
// case let .contactUpdated(toContact):
|
||||
// TODO profile updated
|
||||
case let .newChatItem(aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
return createMessageReceivedNtf(cInfo, cItem)
|
||||
// case let .chatItemUpdated(aChatItem):
|
||||
// TODO message updated
|
||||
// let cInfo = aChatItem.chatInfo
|
||||
// let cItem = aChatItem.chatItem
|
||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
// case let .chatItemDeleted(_, toChatItem):
|
||||
// TODO message updated
|
||||
// case let .rcvFileComplete(aChatItem):
|
||||
// TODO file received?
|
||||
// let cInfo = aChatItem.chatInfo
|
||||
// let cItem = aChatItem.chatItem
|
||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
default:
|
||||
logger.debug("NotificationService ignored event: \(res.responseType)")
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,9 +152,9 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
||||
func apiGetNtfMessage(nonce: String, encNtfInfo: String) {
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
|
||||
print(connEntity)
|
||||
print(msgTs)
|
||||
print(ntfMessages)
|
||||
if let connEntity = connEntity { print("connEntity", connEntity) }
|
||||
if let msgTs = msgTs { print("msgTs", msgTs) }
|
||||
print("ntfMessages", ntfMessages)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
|
||||
5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */; };
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
|
||||
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
|
||||
@@ -40,11 +41,6 @@
|
||||
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
|
||||
5C69D5B22852379F009B27A4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; };
|
||||
5C69D5B32852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; };
|
||||
5C69D5B42852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; };
|
||||
5C69D5B52852379F009B27A4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; };
|
||||
5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; };
|
||||
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||
@@ -102,6 +98,13 @@
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; };
|
||||
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; };
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
5CFA59CA2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */; };
|
||||
5CFA59CB2864464A00863A68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C62864464A00863A68 /* libffi.a */; };
|
||||
5CFA59CC2864464A00863A68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C72864464A00863A68 /* libgmpxx.a */; };
|
||||
5CFA59CD2864464A00863A68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C82864464A00863A68 /* libgmp.a */; };
|
||||
5CFA59CE2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */; };
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
|
||||
@@ -198,6 +201,7 @@
|
||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
|
||||
5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.swift; sourceTree = "<group>"; };
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
|
||||
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
|
||||
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
|
||||
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
|
||||
@@ -209,11 +213,6 @@
|
||||
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
|
||||
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
||||
5C6F2A3728522C9A00103588 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C6F2A3828522C9A00103588 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C6F2A3928522C9A00103588 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a"; sourceTree = "<group>"; };
|
||||
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||
@@ -269,6 +268,13 @@
|
||||
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
|
||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CFA59C62864464A00863A68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CFA59C72864464A00863A68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CFA59C82864464A00863A68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a"; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
|
||||
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
|
||||
@@ -313,13 +319,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */,
|
||||
5CFA59CE2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a in Frameworks */,
|
||||
5CFA59CA2864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a in Frameworks */,
|
||||
5CFA59CC2864464A00863A68 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5C69D5B32852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */,
|
||||
5C69D5B22852379F009B27A4 /* libgmp.a in Frameworks */,
|
||||
5C69D5B42852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C69D5B52852379F009B27A4 /* libffi.a in Frameworks */,
|
||||
5CFA59CD2864464A00863A68 /* libgmp.a in Frameworks */,
|
||||
5CFA59CB2864464A00863A68 /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -349,6 +355,7 @@
|
||||
5C5F4AC227A5E9AF00B51EF1 /* Chat */,
|
||||
5CB9250B27A942F300ACCCDD /* ChatList */,
|
||||
5CB924DD27A8622200ACCCDD /* NewChat */,
|
||||
5CFA59C22860B04D00863A68 /* Database */,
|
||||
5CB924DF27A8678B00ACCCDD /* UserSettings */,
|
||||
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
|
||||
);
|
||||
@@ -372,11 +379,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C6F2A3828522C9A00103588 /* libffi.a */,
|
||||
5C6F2A3728522C9A00103588 /* libgmp.a */,
|
||||
5C6F2A3928522C9A00103588 /* libgmpxx.a */,
|
||||
5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */,
|
||||
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */,
|
||||
5CFA59C62864464A00863A68 /* libffi.a */,
|
||||
5CFA59C82864464A00863A68 /* libgmp.a */,
|
||||
5CFA59C72864464A00863A68 /* libgmpxx.a */,
|
||||
5CFA59C52864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl-ghc8.10.7.a */,
|
||||
5CFA59C92864464A00863A68 /* libHSsimplex-chat-2.2.0-40J3CUJkXRXAsiR552cpHl.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -584,6 +591,16 @@
|
||||
path = ComposeMessage;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CFA59C22860B04D00863A68 /* Database */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */,
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */,
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */,
|
||||
);
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -685,7 +702,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1330;
|
||||
LastUpgradeCheck = 1330;
|
||||
LastUpgradeCheck = 1340;
|
||||
ORGANIZATIONNAME = "SimpleX Chat";
|
||||
TargetAttributes = {
|
||||
5CA059C9279559F40002BEB4 = {
|
||||
@@ -713,6 +730,7 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
ru,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 5CA059BD279559F40002BEB4;
|
||||
packageReferences = (
|
||||
@@ -786,7 +804,9 @@
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */,
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
|
||||
@@ -825,6 +845,7 @@
|
||||
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */,
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
||||
5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */,
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
|
||||
@@ -948,6 +969,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
@@ -1008,6 +1030,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
@@ -1094,7 +1117,6 @@
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@@ -1217,7 +1239,6 @@
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@@ -1284,10 +1305,6 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/ios",
|
||||
@@ -1305,7 +1322,6 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_INCLUDE_PATHS = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -1335,10 +1351,6 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/ios",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
LastUpgradeVersion = "1340"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -12,21 +12,61 @@ private var chatController: chat_ctrl?
|
||||
|
||||
public func getChatCtrl() -> chat_ctrl {
|
||||
if let controller = chatController { return controller }
|
||||
let dataDir = getDocumentsDirectory().path + "/mobile_v1"
|
||||
logger.debug("documents directory \(dataDir)")
|
||||
var cstr = dataDir.cString(using: .utf8)!
|
||||
let dbPath = getAppDatabasePath().path
|
||||
logger.debug("getChatCtrl DB path: \(dbPath)")
|
||||
var cstr = dbPath.cString(using: .utf8)!
|
||||
chatController = chat_init(&cstr)
|
||||
logger.debug("getChatCtrl: chat_init")
|
||||
return chatController!
|
||||
}
|
||||
|
||||
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
return chatResponse(chat_send_cmd(getChatCtrl(), &c))
|
||||
public func resetChatCtrl() {
|
||||
chatController = nil
|
||||
}
|
||||
|
||||
public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
|
||||
let s = String.init(cString: cjson)
|
||||
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
let cjson = chat_send_cmd(getChatCtrl(), &c)!
|
||||
return chatResponse(fromCString(cjson))
|
||||
}
|
||||
|
||||
// in microseconds
|
||||
let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
||||
|
||||
public func recvSimpleXMsg() -> ChatResponse? {
|
||||
if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) {
|
||||
let s = fromCString(cjson)
|
||||
return s == "" ? nil : chatResponse(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
|
||||
var c = s.cString(using: .utf8)!
|
||||
if let cjson = chat_parse_markdown(&c) {
|
||||
if let d = fromCString(cjson).data(using: .utf8) {
|
||||
do {
|
||||
let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d)
|
||||
return r.formattedText
|
||||
} catch {
|
||||
logger.error("parseSimpleXMarkdown jsonDecoder.decode error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
struct ParsedMarkdown: Decodable {
|
||||
var formattedText: [FormattedText]?
|
||||
}
|
||||
|
||||
private func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
|
||||
let s = String.init(cString: c)
|
||||
free(c)
|
||||
return s
|
||||
}
|
||||
|
||||
public func chatResponse(_ s: String) -> ChatResponse {
|
||||
let d = s.data(using: .utf8)!
|
||||
// TODO is there a way to do it without copying the data? e.g:
|
||||
// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson))
|
||||
@@ -46,7 +86,6 @@ public func chatResponse(_ cjson: UnsafeMutablePointer<CChar>) -> ChatResponse {
|
||||
}
|
||||
json = prettyJSON(j)
|
||||
}
|
||||
free(cjson)
|
||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ public enum ChatCommand {
|
||||
case apiStopChat
|
||||
case apiSetAppPhase(appPhase: AgentPhase)
|
||||
case setFilesFolder(filesFolder: String)
|
||||
case apiExportArchive(config: ArchiveConfig)
|
||||
case apiImportArchive(config: ArchiveConfig)
|
||||
case apiDeleteStorage
|
||||
case apiGetChats
|
||||
case apiGetChat(type: ChatType, id: Int64)
|
||||
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
|
||||
@@ -35,7 +38,6 @@ public enum ChatCommand {
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
case apiClearChat(type: ChatType, id: Int64)
|
||||
case apiUpdateProfile(profile: Profile)
|
||||
case apiParseMarkdown(text: String)
|
||||
case createMyAddress
|
||||
case deleteMyAddress
|
||||
case showMyAddress
|
||||
@@ -62,6 +64,9 @@ public enum ChatCommand {
|
||||
case .apiStopChat: return "/_stop"
|
||||
case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase)"
|
||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||
case .apiDeleteStorage: return "/_db delete"
|
||||
case .apiGetChats: return "/_get chats pcc=on"
|
||||
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
|
||||
case let .apiSendMessage(type, id, file, quotedItemId, mc):
|
||||
@@ -81,7 +86,6 @@ public enum ChatCommand {
|
||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
|
||||
case let .apiParseMarkdown(text): return "/_parse \(text)"
|
||||
case .createMyAddress: return "/address"
|
||||
case .deleteMyAddress: return "/delete_address"
|
||||
case .showMyAddress: return "/show_address"
|
||||
@@ -110,6 +114,9 @@ public enum ChatCommand {
|
||||
case .apiStopChat: return "apiStopChat"
|
||||
case .apiSetAppPhase: return "apiSetAppPhase"
|
||||
case .setFilesFolder: return "setFilesFolder"
|
||||
case .apiExportArchive: return "apiExportArchive"
|
||||
case .apiImportArchive: return "apiImportArchive"
|
||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||
case .apiGetChats: return "apiGetChats"
|
||||
case .apiGetChat: return "apiGetChat"
|
||||
case .apiSendMessage: return "apiSendMessage"
|
||||
@@ -127,7 +134,6 @@ public enum ChatCommand {
|
||||
case .apiDeleteChat: return "apiDeleteChat"
|
||||
case .apiClearChat: return "apiClearChat"
|
||||
case .apiUpdateProfile: return "apiUpdateProfile"
|
||||
case .apiParseMarkdown: return "apiParseMarkdown"
|
||||
case .createMyAddress: return "createMyAddress"
|
||||
case .deleteMyAddress: return "deleteMyAddress"
|
||||
case .showMyAddress: return "showMyAddress"
|
||||
@@ -178,7 +184,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case chatCleared(chatInfo: ChatInfo)
|
||||
case userProfileNoChange
|
||||
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case apiParsedMarkdown(formattedText: [FormattedText]?)
|
||||
case userContactLink(connReqContact: String)
|
||||
case userContactLinkCreated(connReqContact: String)
|
||||
case userContactLinkDeleted
|
||||
@@ -243,7 +248,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .chatCleared: return "chatCleared"
|
||||
case .userProfileNoChange: return "userProfileNoChange"
|
||||
case .userProfileUpdated: return "userProfileUpdated"
|
||||
case .apiParsedMarkdown: return "apiParsedMarkdown"
|
||||
case .userContactLink: return "userContactLink"
|
||||
case .userContactLinkCreated: return "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
||||
@@ -309,7 +313,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .chatCleared(chatInfo): return String(describing: chatInfo)
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
|
||||
case let .apiParsedMarkdown(formattedText): return String(describing: formattedText)
|
||||
case let .userContactLink(connReq): return connReq
|
||||
case let .userContactLinkCreated(connReq): return connReq
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
@@ -370,6 +373,16 @@ public enum AgentPhase: String, Codable {
|
||||
case suspended = "SUSPENDED"
|
||||
}
|
||||
|
||||
public struct ArchiveConfig: Encodable {
|
||||
var archivePath: String
|
||||
var disableCompression: Bool?
|
||||
|
||||
public init(archivePath: String, disableCompression: Bool? = nil) {
|
||||
self.archivePath = archivePath
|
||||
self.disableCompression = disableCompression
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeJSON<T: Decodable>(_ json: String) -> T? {
|
||||
if let data = json.data(using: .utf8) {
|
||||
return try? jsonDecoder.decode(T.self, from: data)
|
||||
|
||||
@@ -10,12 +10,12 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
|
||||
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
|
||||
|
||||
let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||
|
||||
func getGroupDefaults() -> UserDefaults? {
|
||||
UserDefaults(suiteName: APP_GROUP_NAME)
|
||||
}
|
||||
public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
|
||||
|
||||
public enum AppState: String {
|
||||
case active
|
||||
@@ -42,18 +42,66 @@ public enum AppState: String {
|
||||
}
|
||||
}
|
||||
|
||||
public func setAppState(_ state: AppState) {
|
||||
if let defaults = getGroupDefaults() {
|
||||
defaults.set(state.rawValue, forKey: GROUP_DEFAULT_APP_STATE)
|
||||
public enum DBContainer: String {
|
||||
case documents
|
||||
case group
|
||||
}
|
||||
|
||||
public let appStateGroupDefault = EnumDefault<AppState>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_APP_STATE,
|
||||
withDefault: .active
|
||||
)
|
||||
|
||||
public let dbContainerGroupDefault = EnumDefault<DBContainer>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_DB_CONTAINER,
|
||||
withDefault: .documents
|
||||
)
|
||||
|
||||
public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START)
|
||||
|
||||
public class DateDefault {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
|
||||
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String) {
|
||||
self.defaults = defaults
|
||||
self.key = forKey
|
||||
}
|
||||
|
||||
public func get() -> Date {
|
||||
let ts = defaults.double(forKey: key)
|
||||
return Date(timeIntervalSince1970: ts)
|
||||
}
|
||||
|
||||
public func set(_ ts: Date) {
|
||||
defaults.set(ts.timeIntervalSince1970, forKey: key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
public func getAppState() -> AppState {
|
||||
if let defaults = getGroupDefaults(),
|
||||
let rawValue = defaults.string(forKey: GROUP_DEFAULT_APP_STATE),
|
||||
let state = AppState(rawValue: rawValue) {
|
||||
return state
|
||||
public class EnumDefault<T: RawRepresentable> where T.RawValue == String {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
var defaultValue: T
|
||||
|
||||
public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) {
|
||||
self.defaults = defaults
|
||||
self.key = forKey
|
||||
self.defaultValue = withDefault
|
||||
}
|
||||
|
||||
public func get() -> T {
|
||||
if let rawValue = defaults.string(forKey: key),
|
||||
let value = T(rawValue: rawValue) {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
public func set(_ value: T) {
|
||||
defaults.set(value.rawValue, forKey: key)
|
||||
defaults.synchronize()
|
||||
}
|
||||
return .active
|
||||
}
|
||||
|
||||
@@ -17,13 +17,55 @@ public let maxImageSize: Int64 = 236700
|
||||
|
||||
public let maxFileSize: Int64 = 8000000
|
||||
|
||||
func getDocumentsDirectory() -> URL {
|
||||
// FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
public func getDocumentsDirectory() -> URL {
|
||||
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
}
|
||||
|
||||
func getGroupContainerDirectory() -> URL {
|
||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP_NAME)!
|
||||
}
|
||||
|
||||
func getAppDirectory() -> URL {
|
||||
dbContainerGroupDefault.get() == .group
|
||||
? getGroupContainerDirectory()
|
||||
: getDocumentsDirectory()
|
||||
// getDocumentsDirectory()
|
||||
}
|
||||
|
||||
let DB_FILE_PREFIX = "simplex_v1"
|
||||
|
||||
func getLegacyDatabasePath() -> URL {
|
||||
getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false)
|
||||
}
|
||||
|
||||
public func getAppDatabasePath() -> URL {
|
||||
dbContainerGroupDefault.get() == .group
|
||||
? getGroupContainerDirectory().appendingPathComponent(DB_FILE_PREFIX, isDirectory: false)
|
||||
: getLegacyDatabasePath()
|
||||
// getLegacyDatabasePath()
|
||||
}
|
||||
|
||||
public func hasLegacyDatabase() -> Bool {
|
||||
let dbPath = getLegacyDatabasePath()
|
||||
let fm = FileManager.default
|
||||
return fm.isReadableFile(atPath: dbPath.path + "_agent.db") &&
|
||||
fm.isReadableFile(atPath: dbPath.path + "_chat.db")
|
||||
}
|
||||
|
||||
public func removeLegacyDatabaseAndFiles() -> Bool {
|
||||
let dbPath = getLegacyDatabasePath()
|
||||
let appFiles = getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
|
||||
let fm = FileManager.default
|
||||
let r1 = nil != (try? fm.removeItem(atPath: dbPath.path + "_agent.db"))
|
||||
let r2 = nil != (try? fm.removeItem(atPath: dbPath.path + "_chat.db"))
|
||||
try? fm.removeItem(atPath: dbPath.path + "_agent.db.bak")
|
||||
try? fm.removeItem(atPath: dbPath.path + "_chat.db.bak")
|
||||
try? fm.removeItem(at: appFiles)
|
||||
return r1 && r2
|
||||
}
|
||||
|
||||
public func getAppFilesDirectory() -> URL {
|
||||
getDocumentsDirectory().appendingPathComponent("app_files", isDirectory: true)
|
||||
getAppDirectory().appendingPathComponent("app_files", isDirectory: true)
|
||||
}
|
||||
|
||||
func getAppFilePath(_ fileName: String) -> URL {
|
||||
@@ -96,8 +138,9 @@ private func saveFile(_ data: Data, _ fileName: String) -> String? {
|
||||
|
||||
private func uniqueCombine(_ fileName: String) -> String {
|
||||
func tryCombine(_ fileName: String, _ n: Int) -> String {
|
||||
let name = fileName.deletingPathExtension
|
||||
let ext = fileName.pathExtension
|
||||
let ns = fileName as NSString
|
||||
let name = ns.deletingPathExtension
|
||||
let ext = ns.pathExtension
|
||||
let suffix = (n == 0) ? "" : "_\(n)"
|
||||
let f = "\(name)\(suffix).\(ext)"
|
||||
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
|
||||
@@ -105,18 +148,6 @@ private func uniqueCombine(_ fileName: String) -> String {
|
||||
return tryCombine(fileName, 0)
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var ns: NSString {
|
||||
return self as NSString
|
||||
}
|
||||
var pathExtension: String {
|
||||
return ns.pathExtension
|
||||
}
|
||||
var deletingPathExtension: String {
|
||||
return ns.deletingPathExtension
|
||||
}
|
||||
}
|
||||
|
||||
public func removeFile(_ fileName: String) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path)
|
||||
|
||||
@@ -18,3 +18,5 @@ typedef void* chat_ctrl;
|
||||
extern chat_ctrl chat_init(char *path);
|
||||
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctl);
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
|
||||
extern char *chat_parse_markdown(char *str);
|
||||
|
||||
Reference in New Issue
Block a user