Skip to content

Commit 54614e2

Browse files
committed
IOS-5015 Supported preloading for chat files
1 parent e953de9 commit 54614e2

File tree

8 files changed

+178
-65
lines changed

8 files changed

+178
-65
lines changed

Anytype/Sources/PresentationLayer/Modules/Chat/AttachmentProcessor.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ struct FileAttachmentProcessor: AttachmentProcessor {
3434
throw AttachmentError.fileCreationFailed
3535
}
3636

37-
return .localBinaryFile(fileData)
37+
return .localBinaryFile(ChatLocalBinaryFile(data: fileData))
3838
}
3939
}
4040

@@ -50,12 +50,12 @@ struct CameraMediaProcessor: AttachmentProcessor {
5050
guard let fileData = try? fileActionsService.createFileData(image: image, type: type) else {
5151
throw AttachmentError.fileCreationFailed
5252
}
53-
return .localBinaryFile(fileData)
53+
return .localBinaryFile(ChatLocalBinaryFile(data: fileData))
5454
case .video(let file):
5555
guard let fileData = try? fileActionsService.createFileData(fileUrl: file) else {
5656
throw AttachmentError.fileCreationFailed
5757
}
58-
return .localBinaryFile(fileData)
58+
return .localBinaryFile(ChatLocalBinaryFile(data: fileData))
5959
}
6060
}
6161
}
@@ -68,7 +68,7 @@ struct PhotosPickerProcessor: AsyncAttachmentProcessor {
6868

6969
func process(_ input: PhotosPickerItem, spaceId: String) async throws -> ChatLinkedObject {
7070
let data = try await fileActionsService.createFileData(photoItem: input)
71-
return .localPhotosFile(ChatLocalPhotosFile(data: data, photosPickerItemHash: input.hashValue))
71+
return .localPhotosFile(ChatLocalPhotosFile(data: ChatLocalBinaryFile(data: data), photosPickerItemHash: input.hashValue))
7272
}
7373
}
7474

@@ -82,7 +82,7 @@ struct PasteBufferProcessor: AsyncAttachmentProcessor {
8282
guard let fileData = try? await fileActionsService.createFileData(source: .itemProvider(input)) else {
8383
throw AttachmentError.fileCreationFailed
8484
}
85-
return .localBinaryFile(fileData)
85+
return .localBinaryFile(ChatLocalBinaryFile(data: fileData))
8686
}
8787
}
8888

Anytype/Sources/PresentationLayer/Modules/Chat/ChatAttachmentHandler.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ protocol ChatAttachmentHandlerProtocol: ObservableObject {
1515

1616
func addUploadedObject(_ details: MessageAttachmentDetails) throws
1717
func removeLinkedObject(_ linkedObject: ChatLinkedObject)
18-
func clearAll()
18+
func clearState()
1919
func canAddOneAttachment() -> Bool
2020
func setPhotosItems(_ items: [PhotosPickerItem]) throws
2121
func getPhotosItems() -> [PhotosPickerItem]
@@ -54,7 +54,7 @@ final class ChatAttachmentHandler: ChatAttachmentHandlerProtocol {
5454

5555
// MARK: - State
5656

57-
private let state = ChatAttachmentState()
57+
private let state: ChatAttachmentState
5858

5959
var linkedObjectsPublisher: AnyPublisher<[ChatLinkedObject], Never> {
6060
state.linkedObjectsPublisher
@@ -91,6 +91,7 @@ final class ChatAttachmentHandler: ChatAttachmentHandlerProtocol {
9191

9292
init(spaceId: String) {
9393
self.spaceId = spaceId
94+
self.state = ChatAttachmentState(spaceId: spaceId)
9495
}
9596

9697
// MARK: - Public Methods
@@ -111,11 +112,8 @@ final class ChatAttachmentHandler: ChatAttachmentHandlerProtocol {
111112
AnytypeAnalytics.instance().logDetachItemChat()
112113
}
113114

114-
func clearAll() {
115-
state.clearAllLinkedObjects()
116-
state.clearPhotosItems()
117-
state.updatePhotosItemsTask()
118-
state.cancelAllLinkPreviewTasks()
115+
func clearState() {
116+
state.clearState()
119117
}
120118

121119
func canAddOneAttachment() -> Bool {

Anytype/Sources/PresentationLayer/Modules/Chat/ChatAttachmentState.swift

Lines changed: 117 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,23 @@ import _PhotosUI_SwiftUI
77

88
@MainActor
99
final class ChatAttachmentState {
10-
11-
nonisolated init() { }
10+
11+
@Injected(\.fileActionsService)
12+
private var fileActionsService: any FileActionsServiceProtocol
1213

1314
private let linkedObjectsSubject = CurrentValueSubject<[ChatLinkedObject], Never>([])
1415
private let attachmentsDownloadingSubject = CurrentValueSubject<Bool, Never>(false)
1516
private let photosItemsTaskSubject = CurrentValueSubject<UUID, Never>(UUID())
1617
private var linkPreviewTasks: [URL: AnyCancellable] = [:]
18+
private var preloadTasks: [Int: Task<Void, Never>] = [:]
1719
private var photosItems: [PhotosPickerItem] = []
1820

21+
let spaceId: String
22+
23+
nonisolated init(spaceId: String) {
24+
self.spaceId = spaceId
25+
}
26+
1927
var linkedObjectsPublisher: AnyPublisher<[ChatLinkedObject], Never> {
2028
linkedObjectsSubject.eraseToAnyPublisher()
2129
}
@@ -36,29 +44,6 @@ final class ChatAttachmentState {
3644
linkedObjectsSubject.send(objects)
3745
}
3846

39-
func addLinkedObject(_ object: ChatLinkedObject) {
40-
var current = linkedObjectsSubject.value
41-
current.append(object)
42-
linkedObjectsSubject.send(current)
43-
}
44-
45-
func removeLinkedObject(with id: Int) {
46-
var current = linkedObjectsSubject.value
47-
current.removeAll { $0.id == id }
48-
linkedObjectsSubject.send(current)
49-
}
50-
51-
func updateLinkedObject(at index: Int, with object: ChatLinkedObject) {
52-
var current = linkedObjectsSubject.value
53-
guard index < current.count else { return }
54-
current[index] = object
55-
linkedObjectsSubject.send(current)
56-
}
57-
58-
func clearAllLinkedObjects() {
59-
linkedObjectsSubject.send([])
60-
}
61-
6247
func setAttachmentsDownloading(_ downloading: Bool) {
6348
attachmentsDownloadingSubject.send(downloading)
6449
}
@@ -75,11 +60,6 @@ final class ChatAttachmentState {
7560
linkPreviewTasks[url] = nil
7661
}
7762

78-
func cancelAllLinkPreviewTasks() {
79-
linkPreviewTasks.values.forEach { $0.cancel() }
80-
linkPreviewTasks.removeAll()
81-
}
82-
8363
func hasLinkPreviewTask(for url: URL) -> Bool {
8464
return linkPreviewTasks[url] != nil
8565
}
@@ -96,7 +76,113 @@ final class ChatAttachmentState {
9676
photosItems.removeAll(where: predicate)
9777
}
9878

99-
func clearPhotosItems() {
79+
func clearState() {
80+
preloadTasks.values.forEach { $0.cancel() }
81+
preloadTasks.removeAll()
82+
83+
discardPreloadedFiles(linkedObjects.compactMap { $0.preloadFileId })
84+
85+
linkedObjectsSubject.send([])
86+
87+
linkPreviewTasks.values.forEach { $0.cancel() }
88+
linkPreviewTasks.removeAll()
89+
10090
photosItems = []
91+
92+
updatePhotosItemsTask()
93+
}
94+
95+
// MARK: - Preload Management
96+
97+
func addLinkedObject(_ linkedObject: ChatLinkedObject) {
98+
storeLinkedObject(linkedObject)
99+
startPreload(linkedObject: linkedObject)
100+
}
101+
102+
private func storeLinkedObject(_ object: ChatLinkedObject) {
103+
var current = linkedObjectsSubject.value
104+
current.append(object)
105+
linkedObjectsSubject.send(current)
106+
}
107+
108+
func updateLinkedObject(at index: Int, with linkedObject: ChatLinkedObject) {
109+
updateLinkedObjectStorage(at: index, with: linkedObject)
110+
startPreload(linkedObject: linkedObject)
111+
}
112+
113+
private func updateLinkedObjectStorage(at index: Int, with object: ChatLinkedObject) {
114+
var current = linkedObjectsSubject.value
115+
guard index < current.count else { return }
116+
current[index] = object
117+
linkedObjectsSubject.send(current)
118+
}
119+
120+
func removeLinkedObject(with id: Int) {
121+
if let objectToRemove = linkedObjects.first(where: { $0.id == id }) {
122+
discardPreloadedFile(from: objectToRemove)
123+
}
124+
removeLinkedObjectFromStorage(with: id)
125+
}
126+
127+
private func removeLinkedObjectFromStorage(with id: Int) {
128+
var current = linkedObjectsSubject.value
129+
current.removeAll { $0.id == id }
130+
linkedObjectsSubject.send(current)
131+
}
132+
133+
private func startPreload(linkedObject: ChatLinkedObject) {
134+
guard let data = linkedObject.fileData else { return }
135+
136+
let task = Task { [weak self] in
137+
guard let self = self else { return }
138+
139+
if let preloadFileId = try? await fileActionsService.preloadFileObject(spaceId: spaceId, data: data, origin: .none) {
140+
await MainActor.run { self.updatePreloadFileId(for: linkedObject.id, preloadFileId: preloadFileId) }
141+
}
142+
await MainActor.run { self.removePreloadTask(objectId: linkedObject.id) }
143+
}
144+
145+
addPreloadTask(objectId: linkedObject.id, task: task)
146+
}
147+
148+
private func updatePreloadFileId(for objectId: Int, preloadFileId: String) {
149+
var linkedObjects = linkedObjectsSubject.value
150+
guard let index = linkedObjects.firstIndex(where: { $0.id == objectId }) else { return }
151+
152+
switch linkedObjects[index] {
153+
case .localPhotosFile(var file):
154+
file.data?.preloadFileId = preloadFileId
155+
linkedObjects[index] = .localPhotosFile(file)
156+
case .localBinaryFile(var file):
157+
file.preloadFileId = preloadFileId
158+
linkedObjects[index] = .localBinaryFile(file)
159+
default:
160+
return
161+
}
162+
163+
linkedObjectsSubject.send(linkedObjects)
164+
}
165+
166+
private func addPreloadTask(objectId: Int, task: Task<Void, Never>) {
167+
removePreloadTask(objectId: objectId)
168+
preloadTasks[objectId] = task
169+
}
170+
171+
private func removePreloadTask(objectId: Int) {
172+
preloadTasks[objectId]?.cancel()
173+
preloadTasks[objectId] = nil
174+
}
175+
176+
private func discardPreloadedFile(from linkedObject: ChatLinkedObject) {
177+
guard let preloadFileId = linkedObject.preloadFileId else { return }
178+
discardPreloadedFiles([preloadFileId])
179+
}
180+
181+
private func discardPreloadedFiles(_ preloadFileIds: [String]) {
182+
for preloadFileId in preloadFileIds {
183+
Task {
184+
try await fileActionsService.discardPreloadFile(fileId: preloadFileId, spaceId: spaceId)
185+
}
186+
}
101187
}
102188
}

Anytype/Sources/PresentationLayer/Modules/Chat/ChatViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ final class ChatViewModel: ObservableObject, MessageModuleOutput, ChatActionProv
659659

660660
private func clearInput() {
661661
message = NSAttributedString()
662-
attachmentHandler.clearAll()
662+
attachmentHandler.clearState()
663663
replyToMessage = nil
664664
editMessage = nil
665665
}

Anytype/Sources/PresentationLayer/Modules/Chat/Models/ChatLinkedObject.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import Foundation
22
import Services
33

44
struct ChatLocalPhotosFile: Equatable {
5-
let data: FileData?
5+
var data: ChatLocalBinaryFile?
66
let photosPickerItemHash: Int
77
}
88

9+
struct ChatLocalBinaryFile: Equatable {
10+
let data: FileData
11+
var preloadFileId: String?
12+
}
13+
914
enum ChatLinkedObject: Identifiable, Equatable {
1015
case uploadedObject(MessageAttachmentDetails)
1116
case localPhotosFile(ChatLocalPhotosFile)
12-
case localBinaryFile(FileData)
17+
case localBinaryFile(ChatLocalBinaryFile)
1318
case localBookmark(ChatLocalBookmark)
1419

1520
var id: Int {
@@ -19,7 +24,7 @@ enum ChatLinkedObject: Identifiable, Equatable {
1924
case .localPhotosFile(let file):
2025
return file.photosPickerItemHash
2126
case .localBinaryFile(let file):
22-
return file.path.hashValue
27+
return file.data.path.hashValue
2328
case .localBookmark(let bookmark):
2429
return bookmark.hashValue
2530
}
@@ -51,4 +56,27 @@ enum ChatLinkedObject: Identifiable, Equatable {
5156
return nil
5257
}
5358
}
59+
60+
var preloadFileId: String? {
61+
switch self {
62+
case .localPhotosFile(let file):
63+
return file.data?.preloadFileId
64+
case .localBinaryFile(let file):
65+
return file.preloadFileId
66+
default:
67+
return nil
68+
}
69+
}
70+
71+
var fileData: FileData? {
72+
switch self {
73+
case .localPhotosFile(let file):
74+
return file.data?.data
75+
case .localBinaryFile(let file):
76+
return file.data
77+
case .localBookmark, .uploadedObject:
78+
return nil
79+
}
80+
81+
}
5482
}

Anytype/Sources/PresentationLayer/Modules/Chat/Subviews/InputPanel/Attachments/Local/ChatInputLocalFile.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import UIKit
33

44
struct ChatInputLocalFile: View {
55

6-
let fileData: FileData
6+
let fileData: ChatLocalBinaryFile
77
let onTapObject: () -> Void
88
let onTapRemove: () -> Void
99

@@ -16,15 +16,15 @@ struct ChatInputLocalFile: View {
1616

1717
@ViewBuilder
1818
private var content: some View {
19-
switch fileData.chatViewType {
19+
switch fileData.data.chatViewType {
2020
case .image:
21-
ChatInputLocalImageView(contentsOfFile: fileData.path, onTapRemove: onTapRemove)
22-
.id(fileData.path)
21+
ChatInputLocalImageView(contentsOfFile: fileData.data.path, onTapRemove: onTapRemove)
22+
.id(fileData.data.path)
2323
case .file:
24-
ChatInputLocalBinaryFileView(path: fileData.path, type: fileData.type, sizeInBytes: fileData.sizeInBytes, onTapRemove: onTapRemove)
24+
ChatInputLocalBinaryFileView(path: fileData.data.path, type: fileData.data.type, sizeInBytes: fileData.data.sizeInBytes, onTapRemove: onTapRemove)
2525
case .video:
26-
ChatInputVideoView(url: URL(fileURLWithPath: fileData.path), onTapRemove: onTapRemove)
27-
.id(fileData.path)
26+
ChatInputVideoView(url: URL(fileURLWithPath: fileData.data.path), onTapRemove: onTapRemove)
27+
.id(fileData.data.path)
2828
}
2929
}
3030
}

Anytype/Sources/ServiceLayer/Block/File/FileActionsService.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,6 @@ final class FileActionsService: FileActionsServiceProtocol, Sendable {
165165
}
166166

167167
func preloadFileObject(spaceId: String, data: FileData, origin: ObjectOrigin) async throws -> String {
168-
defer {
169-
if data.isTemporary {
170-
try? FileManager.default.removeItem(atPath: data.path)
171-
}
172-
}
173-
174168
return try await fileService.preloadFileObject(path: data.path, spaceId: spaceId, origin: origin)
175169
}
176170

0 commit comments

Comments
 (0)