This commit is contained in:
fmodf 2024-07-13 15:38:15 +02:00
parent e21610d425
commit f7eee58347
10 changed files with 87 additions and 170 deletions

View file

@ -2,7 +2,6 @@ enum ConversationAction: Codable {
case makeConversationActive(chat: Chat, roster: Roster?) case makeConversationActive(chat: Chat, roster: Roster?)
case messagesUpdated(messages: [Message]) case messagesUpdated(messages: [Message])
case attachmentsUpdated(attachments: [Attachment])
case sendMessage(from: String, to: String, body: String) case sendMessage(from: String, to: String, body: String)
case setReplyText(String) case setReplyText(String)

View file

@ -58,12 +58,7 @@ 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("attachmentType", .integer)
// 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("localPath", .text)
table.column("remotePath", .text) table.column("remotePath", .text)
table.column("localThumbnailPath", .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
return migrator return migrator
}() }()

View file

@ -176,22 +176,6 @@ 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))))
@ -289,9 +273,9 @@ final class DatabaseMiddleware {
} }
do { do {
_ = try database._db.write { db in _ = try database._db.write { db in
try Attachment try Message
.filter(Column("id") == id) .filter(Column("id") == id)
.updateAll(db, Column("downloadFailed").set(to: true)) .updateAll(db, Column("downloadFailed").set(to: false))
} }
promise(.success(.empty)) promise(.success(.empty))
} catch { } catch {
@ -312,7 +296,7 @@ final class DatabaseMiddleware {
} }
do { do {
_ = try database._db.write { db in _ = try database._db.write { db in
try Attachment try Message
.filter(Column("id") == id) .filter(Column("id") == id)
.updateAll(db, Column("localPath").set(to: localUrl)) .updateAll(db, Column("localPath").set(to: localUrl))
} }
@ -335,7 +319,7 @@ final class DatabaseMiddleware {
} }
do { do {
_ = try database._db.write { db in _ = try database._db.write { db in
try Attachment try Message
.filter(Column("id") == id) .filter(Column("id") == id)
.updateAll(db, Column("localThumbnailPath").set(to: thumbnailUrl)) .updateAll(db, Column("localThumbnailPath").set(to: thumbnailUrl))
} }
@ -365,7 +349,6 @@ 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)
@ -375,23 +358,6 @@ private extension DatabaseMiddleware {
DispatchQueue.main.async { DispatchQueue.main.async {
store.dispatch(.conversationAction(.messagesUpdated(messages: messages))) 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) .store(in: &conversationCancellables)
} }

View file

@ -7,12 +7,12 @@ final class FileMiddleware {
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> { func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
switch action { switch action {
case .conversationAction(.attachmentsUpdated(let attachments)): case .conversationAction(.messagesUpdated(let messages)):
return Future { promise in 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 { DispatchQueue.main.async {
// swiftlint:disable:next force_unwrapping // 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)) promise(.success(.empty))

View file

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

View file

@ -8,10 +8,18 @@ enum MessageType: String, Codable, DatabaseValueConvertible {
case error case error
} }
enum MessageAttachmentType: Int, Stateable, DatabaseValueConvertible {
case movie = 0
case image = 1
case audio = 2
case file = 3
}
enum MessageContentType: String, Codable, DatabaseValueConvertible { enum MessageContentType: String, Codable, DatabaseValueConvertible {
case text case text
case typing case typing
case invite case invite
case attachment
} }
struct Message: DBStorable, Equatable { struct Message: DBStorable, Equatable {
@ -33,12 +41,11 @@ struct Message: DBStorable, Equatable {
let pending: Bool let pending: Bool
let sentError: Bool let sentError: Bool
static let attachment = hasOne(Attachment.self) var attachmentType: MessageAttachmentType?
var attachment: QueryInterfaceRequest<Attachment> { var localPath: URL?
request(for: Message.attachment) var remotePath: URL?
} var localThumbnailPath: URL?
var downloadFailed: Bool = false
var attachmentId: String?
} }
extension Message { extension Message {
@ -64,7 +71,9 @@ extension Message {
// Content type // Content type
var contentType: MessageContentType = .text var contentType: MessageContentType = .text
if martinMessage.hints.contains(.noStore) { if martinMessage.oob != nil {
contentType = .attachment
} else if martinMessage.hints.contains(.noStore) {
contentType = .typing contentType = .typing
} }
@ -73,7 +82,7 @@ extension Message {
let to = martinMessage.to?.bareJid.stringValue let to = martinMessage.to?.bareJid.stringValue
// Msg // Msg
let msg = Message( var msg = Message(
id: martinMessage.id ?? UUID().uuidString, id: martinMessage.id ?? UUID().uuidString,
type: type, type: type,
contentType: contentType, contentType: contentType,
@ -85,8 +94,17 @@ extension Message {
oobUrl: martinMessage.oob, oobUrl: martinMessage.oob,
date: Date(), date: Date(),
pending: false, 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 return msg
} }
} }

View file

@ -17,9 +17,6 @@ extension ConversationState {
state.replyText = text.makeReply state.replyText = text.makeReply
} }
case .attachmentsUpdated(let attachments):
state.currentAttachments = attachments
default: default:
break break
} }

View file

@ -2,7 +2,6 @@ struct ConversationState: Stateable {
var currentChat: Chat? var currentChat: Chat?
var currentRoster: Roster? var currentRoster: Roster?
var currentMessages: [Message] var currentMessages: [Message]
var currentAttachments: [Attachment]
var replyText: String var replyText: String
} }
@ -11,7 +10,6 @@ struct ConversationState: Stateable {
extension ConversationState { extension ConversationState {
init() { init() {
currentMessages = [] currentMessages = []
currentAttachments = []
replyText = "" replyText = ""
} }
} }

View file

@ -26,3 +26,26 @@ extension String {
return CLLocationCoordinate2D(latitude: lat, longitude: lon) 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
}
}
}

View file

@ -9,8 +9,8 @@ struct ConversationMessageContainer: View {
var body: some View { var body: some View {
if let msgText = message.body, msgText.isLocation { if let msgText = message.body, msgText.isLocation {
EmbededMapView(location: msgText.getLatLon) EmbededMapView(location: msgText.getLatLon)
} else if let attachmentId = message.attachmentId { } else if message.attachmentType != nil {
AttachmentView(attachmentId: attachmentId) AttachmentView(message: message)
} else { } else {
Text(message.body ?? "...") Text(message.body ?? "...")
.font(.body2) .font(.body2)
@ -67,37 +67,31 @@ private struct EmbededMapView: View {
} }
private struct AttachmentView: View { private struct AttachmentView: View {
@EnvironmentObject var store: AppStore let message: Message
let attachmentId: String
var body: some View { var body: some View {
if let attachment { switch message.attachmentType {
switch attachment.type { case .image:
case .image: if let thumbnail = thumbnail() {
if let thumbnail = thumbnail() { thumbnail
thumbnail .resizable()
.resizable() .aspectRatio(contentMode: .fit)
.aspectRatio(contentMode: .fit) .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) } else {
} 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:
placeholder 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 placeholder
} }
} }
@ -111,21 +105,15 @@ private struct AttachmentView: View {
ProgressView() ProgressView()
.scaleEffect(1.5) .scaleEffect(1.5)
.progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active)) .progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active))
if let attachment { let imageName = progressImageName(message.attachmentType ?? .file)
let imageName = progressImageName(attachment.type) Image(systemName: imageName)
Image(systemName: imageName) .font(.body1)
.font(.body1) .foregroundColor(.Material.Elements.active)
.foregroundColor(.Material.Elements.active)
}
} }
} }
} }
private var attachment: Attachment? { private func progressImageName(_ type: MessageAttachmentType) -> String {
store.state.conversationsState.currentAttachments.first(where: { $0.id == attachmentId })
}
private func progressImageName(_ type: AttachmentType) -> String {
switch type { switch type {
case .image: case .image:
return "photo" return "photo"
@ -139,8 +127,7 @@ private struct AttachmentView: View {
} }
private func thumbnail() -> Image? { private func thumbnail() -> Image? {
guard let attachment = attachment else { return nil } guard let thumbnailPath = message.localThumbnailPath else { return nil }
guard let thumbnailPath = attachment.localThumbnailPath else { return nil }
guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil } guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil }
return Image(uiImage: uiImage) return Image(uiImage: uiImage)
} }