2022-06-24 13:52:20 +01:00
//
// D a t a b a s e V i e w . s w i f t
// S i m p l e X ( i O S )
//
// C r e a t e d b y E v g e n y o n 1 9 / 0 6 / 2 0 2 2 .
// C o p y r i g h t © 2 0 2 2 S i m p l e X C h a t . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
import SimpleXChat
enum DatabaseAlert : Identifiable {
case stopChat
2022-09-07 12:49:41 +01:00
case exportProhibited
2022-06-24 13:52:20 +01:00
case importArchive
case archiveImported
2023-05-24 14:22:12 +04:00
case archiveImportedWithErrors ( archiveErrors : [ ArchiveError ] )
2022-06-24 13:52:20 +01:00
case deleteChat
case chatDeleted
case deleteLegacyDatabase
2022-09-19 19:05:29 +04:00
case deleteFilesAndMedia
2022-10-03 16:42:43 +04:00
case setChatItemTTL ( ttl : ChatItemTTL )
2022-06-24 13:52:20 +01:00
case error ( title : LocalizedStringKey , error : String = " " )
var id : String {
switch self {
case . stopChat : return " stopChat "
2022-09-07 12:49:41 +01:00
case . exportProhibited : return " exportProhibited "
2022-06-24 13:52:20 +01:00
case . importArchive : return " importArchive "
case . archiveImported : return " archiveImported "
2023-05-24 14:22:12 +04:00
case . archiveImportedWithErrors : return " archiveImportedWithErrors "
2022-06-24 13:52:20 +01:00
case . deleteChat : return " deleteChat "
case . chatDeleted : return " chatDeleted "
case . deleteLegacyDatabase : return " deleteLegacyDatabase "
2022-09-19 19:05:29 +04:00
case . deleteFilesAndMedia : return " deleteFilesAndMedia "
2022-10-03 16:42:43 +04:00
case . setChatItemTTL : return " setChatItemTTL "
2022-06-24 13:52:20 +01:00
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 ( )
2022-09-08 17:36:16 +01:00
@ State private var useKeychain = storeDBPassphraseGroupDefault . get ( )
2022-09-19 19:05:29 +04:00
@ State private var appFilesCountAndSize : ( Int , Int ) ?
2022-06-24 13:52:20 +01:00
2022-10-03 16:42:43 +04:00
@ State var chatItemTTL : ChatItemTTL
@ State private var currentChatItemTTL : ChatItemTTL = . none
2022-06-24 13:52:20 +01:00
var body : some View {
ZStack {
chatDatabaseView ( )
if progressIndicator {
ProgressView ( ) . scaleEffect ( 2 )
}
}
}
private func chatDatabaseView ( ) -> some View {
List {
let stopped = m . chatRunning = = false
2023-01-20 12:38:38 +00:00
Section {
Picker ( " Delete messages after " , selection : $ chatItemTTL ) {
ForEach ( ChatItemTTL . values ) { ttl in
Text ( ttl . deleteAfterText ) . tag ( ttl )
}
if case . seconds = chatItemTTL {
Text ( chatItemTTL . deleteAfterText ) . tag ( chatItemTTL )
}
}
. frame ( height : 36 )
2023-04-19 15:21:28 +04:00
. disabled ( stopped || progressIndicator )
2023-01-20 12:38:38 +00:00
} header : {
Text ( " Messages " )
} footer : {
Text ( " This setting applies to messages in your current chat profile ** \( m . currentUser ? . displayName ? ? " " ) **. " )
}
2022-06-24 13:52:20 +01:00
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 {
2022-09-07 12:49:41 +01:00
let unencrypted = m . chatDbEncrypted = = false
let color : Color = unencrypted ? . orange : . secondary
2022-09-08 17:36:16 +01:00
settingsRow ( unencrypted ? " lock.open " : useKeychain ? " key " : " lock " , color : color ) {
2022-09-07 12:49:41 +01:00
NavigationLink {
2022-09-08 17:36:16 +01:00
DatabaseEncryptionView ( useKeychain : $ useKeychain )
2022-09-07 12:49:41 +01:00
. navigationTitle ( " Database passphrase " )
2022-06-24 13:52:20 +01:00
} label : {
2022-09-07 12:49:41 +01:00
Text ( " Database passphrase " )
}
}
settingsRow ( " square.and.arrow.up " ) {
Button ( " Export database " ) {
2022-09-22 13:10:25 +01:00
if initialRandomDBPassphraseGroupDefault . get ( ) && ! unencrypted {
2022-09-07 12:49:41 +01:00
alert = . exportProhibited
} else {
exportArchive ( )
}
2022-06-24 13:52:20 +01:00
}
}
settingsRow ( " square.and.arrow.down " ) {
2022-09-07 12:49:41 +01:00
Button ( " Import database " , role : . destructive ) {
2022-06-24 13:52:20 +01:00
showFileImporter = true
}
}
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 " ) {
2022-09-07 12:49:41 +01:00
Button ( " Delete database " , role : . destructive ) {
2022-06-24 13:52:20 +01:00
alert = . deleteChat
}
}
} 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 " ) {
2022-09-07 12:49:41 +01:00
Button ( " Delete old database " ) {
2022-06-24 13:52:20 +01:00
alert = . deleteLegacyDatabase
}
}
}
}
2022-09-19 19:05:29 +04:00
Section {
2023-01-20 12:38:38 +00:00
Button ( m . users . count > 1 ? " Delete files for all chat profiles " : " Delete all files " , role : . destructive ) {
2022-09-19 19:05:29 +04:00
alert = . deleteFilesAndMedia
}
2022-10-03 16:42:43 +04:00
. disabled ( ! stopped || appFilesCountAndSize ? . 0 = = 0 )
2022-09-19 19:05:29 +04:00
} header : {
2023-01-20 12:38:38 +00:00
Text ( " Files & media " )
2022-09-19 19:05:29 +04:00
} footer : {
if let ( fileCount , size ) = appFilesCountAndSize {
if fileCount = = 0 {
Text ( " No received or sent files " )
} else {
2023-03-28 22:20:06 +04:00
Text ( " \( fileCount ) file(s) with total size of \( ByteCountFormatter . string ( fromByteCount : Int64 ( size ) , countStyle : . binary ) ) " )
2022-09-19 19:05:29 +04:00
}
}
}
}
. onAppear {
runChat = m . chatRunning ? ? true
appFilesCountAndSize = directoryFileCountAndSize ( getAppFilesDirectory ( ) )
2022-10-03 16:42:43 +04:00
currentChatItemTTL = chatItemTTL
}
. onChange ( of : chatItemTTL ) { ttl in
if ttl < currentChatItemTTL {
alert = . setChatItemTTL ( ttl : ttl )
} else if ttl != currentChatItemTTL {
setCiTTL ( ttl )
}
2022-06-24 13:52:20 +01:00
}
. 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 " ) ) {
2022-09-08 17:36:16 +01:00
authStopChat ( )
2022-06-24 13:52:20 +01:00
} ,
secondaryButton : . cancel {
withAnimation { runChat = true }
}
)
2022-09-07 12:49:41 +01:00
case . exportProhibited :
return Alert (
title : Text ( " Set passphrase to export " ) ,
message : Text ( " Database is encrypted using a random passphrase. Please change it before exporting. " )
)
2022-06-24 13:52:20 +01:00
case . importArchive :
if let fileURL = importedArchivePath {
return Alert (
title : Text ( " Import chat database? " ) ,
2022-07-08 22:42:38 +01:00
message : Text ( " Your current chat database will be DELETED and REPLACED with the imported one. " ) + Text ( " This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. " ) ,
2022-06-24 13:52:20 +01:00
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 " ) ,
2022-07-08 22:42:38 +01:00
message : Text ( " Restart the app to use imported chat database " )
2022-06-24 13:52:20 +01:00
)
2023-05-24 14:22:12 +04:00
case . archiveImportedWithErrors :
return Alert (
title : Text ( " Chat database imported " ) ,
message : Text ( " Restart the app to use imported chat database " ) + Text ( " \n " ) + Text ( " Some non-fatal errors occurred during import - you may see Chat console for more details. " )
)
2022-06-24 13:52:20 +01:00
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 " ) ,
2022-07-08 22:42:38 +01:00
message : Text ( " Restart the app to create a new chat profile " )
2022-06-24 13:52:20 +01:00
)
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 ( )
)
2022-09-19 19:05:29 +04:00
case . deleteFilesAndMedia :
return Alert (
title : Text ( " Delete files and media? " ) ,
message : Text ( " This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. " ) ,
primaryButton : . destructive ( Text ( " Delete " ) ) {
deleteFiles ( )
} ,
secondaryButton : . cancel ( )
)
2022-10-03 16:42:43 +04:00
case let . setChatItemTTL ( ttl ) :
return Alert (
title : Text ( " Enable automatic message deletion? " ) ,
2022-10-04 09:53:43 +01:00
message : Text ( " This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. " ) ,
2022-10-03 16:42:43 +04:00
primaryButton : . destructive ( Text ( " Delete messages " ) ) {
setCiTTL ( ttl )
} ,
secondaryButton : . cancel ( ) {
chatItemTTL = currentChatItemTTL
}
)
2022-06-24 13:52:20 +01:00
case let . error ( title , error ) :
2022-11-25 13:50:26 +00:00
return Alert ( title : Text ( title ) , message : Text ( error ) )
2022-06-24 13:52:20 +01:00
}
}
2022-09-08 17:36:16 +01:00
private func authStopChat ( ) {
if UserDefaults . standard . bool ( forKey : DEFAULT_PERFORM_LA ) {
authenticate ( reason : NSLocalizedString ( " Stop SimpleX " , comment : " authentication reason " ) ) { laResult in
switch laResult {
case . success : stopChat ( )
case . unavailable : stopChat ( )
case . failed : withAnimation { runChat = true }
}
}
} else {
stopChat ( )
}
}
2022-06-24 13:52:20 +01:00
private func stopChat ( ) {
Task {
do {
2023-05-09 10:33:30 +02:00
try await stopChatAsync ( )
2022-06-24 13:52:20 +01:00
} 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 )
2023-05-24 14:22:12 +04:00
let archiveErrors = try await apiImportArchive ( config : config )
2023-04-12 12:22:55 +02:00
_ = kcDatabasePassword . remove ( )
2023-05-24 14:22:12 +04:00
if archiveErrors . isEmpty {
await operationEnded ( . archiveImported )
} else {
await operationEnded ( . archiveImportedWithErrors ( archiveErrors : archiveErrors ) )
}
2022-06-24 13:52:20 +01:00
} 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 {
2023-05-09 10:33:30 +02:00
try await deleteChatAsync ( )
2022-06-24 13:52:20 +01:00
await operationEnded ( . chatDeleted )
2022-09-25 20:53:32 +01:00
appFilesCountAndSize = directoryFileCountAndSize ( getAppFilesDirectory ( ) )
2022-06-24 13:52:20 +01:00
} 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
2022-09-23 12:51:40 +01:00
m . chatInitialized = false
2022-06-24 13:52:20 +01:00
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
2022-07-06 14:07:27 +01:00
appStateGroupDefault . set ( . active )
2022-06-24 13:52:20 +01:00
} catch let error {
fatalError ( " Error starting chat \( responseError ( error ) ) " )
}
}
} else {
do {
_ = try apiStartChat ( )
runChat = true
m . chatRunning = true
2022-06-27 10:28:30 +01:00
ChatReceiver . shared . start ( )
2022-06-24 13:52:20 +01:00
chatLastStartGroupDefault . set ( Date . now )
2022-07-06 14:07:27 +01:00
appStateGroupDefault . set ( . active )
2022-06-24 13:52:20 +01:00
} catch let error {
runChat = false
alert = . error ( title : " Error starting chat " , error : responseError ( error ) )
}
}
}
2022-09-19 19:05:29 +04:00
2022-10-03 16:42:43 +04:00
private func setCiTTL ( _ ttl : ChatItemTTL ) {
logger . debug ( " DatabaseView setChatItemTTL \( ttl . seconds ? ? - 1 ) " )
progressIndicator = true
Task {
do {
try await setChatItemTTL ( ttl )
await MainActor . run {
m . chatItemTTL = ttl
currentChatItemTTL = ttl
2022-10-07 10:55:54 +04:00
afterSetCiTTL ( )
2022-10-03 16:42:43 +04:00
}
} catch {
await MainActor . run {
2022-10-04 09:53:43 +01:00
alert = . error ( title : " Error changing setting " , error : responseError ( error ) )
2022-10-03 16:42:43 +04:00
chatItemTTL = currentChatItemTTL
2022-10-07 10:55:54 +04:00
afterSetCiTTL ( )
2022-10-03 16:42:43 +04:00
}
}
}
}
2022-10-07 10:55:54 +04:00
private func afterSetCiTTL ( ) {
progressIndicator = false
appFilesCountAndSize = directoryFileCountAndSize ( getAppFilesDirectory ( ) )
do {
let chats = try apiGetChats ( )
m . updateChats ( with : chats )
} catch let error {
logger . error ( " apiGetChats: cannot update chats \( responseError ( error ) ) " )
}
}
2022-09-19 19:05:29 +04:00
private func deleteFiles ( ) {
deleteAppFiles ( )
appFilesCountAndSize = directoryFileCountAndSize ( getAppFilesDirectory ( ) )
}
2022-06-24 13:52:20 +01:00
}
2023-05-09 10:33:30 +02:00
func stopChatAsync ( ) async throws {
try await apiStopChat ( )
ChatReceiver . shared . stop ( )
await MainActor . run { ChatModel . shared . chatRunning = false }
appStateGroupDefault . set ( . stopped )
}
func deleteChatAsync ( ) async throws {
try await apiDeleteStorage ( )
_ = kcDatabasePassword . remove ( )
storeDBPassphraseGroupDefault . set ( true )
}
2022-06-24 13:52:20 +01:00
struct DatabaseView_Previews : PreviewProvider {
static var previews : some View {
2022-10-03 16:42:43 +04:00
DatabaseView ( showSettings : Binding . constant ( false ) , chatItemTTL : . none )
2022-06-24 13:52:20 +01:00
}
}