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 messagesUpdated(messages: [Message])
case attachmentsUpdated(attachments: [Attachment])
case sendMessage(from: String, to: String, body: String)
case setReplyText(String)

View file

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

View file

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

View file

@ -7,12 +7,12 @@ final class FileMiddleware {
func middleware(state _: AppState, action: AppAction) -> AnyPublisher<AppAction, Never> {
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))

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
}
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<Attachment> {
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
}
}

View file

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

View file

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

View file

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

View file

@ -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,13 +67,10 @@ 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 {
switch message.attachmentType {
case .image:
if let thumbnail = thumbnail() {
thumbnail
@ -85,7 +82,7 @@ private struct AttachmentView: View {
}
case .movie:
if let file = attachment.localPath {
if let file = message.localPath {
VideoPlayerView(url: file)
.frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
.cornerRadius(Const.attachmentPreviewSize / 10)
@ -97,9 +94,6 @@ private struct AttachmentView: View {
default:
placeholder
}
} else {
placeholder
}
}
@ViewBuilder private var placeholder: some View {
@ -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)
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)
}