diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 6b70f0f054..f20882383a 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -77,6 +77,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { func applicationWillTerminate(_ application: UIApplication) { logger.debug("AppDelegate: applicationWillTerminate") + ChatModel.shared.filesToDelete.forEach { + removeFile($0) + } + ChatModel.shared.filesToDelete = [] terminateChat() } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 1ca049a84a..05b5ce1bcc 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -63,6 +63,8 @@ final class ChatModel: ObservableObject { var messageDelivery: Dictionary Void> = [:] + var filesToDelete: [String] = [] + static let shared = ChatModel() static var ok: Bool { ChatModel.shared.chatDbStatus == .ok } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 098af6cabe..ab916db9ad 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -84,7 +84,7 @@ struct MsgContentView: View { } } -func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, preview: Bool = false) -> Text { +func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text { let s = text var res: Text if let ft = formattedText, ft.count > 0 { @@ -98,6 +98,10 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St res = Text(s) } + if let i = icon { + res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res + } + if let s = sender { let t = Text(s) return (preview ? t : t.fontWeight(.medium)) + Text(": ") + res diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index e78589313f..fe6521129c 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -52,7 +52,7 @@ struct ChatView: View { } Spacer(minLength: 0) - + ComposeView( chat: chat, composeState: $composeState, @@ -79,10 +79,6 @@ struct ChatView: View { .onDisappear { if chatModel.chatId == cInfo.id { chatModel.chatId = nil - if !composeState.empty { - chatModel.draft = composeState - chatModel.draftChatId = chat.id - } DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { if chatModel.chatId == nil { chatModel.reversedChatItems = [] diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 0136159ed4..baac736d16 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -14,9 +14,9 @@ import PhotosUI enum ComposePreview { case noPreview case linkPreview(linkPreview: LinkPreview?) - case imagePreviews(imagePreviews: [String]) + case imagePreviews(imagePreviews: [(String, UploadContent?)]) case voicePreview(recordingFileName: String, duration: Int) - case filePreview(fileName: String) + case filePreview(fileName: String, file: URL) } enum ComposeContextItem { @@ -163,7 +163,7 @@ struct ComposeState { } var empty: Bool { - message == "" && liveMessage == nil && noPreview + message == "" && noPreview } } @@ -175,11 +175,12 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview { case let .link(_, preview: preview): chatItemPreview = .linkPreview(linkPreview: preview) case let .image(_, image): - chatItemPreview = .imagePreviews(imagePreviews: [image]) + chatItemPreview = .imagePreviews(imagePreviews: [(image, nil)]) case let .voice(_, duration): chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration) case .file: - chatItemPreview = .filePreview(fileName: chatItem.file?.fileName ?? "") + let fileName = chatItem.file?.fileName ?? "" + chatItemPreview = .filePreview(fileName: fileName, file: getAppFilePath(fileName)) default: chatItemPreview = .noPreview } @@ -233,7 +234,6 @@ struct ComposeView: View { @State private var showTakePhoto = false @State var chosenImages: [UploadContent] = [] @State private var showFileImporter = false - @State var chosenFile: URL? = nil @State private var audioRecorder: AudioRecorder? @State private var voiceMessageRecordingTime: TimeInterval? @@ -341,10 +341,10 @@ struct ComposeView: View { } .onChange(of: chosenImages) { images in Task { - var imgs: [String] = [] + var imgs: [(String, UploadContent)] = [] for image in images { if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) { - imgs.append(img) + imgs.append((img, image)) await MainActor.run { composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs)) } @@ -371,9 +371,8 @@ struct ComposeView: View { } fileURL.stopAccessingSecurityScopedResource() if let fileSize = fileSize, - fileSize <= MAX_FILE_SIZE { - chosenFile = fileURL - composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent)) + fileSize <= MAX_FILE_SIZE { + composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL)) } else { let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE) AlertManager.shared.showAlertMsg( @@ -387,12 +386,18 @@ struct ComposeView: View { } } .onDisappear { - if let fileName = composeState.voiceMessageRecordingFileName { - cancelVoiceMessageRecording(fileName) - } - if composeState.liveMessage != nil && (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) { + if composeState.liveMessage != nil + && (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) { + cancelCurrentVoiceRecording() + clearCurrentDraft() sendMessage() resetLinkPreview() + } else if !composeState.empty { + saveCurrentDraft() + } else { + cancelCurrentVoiceRecording() + clearCurrentDraft() + clearState() } chatModel.removeLiveDummy(animated: false) } @@ -409,6 +414,12 @@ struct ComposeView: View { if !vmAllowed && composeState.voicePreview, let fileName = composeState.voiceMessageRecordingFileName { cancelVoiceMessageRecording(fileName) + clearState() + } + } + .onAppear { + if case let .voicePreview(_, duration) = composeState.preview { + voiceMessageRecordingTime = TimeInterval(duration) } } } @@ -475,7 +486,7 @@ struct ComposeView: View { ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview) case let .imagePreviews(imagePreviews: images): ComposeImageView( - images: images, + images: images.map { (img, _) in img }, cancelImage: { composeState = composeState.copy(preview: .noPreview) chosenImages = [] @@ -486,16 +497,18 @@ struct ComposeView: View { recordingFileName: recordingFileName, recordingTime: $voiceMessageRecordingTime, recordingState: $composeState.voiceMessageRecordingState, - cancelVoiceMessage: { cancelVoiceMessageRecording($0) }, + cancelVoiceMessage: { + cancelVoiceMessageRecording($0) + clearState() + }, cancelEnabled: !composeState.editing, stopPlayback: $stopPlayback ) - case let .filePreview(fileName: fileName): + case let .filePreview(fileName, _): ComposeFileView( fileName: fileName, cancelFile: { composeState = composeState.copy(preview: .noPreview) - chosenFile = nil }, cancelEnabled: !composeState.editing) } @@ -552,27 +565,23 @@ struct ComposeView: View { case .linkPreview: sent = await send(checkLinkPreview(), quoted: quoted, live: live) case let .imagePreviews(imagePreviews: images): - let last = min(chosenImages.count, images.count) - 1 + let last = images.count - 1 if last >= 0 { for i in 0.. ChatItem? { + let (image, data) = imageData + if let data = data, let savedFile = saveAnyImage(data) { + return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live) + } + return nil + } + func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false) async -> ChatItem? { if let chatItem = await apiSendMessage( type: chat.chatInfo.chatType, @@ -737,11 +754,16 @@ struct ComposeView: View { ) } + private func cancelCurrentVoiceRecording() { + if let fileName = composeState.voiceMessageRecordingFileName { + cancelVoiceMessageRecording(fileName) + } + } + private func cancelVoiceMessageRecording(_ fileName: String) { stopPlayback.toggle() audioRecorder?.stop() removeFile(fileName) - clearState() } private func clearState(live: Bool = false) { @@ -753,12 +775,29 @@ struct ComposeView: View { resetLinkPreview() } chosenImages = [] - chosenFile = nil audioRecorder = nil voiceMessageRecordingTime = nil startingRecording = false } + private func saveCurrentDraft() { + if case .recording = composeState.voiceMessageRecordingState { + finishVoiceMessageRecording() + if let fileName = composeState.voiceMessageRecordingFileName { + chatModel.filesToDelete.append(fileName) + } + } + chatModel.draft = composeState + chatModel.draftChatId = chat.id + } + + private func clearCurrentDraft() { + if chatModel.draftChatId == chat.id { + chatModel.draft = nil + chatModel.draftChatId = nil + } + } + private func showLinkPreview(_ s: String) { prevLinkUrl = linkUrl linkUrl = parseMessage(s) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index f006ebf13c..5a40f158e4 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -40,7 +40,7 @@ struct ChatPreviewView: View { .padding(.horizontal, 8) ZStack(alignment: .topTrailing) { - chatPreviewText(cItem) + chatMessagePreview(cItem) if case .direct = chat.chatInfo { chatStatusImage() .padding(.top, 24) @@ -142,21 +142,34 @@ struct ChatPreviewView: View { func attachment() -> Text { switch draft.preview { - case .filePreview: return image("doc.fill") + case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ") case .imagePreviews: return image("photo") - case let .voicePreview(_, duration): return image("play.fill") + Text(voiceMessageTime(TimeInterval(duration))) + case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration)) default: return Text("") } } } - @ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View { + func chatItemPreview(_ cItem: ChatItem) -> Text { + let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") + let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil + return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true) + + func attachment() -> String? { + switch cItem.content.msgContent { + case .file: return "doc.fill" + case .image: return "photo" + case .voice: return "play.fill" + default: return nil + } + } + } + + @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { chatPreviewLayout(messageDraft(draft)) } else if let cItem = cItem { - let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") - let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil - chatPreviewLayout(itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true)) + chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) } else { switch (chat.chatInfo) { case let .direct(contact):