wip
This commit is contained in:
parent
32086a28e8
commit
f89831f82d
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -121,3 +121,4 @@ xcuserdata
|
||||||
/buildServer.json
|
/buildServer.json
|
||||||
TODO.txt
|
TODO.txt
|
||||||
PASSWD.txt
|
PASSWD.txt
|
||||||
|
sandbox.xml
|
||||||
|
|
|
@ -44,22 +44,6 @@ extension Database {
|
||||||
table.column("type", .integer).notNull()
|
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
|
// messages
|
||||||
try db.create(table: "messages", options: [.ifNotExists]) { table in
|
try db.create(table: "messages", options: [.ifNotExists]) { table in
|
||||||
table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace)
|
table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace)
|
||||||
|
@ -74,7 +58,28 @@ extension Database {
|
||||||
table.column("date", .datetime).notNull()
|
table.column("date", .datetime).notNull()
|
||||||
table.column("pending", .boolean).notNull()
|
table.column("pending", .boolean).notNull()
|
||||||
table.column("sentError", .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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,22 @@ final class DatabaseMiddleware {
|
||||||
try database._db.write { db in
|
try database._db.write { db in
|
||||||
try message.insert(db)
|
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))
|
promise(.success(.empty))
|
||||||
} catch {
|
} catch {
|
||||||
promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))))
|
promise(.success(.databaseAction(.storeMessageFailed(reason: error.localizedDescription))))
|
||||||
|
@ -279,6 +295,7 @@ private extension DatabaseMiddleware {
|
||||||
(Column("from") == chat.account && Column("to") == chat.participant)
|
(Column("from") == chat.account && Column("to") == chat.participant)
|
||||||
)
|
)
|
||||||
.order(Column("date").desc)
|
.order(Column("date").desc)
|
||||||
|
.including(optional: Message.attachment)
|
||||||
.fetchAll
|
.fetchAll
|
||||||
)
|
)
|
||||||
.publisher(in: database._db, scheduling: .immediate)
|
.publisher(in: database._db, scheduling: .immediate)
|
||||||
|
@ -291,3 +308,15 @@ private extension DatabaseMiddleware {
|
||||||
.store(in: &conversationCancellables)
|
.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)
|
||||||
|
// }
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,29 +8,41 @@ enum AttachmentType: Int, Stateable, DatabaseValueConvertible {
|
||||||
case image = 1
|
case image = 1
|
||||||
case audio = 2
|
case audio = 2
|
||||||
case file = 3
|
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 {
|
struct Attachment: DBStorable {
|
||||||
static let databaseTableName = "attachments"
|
static let databaseTableName = "attachments"
|
||||||
|
|
||||||
let id: String
|
let id: String
|
||||||
static let items = hasMany(AttachmentItem.self)
|
let type: AttachmentType
|
||||||
static let message = hasOne(Message.self)
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,11 @@ struct Message: DBStorable, Equatable {
|
||||||
let sentError: Bool
|
let sentError: Bool
|
||||||
|
|
||||||
static let attachment = hasOne(Attachment.self)
|
static let attachment = hasOne(Attachment.self)
|
||||||
|
var attachment: QueryInterfaceRequest<Attachment> {
|
||||||
|
request(for: Message.attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attachmentId: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Message {
|
extension Message {
|
||||||
|
|
|
@ -14,7 +14,8 @@ let store = AppStore(
|
||||||
RostersMiddleware.shared.middleware,
|
RostersMiddleware.shared.middleware,
|
||||||
ChatsMiddleware.shared.middleware,
|
ChatsMiddleware.shared.middleware,
|
||||||
ConversationMiddleware.shared.middleware,
|
ConversationMiddleware.shared.middleware,
|
||||||
SharingMiddleware.shared.middleware
|
SharingMiddleware.shared.middleware,
|
||||||
|
FileMiddleware.shared.middleware
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -37,4 +37,7 @@ enum Const {
|
||||||
|
|
||||||
// Grid size for gallery preview (3 in a row)
|
// Grid size for gallery preview (3 in a row)
|
||||||
static let galleryGridSize = UIScreen.main.bounds.width / 3
|
static let galleryGridSize = UIScreen.main.bounds.width / 3
|
||||||
|
|
||||||
|
// Size for map preview for location messages
|
||||||
|
static let mapPreviewSize = UIScreen.main.bounds.width * 0.75
|
||||||
}
|
}
|
||||||
|
|
16
ConversationsClassic/Helpers/Map+Extensions.swift
Normal file
16
ConversationsClassic/Helpers/Map+Extensions.swift
Normal 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)"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import CoreLocation
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
@ -12,4 +13,16 @@ extension String {
|
||||||
result = "> \(result)"
|
result = "> \(result)"
|
||||||
return 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import MapKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ConversationMessageContainer: View {
|
struct ConversationMessageContainer: View {
|
||||||
|
@ -5,11 +6,23 @@ struct ConversationMessageContainer: View {
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(message.body ?? "...")
|
if let msgText = message.body {
|
||||||
.font(.body2)
|
if msgText.isLocation {
|
||||||
.foregroundColor(.Material.Text.main)
|
EmbededMapView(location: msgText.getLatLon)
|
||||||
.multilineTextAlignment(.leading)
|
} else {
|
||||||
.padding(10)
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue