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:
Evgeny Poberezkin
2022-06-24 13:52:20 +01:00
committed by GitHub
parent 4d9e446489
commit 6a2f2a512f
21 changed files with 1097 additions and 206 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)")
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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()

View 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: "")
}
}

View 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))
}
}

View 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()
}
}

View File

@@ -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))")
}
}

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1330"
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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);