This commit is contained in:
fmodf 2024-08-18 00:05:18 +02:00
parent dcd275b279
commit 4b092a8831
5 changed files with 54 additions and 406 deletions

View file

@ -91,7 +91,7 @@ extension Client {
guard let to = message.to else {
return
}
guard let chat = connection.module(MessageModule.self).chatManager.chat(for: connection.context, with: BareJID(to)) else {
guard let chat = connection.module(MessageModule.self).chatManager.createChat(for: connection.context, with: BareJID(to)) else {
return
}
@ -99,11 +99,8 @@ extension Client {
try await chat.send(message: msg)
}
func uploadFile(_ localPath: String) async throws -> String {
guard let localPath = URL(string: localPath) else {
throw ClientStoreError.invalidPath
}
guard let data = try? Data(contentsOf: localPath) else {
func uploadFile(_ localURL: URL) async throws -> String {
guard let data = try? Data(contentsOf: localURL) else {
throw ClientStoreError.noData
}
let httpModule = connection.module(HttpFileUploadModule.self)
@ -115,9 +112,9 @@ extension Client {
let slot = try await httpModule.requestUploadSlot(
componentJid: component.jid,
filename: localPath.lastPathComponent,
filename: localURL.lastPathComponent,
size: data.count,
contentType: localPath.mimeType
contentType: localURL.mimeType
)
var request = URLRequest(url: slot.putUri)
for (key, value) in slot.putHeaders {
@ -126,11 +123,12 @@ extension Client {
request.httpMethod = "PUT"
request.httpBody = data
request.addValue(String(data.count), forHTTPHeaderField: "Content-Length")
request.addValue(localPath.mimeType, forHTTPHeaderField: "Content-Type")
request.addValue(localURL.mimeType, forHTTPHeaderField: "Content-Type")
let (_, response) = try await URLSession.shared.data(for: request)
switch response {
case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 200:
case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 201:
return slot.getUri.absoluteString
default:
throw URLError(.badServerResponse)
}

View file

@ -20,6 +20,16 @@ struct Attachment: Codable & Equatable, DatabaseValueConvertible {
var localName: String?
var thumbnailName: String?
var remotePath: String?
var localPath: URL? {
guard let attachmentLocalName = localName else { return nil }
return Const.fileFolder.appendingPathComponent(attachmentLocalName)
}
var thumbnailPath: URL? {
guard let attachmentThumbnailName = thumbnailName else { return nil }
return Const.fileFolder.appendingPathComponent(attachmentThumbnailName)
}
}
enum MessageContentType: Codable & Equatable, DatabaseValueConvertible {
@ -47,10 +57,10 @@ struct Message: DBStorable, Equatable {
let from: String
let to: String?
let body: String?
var body: String?
let subject: String?
let thread: String?
let oobUrl: String?
var oobUrl: String?
}
extension Message {

View file

@ -1,389 +0,0 @@
import Foundation
final class FileStore {
static let shared = FileStore()
static var fileFolder: URL {
// swiftlint:disable:next force_unwrapping
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let subdirectoryURL = documentsURL.appendingPathComponent(Const.fileFolder)
if !FileManager.default.fileExists(atPath: subdirectoryURL.path) {
try? FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil)
}
return subdirectoryURL
}
func storeCaptured(messageId: String, _ data: Data, _ type: GalleryMediaType) async throws -> (String, AttachmentType) {
try await Task {
// local name
let fileId = UUID().uuidString
let localName: String
let msgType: AttachmentType
switch type {
case .photo:
localName = "\(messageId)_\(fileId).jpg"
msgType = .image
case .video:
localName = "\(messageId)_\(fileId).mov"
msgType = .video
}
// save
let localUrl = FileStore.fileFolder.appendingPathComponent(localName)
try data.write(to: localUrl)
return (localName, msgType)
}.value
}
}
// import Foundation
// import Photos
// import UIKit
//
// final class FileProcessing {
// static let shared = FileProcessing()
//
// static var fileFolder: URL {
// // swiftlint:disable:next force_unwrapping
// let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// let subdirectoryURL = documentsURL.appendingPathComponent(Const.fileFolder)
// if !FileManager.default.fileExists(atPath: subdirectoryURL.path) {
// try? FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil)
// }
// return subdirectoryURL
// }
//
// func createThumbnail(localName: String) -> String? {
// let thumbnailFileName = "thumb_\(localName)"
// let thumbnailUrl = FileProcessing.fileFolder.appendingPathComponent(thumbnailFileName)
// let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
//
// // check if thumbnail already exists
// if FileManager.default.fileExists(atPath: thumbnailUrl.path) {
// return thumbnailFileName
// }
//
// // create thumbnail if not exists
// switch localName.attachmentType {
// case .image:
// guard let image = UIImage(contentsOfFile: localUrl.path) else {
// print("FileProcessing: Error loading image: \(localUrl)")
// return nil
// }
// let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
// guard let thumbnail = scaleAndCropImage(image, targetSize) else {
// print("FileProcessing: Error scaling image: \(localUrl)")
// return nil
// }
// guard let data = thumbnail.pngData() else {
// print("FileProcessing: Error converting thumbnail of \(localUrl) to data")
// return nil
// }
// do {
// try data.write(to: thumbnailUrl)
// return thumbnailFileName
// } catch {
// print("FileProcessing: Error writing thumbnail: \(error)")
// return nil
// }
//
// default:
// return nil
// }
// }
//
// func fetchGallery() -> [SharingGalleryItem] {
// let items = syncGalleryEnumerate()
// .map {
// SharingGalleryItem(
// id: $0.localIdentifier,
// type: $0.mediaType == .image ? .photo : .video,
// duration: $0.mediaType == .video ? $0.duration.minAndSec : nil
// )
// }
// return items
// }
//
// func fillGalleryItemsThumbnails(items: [SharingGalleryItem]) -> [SharingGalleryItem] {
// let ids = items
// .filter { $0.thumbnail == nil }
// .map { $0.id }
//
// let assets = syncGalleryEnumerate(ids)
// return assets.compactMap { asset in
// if asset.mediaType == .image {
// return syncGalleryProcessImage(asset) { [weak self] image in
// if let thumbnail = self?.scaleAndCropImage(image, CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) {
// let data = thumbnail.jpegData(compressionQuality: 1.0) ?? Data()
// return SharingGalleryItem(id: asset.localIdentifier, type: .photo, thumbnail: data)
// } else {
// return nil
// }
// }
// } else if asset.mediaType == .video {
// return syncGalleryProcessVideo(asset) { [weak self] avAsset in
// // swiftlint:disable:next force_cast
// let assetURL = avAsset as! AVURLAsset
// let url = assetURL.url
// if let thumbnail = self?.generateVideoThumbnail(url, CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)) {
// let data = thumbnail.jpegData(compressionQuality: 1.0) ?? Data()
// return SharingGalleryItem(
// id: asset.localIdentifier,
// type: .video,
// thumbnail: data,
// duration: asset.duration.minAndSec
// )
// } else {
// return nil
// }
// }
// } else {
// return nil
// }
// }
// }
//
// // This function also creates new ids for messages for each new attachment
// func copyGalleryItemsForUploading(items: [SharingGalleryItem]) -> [(String, String)] {
// let assets = syncGalleryEnumerate(items.map { $0.id })
// return assets
// .compactMap { asset in
// let newMessageId = UUID().uuidString
// let fileId = UUID().uuidString
// if asset.mediaType == .image {
// return syncGalleryProcessImage(asset) { image in
// let localName = "\(newMessageId)_\(fileId).jpg"
// let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
// if let data = image.jpegData(compressionQuality: 1.0) {
// do {
// try data.write(to: localUrl)
// return (newMessageId, localName)
// } catch {
// return nil
// }
// } else {
// return nil
// }
// }
// } else if asset.mediaType == .video {
// return syncGalleryProcessVideo(asset) { avAsset in
// // swiftlint:disable:next force_cast
// let assetURL = avAsset as! AVURLAsset
// let url = assetURL.url
// let localName = "\(newMessageId)_\(fileId).mov"
// let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
// do {
// try FileManager.default.copyItem(at: url, to: localUrl)
// return (newMessageId, localName)
// } catch {
// return nil
// }
// }
// } else {
// return nil
// }
// }
// }
//
// // This function also creates new id for file from camera capturing
// func copyCameraCapturedForUploading(media: Data, type: SharingCameraMediaType) -> (String, String)? {
// let newMessageId = UUID().uuidString
// let fileId = UUID().uuidString
// let localName: String
// switch type {
// case .photo:
// localName = "\(newMessageId)_\(fileId).jpg"
// case .video:
// localName = "\(newMessageId)_\(fileId).mov"
// }
// let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
// do {
// try media.write(to: localUrl)
// return (newMessageId, localName)
// } catch {
// return nil
// }
// }
//
// // This function also creates new id for file from document sharing
// func copyDocumentsForUploading(data: [Data], extensions: [String]) -> [(String, String)] {
// data.enumerated().compactMap { index, data in
// let newMessageId = UUID().uuidString
// let fileId = UUID().uuidString
// let localName = "\(newMessageId)_\(fileId).\(extensions[index])"
// let localUrl = FileProcessing.fileFolder.appendingPathComponent(localName)
// do {
// try data.write(to: localUrl)
// return (newMessageId, localName)
// } catch {
// print("FileProcessing: Error writing document: \(error)")
// return nil
// }
// }
// }
// }
//
// private extension FileProcessing {
// func scaleAndCropImage(_ img: UIImage, _ size: CGSize) -> UIImage? {
// let aspect = img.size.width / img.size.height
// let targetAspect = size.width / size.height
// var newWidth: CGFloat
// var newHeight: CGFloat
// if aspect < targetAspect {
// newWidth = size.width
// newHeight = size.width / aspect
// } else {
// newHeight = size.height
// newWidth = size.height * aspect
// }
//
// UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
// img.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight))
// let newImage = UIGraphicsGetImageFromCurrentImageContext()
// UIGraphicsEndImageContext()
//
// return newImage
// }
//
// func syncGalleryEnumerate(_ ids: [String]? = nil) -> [PHAsset] {
// var result: [PHAsset] = []
//
// let group = DispatchGroup()
// DispatchQueue.global(qos: .userInitiated).sync {
// let fetchOptions = PHFetchOptions()
// fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
// if let ids {
// fetchOptions.predicate = NSPredicate(format: "localIdentifier IN %@", ids)
// }
// let assets = PHAsset.fetchAssets(with: fetchOptions)
// assets.enumerateObjects { asset, _, _ in
// group.enter()
// result.append(asset)
// group.leave()
// }
// }
// group.wait()
// return result
// }
//
// func syncGalleryProcess<T>(_ assets: [PHAsset], _ block: @escaping (PHAsset) -> T) -> [T] {
// var result: [T] = []
// let group = DispatchGroup()
// DispatchQueue.global(qos: .userInitiated).sync {
// for asset in assets {
// group.enter()
// let res = block(asset)
// result.append(res)
// group.leave()
// }
// }
// group.wait()
// return result
// }
//
// func syncGalleryProcessImage<T>(_ asset: PHAsset, _ block: @escaping (UIImage) -> T?) -> T? {
// var result: T?
// let semaphore = DispatchSemaphore(value: 0)
// DispatchQueue.global(qos: .userInitiated).sync {
// let options = PHImageRequestOptions()
// options.version = .original
// options.isSynchronous = true
// PHImageManager.default().requestImage(
// for: asset,
// targetSize: PHImageManagerMaximumSize,
// contentMode: .aspectFill,
// options: options
// ) { image, _ in
// if let image {
// result = block(image)
// } else {
// result = nil
// }
// semaphore.signal()
// }
// }
// semaphore.wait()
// return result
// }
//
// func syncGalleryProcessVideo<T>(_ asset: PHAsset, _ block: @escaping (AVAsset) -> T?) -> T? {
// var result: T?
// let semaphore = DispatchSemaphore(value: 0)
// _ = DispatchQueue.global(qos: .userInitiated).sync {
// PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
// if let avAsset {
// result = block(avAsset)
// } else {
// result = nil
// }
// semaphore.signal()
// }
// }
// semaphore.wait()
// return result
// }
//
// func generateVideoThumbnail(_ url: URL, _ size: CGSize) -> UIImage? {
// let asset = AVAsset(url: url)
// let assetImgGenerate = AVAssetImageGenerator(asset: asset)
// assetImgGenerate.appliesPreferredTrackTransform = true
// let time = CMTimeMakeWithSeconds(Float64(1), preferredTimescale: 600)
// do {
// let cgImage = try assetImgGenerate.copyCGImage(at: time, actualTime: nil)
// let image = UIImage(cgImage: cgImage)
// return scaleAndCropImage(image, size)
// } catch {
// return nil
// }
// }
// }
// import Foundation
//
// final class DownloadManager {
// static let shared = DownloadManager()
//
// private let urlSession: URLSession
// private let downloadQueue = DispatchQueue(label: "com.example.downloadQueue")
// private var activeDownloads = Set<URL>()
//
// init() {
// let configuration = URLSessionConfiguration.default
// urlSession = URLSession(configuration: configuration)
// }
//
// func enqueueDownload(from url: URL, to localUrl: URL, completion: @escaping (Error?) -> Void) {
// downloadQueue.async {
// if self.activeDownloads.contains(url) {
// print("Download for this file is already in queue.")
// return
// }
//
// self.activeDownloads.insert(url)
//
// let task = self.urlSession.downloadTask(with: url) { tempLocalUrl, _, error in
// self.downloadQueue.async {
// self.activeDownloads.remove(url)
//
// guard let tempLocalUrl = tempLocalUrl, error == nil else {
// completion(error)
// return
// }
//
// do {
// if FileManager.default.fileExists(atPath: localUrl.path) {
// try FileManager.default.removeItem(at: localUrl)
// }
// let data = try Data(contentsOf: tempLocalUrl)
// try data.write(to: localUrl)
// completion(nil)
// } catch let writeError {
// completion(writeError)
// }
// }
// }
// task.resume()
// }
// }
// }

View file

@ -71,7 +71,26 @@ extension ConversationStore {
let localName: String
let msgType: AttachmentType
do {
(localName, msgType) = try await FileStore.shared.storeCaptured(messageId: messageId, data, type)
(localName, msgType) = try await Task {
// local name
let fileId = UUID().uuidString
let localName: String
let msgType: AttachmentType
switch type {
case .photo:
localName = "\(messageId)_\(fileId).jpg"
msgType = .image
case .video:
localName = "\(messageId)_\(fileId).mov"
msgType = .video
}
// save
let localUrl = Const.fileFolder.appendingPathComponent(localName)
try data.write(to: localUrl)
return (localName, msgType)
}.value
} catch {
logIt(.error, "Can't save file for uploading: \(error)")
return
@ -140,7 +159,7 @@ extension ConversationStore {
guard case .attachment(let attachment) = message.contentType else {
throw ClientStoreError.invalidContentType
}
guard let localName = attachment.localName else {
guard let localName = attachment.localPath else {
throw ClientStoreError.invalidLocalName
}
let remotePath = try await client.uploadFile(localName)
@ -152,6 +171,8 @@ extension ConversationStore {
remotePath: remotePath
)
)
message.body = remotePath
message.oobUrl = remotePath
try await message.save()
try await client.sendMessage(message)
try await message.setStatus(.sent)

View file

@ -23,6 +23,17 @@ enum Const {
Bundle.main.bundleIdentifier ?? "Conversations Classic iOS"
}
// Folder for storing files
static var fileFolder: URL {
// swiftlint:disable:next force_unwrapping
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let subdirectoryURL = documentsURL.appendingPathComponent("Downloads")
if !FileManager.default.fileExists(atPath: subdirectoryURL.path) {
try? FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil)
}
return subdirectoryURL
}
// Trusted servers
enum TrustedServers: String {
case narayana = "narayana.im"
@ -32,9 +43,6 @@ enum Const {
// Limit for video for sharing
static let videoDurationLimit = 60.0
// Upload/download file folder
static let fileFolder = "Downloads"
// Grid size for gallery preview (3 in a row)
static let galleryGridSize = UIScreen.main.bounds.width / 3