diff --git a/ConversationsClassic/AppCore/Actions/ConversationActions.swift b/ConversationsClassic/AppCore/Actions/ConversationActions.swift index b3130df..9294677 100644 --- a/ConversationsClassic/AppCore/Actions/ConversationActions.swift +++ b/ConversationsClassic/AppCore/Actions/ConversationActions.swift @@ -2,7 +2,6 @@ enum ConversationAction: Codable { case makeConversationActive(chat: Chat, roster: Roster?) case messagesUpdated(messages: [Message]) - case attachmentsUpdated(attachments: [Attachment]) case sendMessage(from: String, to: String, body: String) case setReplyText(String) diff --git a/ConversationsClassic/AppCore/Database/Database+Migrations.swift b/ConversationsClassic/AppCore/Database/Database+Migrations.swift index eba10cf..d2e1aaa 100644 --- a/ConversationsClassic/AppCore/Database/Database+Migrations.swift +++ b/ConversationsClassic/AppCore/Database/Database+Migrations.swift @@ -58,12 +58,7 @@ extension Database { table.column("date", .datetime).notNull() table.column("pending", .boolean).notNull() table.column("sentError", .boolean).notNull() - } - - // attachments - try db.create(table: "attachments", options: [.ifNotExists]) { table in - table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) - table.column("type", .integer).notNull() + table.column("attachmentType", .integer) table.column("localPath", .text) table.column("remotePath", .text) table.column("localThumbnailPath", .text) @@ -71,19 +66,6 @@ extension Database { } } - // 2nd migration - add foreign key constraints - migrator.registerMigration("Add foreign keys") { db in - // messages to attachments - try db.alter(table: "messages") { table in - table.add(column: "attachmentId", .text).references("attachments", onDelete: .cascade) - } - - // attachments to messsages - try db.alter(table: "attachments") { table in - table.add(column: "messageId", .text).references("messages", onDelete: .cascade) - } - } - // return migrator return migrator }() diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift index a157758..4dc73d1 100644 --- a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift @@ -176,22 +176,6 @@ final class DatabaseMiddleware { try database._db.write { db in try message.insert(db) } - if let remoteUrl = message.oobUrl { - let attachment = Attachment( - id: UUID().uuidString, - type: remoteUrl.attachmentType, - localPath: nil, - remotePath: URL(string: remoteUrl), - localThumbnailPath: nil, - messageId: message.id - ) - try database._db.write { db in - try attachment.insert(db) - try Message - .filter(Column("id") == message.id) - .updateAll(db, [Column("attachmentId").set(to: attachment.id)]) - } - } promise(.success(.empty)) } catch { promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription)))) @@ -289,9 +273,9 @@ final class DatabaseMiddleware { } do { _ = try database._db.write { db in - try Attachment + try Message .filter(Column("id") == id) - .updateAll(db, Column("downloadFailed").set(to: true)) + .updateAll(db, Column("downloadFailed").set(to: false)) } promise(.success(.empty)) } catch { @@ -312,7 +296,7 @@ final class DatabaseMiddleware { } do { _ = try database._db.write { db in - try Attachment + try Message .filter(Column("id") == id) .updateAll(db, Column("localPath").set(to: localUrl)) } @@ -335,7 +319,7 @@ final class DatabaseMiddleware { } do { _ = try database._db.write { db in - try Attachment + try Message .filter(Column("id") == id) .updateAll(db, Column("localThumbnailPath").set(to: thumbnailUrl)) } @@ -365,7 +349,6 @@ private extension DatabaseMiddleware { (Column("from") == chat.account && Column("to") == chat.participant) ) .order(Column("date").desc) - .including(optional: Message.attachment) .fetchAll ) .publisher(in: database._db, scheduling: .immediate) @@ -375,23 +358,6 @@ private extension DatabaseMiddleware { DispatchQueue.main.async { store.dispatch(.conversationAction(.messagesUpdated(messages: messages))) } - - // attachments - var attachments: [Attachment] = [] - for message in messages { - do { - try self.database._db.read { db in - if let attachment = try message.attachment.fetchOne(db) { - attachments.append(attachment) - } - } - } catch { - print("Failed to fetch attachment for message \(message.id): \(error)") - } - } - DispatchQueue.main.async { - store.dispatch(.conversationAction(.attachmentsUpdated(attachments: attachments))) - } } .store(in: &conversationCancellables) } diff --git a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift index a796bb8..e859380 100644 --- a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift @@ -7,12 +7,12 @@ final class FileMiddleware { func middleware(state _: AppState, action: AppAction) -> AnyPublisher { switch action { - case .conversationAction(.attachmentsUpdated(let attachments)): + case .conversationAction(.messagesUpdated(let messages)): return Future { promise in - for attachment in attachments where attachment.localPath == nil && attachment.remotePath != nil { + for message in messages where message.remotePath != nil && message.localPath == nil { DispatchQueue.main.async { // swiftlint:disable:next force_unwrapping - store.dispatch(.fileAction(.downloadAttachmentFile(id: attachment.id, remotePath: attachment.remotePath!))) + store.dispatch(.fileAction(.downloadAttachmentFile(id: message.id, remotePath: message.remotePath!))) } } promise(.success(.empty)) diff --git a/ConversationsClassic/AppCore/Models/Attachment.swift b/ConversationsClassic/AppCore/Models/Attachment.swift deleted file mode 100644 index 857b13d..0000000 --- a/ConversationsClassic/AppCore/Models/Attachment.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import GRDB -import Martin -import SwiftUI - -enum AttachmentType: Int, Stateable, DatabaseValueConvertible { - case movie = 0 - case image = 1 - case audio = 2 - case file = 3 -} - -struct Attachment: DBStorable { - static let databaseTableName = "attachments" - - let id: String - let type: AttachmentType - let localPath: URL? - let remotePath: URL? - let localThumbnailPath: URL? - let messageId: String - var downloadFailed: Bool = false - - static let message = belongsTo(Message.self) - var message: QueryInterfaceRequest { - request(for: Attachment.message) - } -} - -extension Attachment: Equatable {} - -extension String { - var attachmentType: AttachmentType { - let ext = (self as NSString).pathExtension.lowercased() - - switch ext { - case "mov", "mp4", "avi": - return .movie - - case "jpg", "png", "gif": - return .image - - case "mp3", "wav", "m4a": - return .audio - - case "txt", "doc", "pdf": - return .file - - default: - return .file - } - } -} diff --git a/ConversationsClassic/AppCore/Models/Message.swift b/ConversationsClassic/AppCore/Models/Message.swift index 0bea0e3..1e45bb9 100644 --- a/ConversationsClassic/AppCore/Models/Message.swift +++ b/ConversationsClassic/AppCore/Models/Message.swift @@ -8,10 +8,18 @@ enum MessageType: String, Codable, DatabaseValueConvertible { case error } +enum MessageAttachmentType: Int, Stateable, DatabaseValueConvertible { + case movie = 0 + case image = 1 + case audio = 2 + case file = 3 +} + enum MessageContentType: String, Codable, DatabaseValueConvertible { case text case typing case invite + case attachment } struct Message: DBStorable, Equatable { @@ -33,12 +41,11 @@ struct Message: DBStorable, Equatable { let pending: Bool let sentError: Bool - static let attachment = hasOne(Attachment.self) - var attachment: QueryInterfaceRequest { - request(for: Message.attachment) - } - - var attachmentId: String? + var attachmentType: MessageAttachmentType? + var localPath: URL? + var remotePath: URL? + var localThumbnailPath: URL? + var downloadFailed: Bool = false } extension Message { @@ -64,7 +71,9 @@ extension Message { // Content type var contentType: MessageContentType = .text - if martinMessage.hints.contains(.noStore) { + if martinMessage.oob != nil { + contentType = .attachment + } else if martinMessage.hints.contains(.noStore) { contentType = .typing } @@ -73,7 +82,7 @@ extension Message { let to = martinMessage.to?.bareJid.stringValue // Msg - let msg = Message( + var msg = Message( id: martinMessage.id ?? UUID().uuidString, type: type, contentType: contentType, @@ -85,8 +94,17 @@ extension Message { oobUrl: martinMessage.oob, date: Date(), pending: false, - sentError: false + sentError: false, + attachmentType: nil, + localPath: nil, + remotePath: nil, + localThumbnailPath: nil, + downloadFailed: false ) + if let oob = martinMessage.oob { + msg.attachmentType = oob.attachmentType + msg.remotePath = URL(string: oob) + } return msg } } diff --git a/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift b/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift index 028c17e..cc76c62 100644 --- a/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift +++ b/ConversationsClassic/AppCore/Reducers/ConversationReducer.swift @@ -17,9 +17,6 @@ extension ConversationState { state.replyText = text.makeReply } - case .attachmentsUpdated(let attachments): - state.currentAttachments = attachments - default: break } diff --git a/ConversationsClassic/AppCore/State/ConversationState.swift b/ConversationsClassic/AppCore/State/ConversationState.swift index c392f76..9267bbd 100644 --- a/ConversationsClassic/AppCore/State/ConversationState.swift +++ b/ConversationsClassic/AppCore/State/ConversationState.swift @@ -2,7 +2,6 @@ struct ConversationState: Stateable { var currentChat: Chat? var currentRoster: Roster? var currentMessages: [Message] - var currentAttachments: [Attachment] var replyText: String } @@ -11,7 +10,6 @@ struct ConversationState: Stateable { extension ConversationState { init() { currentMessages = [] - currentAttachments = [] replyText = "" } } diff --git a/ConversationsClassic/Helpers/String+Extensions.swift b/ConversationsClassic/Helpers/String+Extensions.swift index 776c329..0c17952 100644 --- a/ConversationsClassic/Helpers/String+Extensions.swift +++ b/ConversationsClassic/Helpers/String+Extensions.swift @@ -26,3 +26,26 @@ extension String { return CLLocationCoordinate2D(latitude: lat, longitude: lon) } } + +extension String { + var attachmentType: MessageAttachmentType { + let ext = (self as NSString).pathExtension.lowercased() + + switch ext { + case "mov", "mp4", "avi": + return .movie + + case "jpg", "png", "gif": + return .image + + case "mp3", "wav", "m4a": + return .audio + + case "txt", "doc", "pdf": + return .file + + default: + return .file + } + } +} diff --git a/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift b/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift index cdd51d1..1928fce 100644 --- a/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift +++ b/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift @@ -9,8 +9,8 @@ struct ConversationMessageContainer: View { var body: some View { if let msgText = message.body, msgText.isLocation { EmbededMapView(location: msgText.getLatLon) - } else if let attachmentId = message.attachmentId { - AttachmentView(attachmentId: attachmentId) + } else if message.attachmentType != nil { + AttachmentView(message: message) } else { Text(message.body ?? "...") .font(.body2) @@ -67,37 +67,31 @@ private struct EmbededMapView: View { } private struct AttachmentView: View { - @EnvironmentObject var store: AppStore - - let attachmentId: String + let message: Message var body: some View { - if let attachment { - switch attachment.type { - case .image: - if let thumbnail = thumbnail() { - thumbnail - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) - } else { - placeholder - } - - case .movie: - if let file = attachment.localPath { - VideoPlayerView(url: file) - .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) - .cornerRadius(Const.attachmentPreviewSize / 10) - .overlay(RoundedRectangle(cornerRadius: Const.attachmentPreviewSize / 10).stroke(Color.Material.Shape.separator, lineWidth: 1)) - } else { - placeholder - } - - default: + switch message.attachmentType { + case .image: + if let thumbnail = thumbnail() { + thumbnail + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) + } else { placeholder } - } else { + + case .movie: + if let file = message.localPath { + VideoPlayerView(url: file) + .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) + .cornerRadius(Const.attachmentPreviewSize / 10) + .overlay(RoundedRectangle(cornerRadius: Const.attachmentPreviewSize / 10).stroke(Color.Material.Shape.separator, lineWidth: 1)) + } else { + placeholder + } + + default: placeholder } } @@ -111,21 +105,15 @@ private struct AttachmentView: View { ProgressView() .scaleEffect(1.5) .progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active)) - if let attachment { - let imageName = progressImageName(attachment.type) - Image(systemName: imageName) - .font(.body1) - .foregroundColor(.Material.Elements.active) - } + let imageName = progressImageName(message.attachmentType ?? .file) + Image(systemName: imageName) + .font(.body1) + .foregroundColor(.Material.Elements.active) } } } - private var attachment: Attachment? { - store.state.conversationsState.currentAttachments.first(where: { $0.id == attachmentId }) - } - - private func progressImageName(_ type: AttachmentType) -> String { + private func progressImageName(_ type: MessageAttachmentType) -> String { switch type { case .image: return "photo" @@ -139,8 +127,7 @@ private struct AttachmentView: View { } private func thumbnail() -> Image? { - guard let attachment = attachment else { return nil } - guard let thumbnailPath = attachment.localThumbnailPath else { return nil } + guard let thumbnailPath = message.localThumbnailPath else { return nil } guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil } return Image(uiImage: uiImage) }