diff --git a/.gitignore b/.gitignore index 521f6b4..d97b7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,4 @@ xcuserdata /buildServer.json TODO.txt PASSWD.txt +sandbox.xml diff --git a/ConversationsClassic/AppCore/Database/Database+Migrations.swift b/ConversationsClassic/AppCore/Database/Database+Migrations.swift index 35ff447..2534351 100644 --- a/ConversationsClassic/AppCore/Database/Database+Migrations.swift +++ b/ConversationsClassic/AppCore/Database/Database+Migrations.swift @@ -44,22 +44,6 @@ extension Database { table.column("type", .integer).notNull() } - // attachments - try db.create(table: "attachments", options: [.ifNotExists]) { table in - table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) - } - - // attachment items - try db.create(table: "attachment_items", options: [.ifNotExists]) { table in - table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) - table.belongsTo("attachments", onDelete: .cascade).notNull() - table.column("type", .integer).notNull() - table.column("localPath", .text) - table.column("remotePath", .text) - table.column("localThumbnailPath", .text) - table.column("string", .text) - } - // messages try db.create(table: "messages", options: [.ifNotExists]) { table in table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace) @@ -74,7 +58,28 @@ extension Database { table.column("date", .datetime).notNull() table.column("pending", .boolean).notNull() table.column("sentError", .boolean).notNull() - table.column("attachment", .text).references("attachments", onDelete: .cascade) + } + + // 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("localPath", .text) + table.column("remotePath", .text) + table.column("localThumbnailPath", .text) + } + } + + // 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) } } diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift index 86d96ef..52ed568 100644 --- a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift @@ -175,6 +175,22 @@ 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)))) @@ -279,6 +295,7 @@ 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) @@ -291,3 +308,15 @@ private extension DatabaseMiddleware { .store(in: &conversationCancellables) } } + +// try db.write { db in +// // Update the attachment +// var attachment = try Attachment.fetchOne(db, key: attachmentId)! +// attachment.someField = newValue +// try attachment.update(db) +// +// // Update the message +// var message = try Message.fetchOne(db, key: messageId)! +// message.someField = newValue +// try message.update(db) +// } diff --git a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift new file mode 100644 index 0000000..e18a458 --- /dev/null +++ b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift @@ -0,0 +1,21 @@ + +import Combine + +final class FileMiddleware { + static let shared = AccountsMiddleware() + + func middleware(state _: AppState, action: AppAction) -> AnyPublisher { + switch action { + case .conversationAction(.messagesUpdated(let messages)): + for msg in messages { + if msg.attachment != nil { + print("Attachment found") + } + } + return Empty().eraseToAnyPublisher() + + default: + return Empty().eraseToAnyPublisher() + } + } +} diff --git a/ConversationsClassic/AppCore/Models/Attachment.swift b/ConversationsClassic/AppCore/Models/Attachment.swift index ddde7b5..648a496 100644 --- a/ConversationsClassic/AppCore/Models/Attachment.swift +++ b/ConversationsClassic/AppCore/Models/Attachment.swift @@ -8,29 +8,41 @@ enum AttachmentType: Int, Stateable, DatabaseValueConvertible { case image = 1 case audio = 2 case file = 3 - case location = 4 - case contact = 5 -} - -struct AttachmentItem: DBStorable { - static let databaseTableName = "attachment_items" - - let id: String - static let attachment = belongsTo(Attachment.self) - let type: AttachmentType - let localPath: URL? - let remotePath: URL? - let localThumbnailPath: URL? - let string: String? } struct Attachment: DBStorable { static let databaseTableName = "attachments" let id: String - static let items = hasMany(AttachmentItem.self) - static let message = hasOne(Message.self) + let type: AttachmentType + let localPath: URL? + let remotePath: URL? + let localThumbnailPath: URL? + let messageId: String + + static let message = belongsTo(Message.self) + var message: QueryInterfaceRequest { + request(for: Attachment.message) + } } -extension AttachmentItem: Equatable {} 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 // Default to .file if the extension is not recognized + } + } +} diff --git a/ConversationsClassic/AppCore/Models/Message.swift b/ConversationsClassic/AppCore/Models/Message.swift index 9c6f0bd..0bea0e3 100644 --- a/ConversationsClassic/AppCore/Models/Message.swift +++ b/ConversationsClassic/AppCore/Models/Message.swift @@ -34,6 +34,11 @@ struct Message: DBStorable, Equatable { let sentError: Bool static let attachment = hasOne(Attachment.self) + var attachment: QueryInterfaceRequest { + request(for: Message.attachment) + } + + var attachmentId: String? } extension Message { diff --git a/ConversationsClassic/ConversationsClassicApp.swift b/ConversationsClassic/ConversationsClassicApp.swift index 3abb615..570f73c 100644 --- a/ConversationsClassic/ConversationsClassicApp.swift +++ b/ConversationsClassic/ConversationsClassicApp.swift @@ -14,7 +14,8 @@ let store = AppStore( RostersMiddleware.shared.middleware, ChatsMiddleware.shared.middleware, ConversationMiddleware.shared.middleware, - SharingMiddleware.shared.middleware + SharingMiddleware.shared.middleware, + FileMiddleware.shared.middleware ] ) diff --git a/ConversationsClassic/Helpers/Const.swift b/ConversationsClassic/Helpers/Const.swift index 71e4d34..5f65fd9 100644 --- a/ConversationsClassic/Helpers/Const.swift +++ b/ConversationsClassic/Helpers/Const.swift @@ -37,4 +37,7 @@ enum Const { // Grid size for gallery preview (3 in a row) static let galleryGridSize = UIScreen.main.bounds.width / 3 + + // Size for map preview for location messages + static let mapPreviewSize = UIScreen.main.bounds.width * 0.75 } diff --git a/ConversationsClassic/Helpers/Map+Extensions.swift b/ConversationsClassic/Helpers/Map+Extensions.swift new file mode 100644 index 0000000..4c25921 --- /dev/null +++ b/ConversationsClassic/Helpers/Map+Extensions.swift @@ -0,0 +1,16 @@ +import MapKit + +extension MKCoordinateRegion: Equatable { + public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool { + lhs.center.latitude == rhs.center.latitude && + lhs.center.longitude == rhs.center.longitude && + lhs.span.latitudeDelta == rhs.span.latitudeDelta && + lhs.span.longitudeDelta == rhs.span.longitudeDelta + } +} + +extension CLLocationCoordinate2D: Identifiable { + public var id: String { + "\(latitude)-\(longitude)" + } +} diff --git a/ConversationsClassic/Helpers/String+Extensions.swift b/ConversationsClassic/Helpers/String+Extensions.swift index 2423c88..776c329 100644 --- a/ConversationsClassic/Helpers/String+Extensions.swift +++ b/ConversationsClassic/Helpers/String+Extensions.swift @@ -1,3 +1,4 @@ +import CoreLocation import Foundation extension String { @@ -12,4 +13,16 @@ extension String { result = "> \(result)" return result } + + var isLocation: Bool { + hasPrefix("geo:") + } + + var getLatLon: CLLocationCoordinate2D { + let geo = components(separatedBy: ":")[1] + let parts = geo.components(separatedBy: ",") + let lat = Double(parts[0]) ?? 0.0 + let lon = Double(parts[1]) ?? 0.0 + return CLLocationCoordinate2D(latitude: lat, longitude: lon) + } } diff --git a/ConversationsClassic/View/Screens/Attachments/AttachmentLocationPickerView.swift b/ConversationsClassic/View/Screens/Attachments/AttachmentLocationPickerView.swift index 56abfa4..3509cca 100644 --- a/ConversationsClassic/View/Screens/Attachments/AttachmentLocationPickerView.swift +++ b/ConversationsClassic/View/Screens/Attachments/AttachmentLocationPickerView.swift @@ -132,12 +132,3 @@ class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { } } } - -extension MKCoordinateRegion: Equatable { - public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool { - lhs.center.latitude == rhs.center.latitude && - lhs.center.longitude == rhs.center.longitude && - lhs.span.latitudeDelta == rhs.span.latitudeDelta && - lhs.span.longitudeDelta == rhs.span.longitudeDelta - } -} diff --git a/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift b/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift index 4bd8907..7b25871 100644 --- a/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift +++ b/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift @@ -1,3 +1,4 @@ +import MapKit import SwiftUI struct ConversationMessageContainer: View { @@ -5,11 +6,23 @@ struct ConversationMessageContainer: View { let isOutgoing: Bool var body: some View { - Text(message.body ?? "...") - .font(.body2) - .foregroundColor(.Material.Text.main) - .multilineTextAlignment(.leading) - .padding(10) + if let msgText = message.body { + if msgText.isLocation { + EmbededMapView(location: msgText.getLatLon) + } else { + Text(message.body ?? "...") + .font(.body2) + .foregroundColor(.Material.Text.main) + .multilineTextAlignment(.leading) + .padding(10) + } + } else { + Text("...") + .font(.body2) + .foregroundColor(.Material.Text.main) + .multilineTextAlignment(.leading) + .padding(10) + } } } @@ -34,3 +47,27 @@ struct MessageAttr: View { } } } + +private struct EmbededMapView: View { + let location: CLLocationCoordinate2D + + var body: some View { + Map( + coordinateRegion: .constant(MKCoordinateRegion(center: location, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))), + interactionModes: [], + showsUserLocation: false, + userTrackingMode: .none, + annotationItems: [location], + annotationContent: { _ in + MapMarker(coordinate: location, tint: .blue) + } + ) + .frame(width: Const.mapPreviewSize, height: Const.mapPreviewSize) + .cornerRadius(10) + .onTapGesture { + let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: location)) + mapItem.name = "Location" + mapItem.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]) + } + } +}