This commit is contained in:
fmodf 2024-07-11 15:59:24 +02:00
parent 32086a28e8
commit f89831f82d
12 changed files with 183 additions and 49 deletions

1
.gitignore vendored
View file

@ -121,3 +121,4 @@ xcuserdata
/buildServer.json
TODO.txt
PASSWD.txt
sandbox.xml

View file

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

View file

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

View file

@ -0,0 +1,21 @@
import Combine
final class FileMiddleware {
static let shared = AccountsMiddleware()
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
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()
}
}
}

View file

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

View file

@ -34,6 +34,11 @@ struct Message: DBStorable, Equatable {
let sentError: Bool
static let attachment = hasOne(Attachment.self)
var attachment: QueryInterfaceRequest<Attachment> {
request(for: Message.attachment)
}
var attachmentId: String?
}
extension Message {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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