wip
This commit is contained in:
parent
e21610d425
commit
f7eee58347
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue