From 93e5b4ddedfa8733de941063d3d608824a28b3fb Mon Sep 17 00:00:00 2001 From: Bruno Muniz Azevedo Filho <bruno@elixxir.io> Date: Thu, 23 Jun 2022 16:15:32 -0300 Subject: [PATCH] Implemented file transfers on new models --- .../Controllers/SingleChatController.swift | 20 +- .../ChatFeature/Helpers/BubbleBuilder.swift | 10 +- .../Helpers/CellConfigurator.swift | 369 +++++++++--------- .../ViewModels/SingleChatViewModel.swift | 15 +- .../Controller/ChatListController.swift | 100 ++--- .../ChatListSearchTableController.swift | 159 ++++---- .../Controller/ChatListTableController.swift | 243 ++++++------ Sources/ChatListFeature/Models/Chat.swift | 35 -- .../ViewModel/ChatListViewModel.swift | 177 +++++---- .../ViewModels/ContactListViewModel.swift | 1 + Sources/Integration/Client.swift | 53 +-- Sources/Integration/Extensions.swift | 4 +- .../Implementations/TransferManager.swift | 44 ++- .../Interfaces/TransferManagerInterface.swift | 11 +- Sources/Integration/Listeners.swift | 2 +- Sources/Integration/Mocks/BindingsMock.swift | 2 +- .../Mocks/TransferManagerMock.swift | 15 +- .../Integration/Session/Session+Chat.swift | 328 +++++++--------- .../Session/Session+Contacts.swift | 10 +- .../Integration/Session/Session+Group.swift | 78 ++-- Sources/Integration/Session/Session+UD.swift | 13 + Sources/Integration/Session/Session.swift | 75 ++-- Sources/Integration/Session/SessionType.swift | 1 + .../RequestsReceivedViewModel.swift | 71 ++-- .../Service/RestoreService.swift | 38 -- Sources/Shared/Extensions/FileManager.swift | 3 +- 26 files changed, 890 insertions(+), 987 deletions(-) delete mode 100644 Sources/ChatListFeature/Models/Chat.swift delete mode 100644 Sources/RestoreFeature/Service/RestoreService.swift diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index cf4d5595..4dac8847 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -441,10 +441,14 @@ public final class SingleChatController: UIViewController { } private func previewItemAt(_ indexPath: IndexPath) { -// let item = sections[indexPath.section].elements[indexPath.item] -// guard let attachment = item.payload.attachment, item.status != .receiving else { return } -// fileURL = FileManager.url(for: "\(attachment.name).\(attachment._extension.written)") -// coordinator.toPreview(from: self) + let item = sections[indexPath.section].elements[indexPath.item] + guard let ftid = item.fileTransferId, + item.status != .receiving, + item.status != .receivingFailed else { return } + + let ft = viewModel.getFileTransferWith(id: ftid) + fileURL = FileManager.url(for: "\(ft.name).\(ft.type)") + coordinator.toPreview(from: self) } // MARK: Selectors @@ -495,10 +499,10 @@ extension SingleChatController: UICollectionViewDataSource { let performReply: () -> Void = { [weak self] in self?.viewModel.didRequestReply(item) } let factory = CellFactory.combined(factories: [ - .incomingImage(), - .outgoingImage(), - .incomingAudio(voxophone: voxophone), - .outgoingAudio(voxophone: voxophone), + .incomingImage(transfer: viewModel.getFileTransferWith(id:)), + .outgoingImage(transfer: viewModel.getFileTransferWith(id:)), + .incomingAudio(voxophone: voxophone, transfer: viewModel.getFileTransferWith(id:)), + .outgoingAudio(voxophone: voxophone, transfer: viewModel.getFileTransferWith(id:)), .incomingText(performReply: performReply, showRound: showRound), .outgoingText(performReply: performReply, showRound: showRound), .outgoingFailedText(performReply: performReply), diff --git a/Sources/ChatFeature/Helpers/BubbleBuilder.swift b/Sources/ChatFeature/Helpers/BubbleBuilder.swift index 81aa5c03..32b16480 100644 --- a/Sources/ChatFeature/Helpers/BubbleBuilder.swift +++ b/Sources/ChatFeature/Helpers/BubbleBuilder.swift @@ -42,13 +42,13 @@ final class Bubbler { static func build( imageBubble: ImageMessageView, - with item: Message + with message: Message, + with transfer: FileTransfer ) { -// let progress = item.payload.attachment!.progress -// imageBubble.progressLabel.text = String(format: "%.1f%%", progress * 100) -// imageBubble.dateLabel.text = item.date.asHoursAndMinutes() + imageBubble.progressLabel.text = String(format: "%.1f%%", transfer.progress * 100) + imageBubble.dateLabel.text = message.date.asHoursAndMinutes() - switch item.status { + switch message.status { case .received: imageBubble.lockerImageView.removeFromSuperview() imageBubble.backgroundColor = Asset.neutralWhite.color diff --git a/Sources/ChatFeature/Helpers/CellConfigurator.swift b/Sources/ChatFeature/Helpers/CellConfigurator.swift index 63ab95fd..d0eb1671 100644 --- a/Sources/ChatFeature/Helpers/CellConfigurator.swift +++ b/Sources/ChatFeature/Helpers/CellConfigurator.swift @@ -40,197 +40,206 @@ extension CellFactory { extension CellFactory { static func incomingAudio( - voxophone: Voxophone + voxophone: Voxophone, + transfer: @escaping (Data) -> FileTransfer ) -> Self { - fatalError() -// .init( -// canBuild: { item in -// (item.status == .received || item.status == .receiving) -// && item.payload.reply == nil -// -// }, build: { item, collectionView, indexPath in -// guard let attachment = item.payload.attachment else { fatalError() } -// -// let cell: IncomingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// let url = FileManager.url(for: "\(attachment.name).\(attachment._extension.written)")! -// -// var model = AudioMessageCellState( -// date: item.date, -// audioURL: url, -// isPlaying: false, -// transferProgress: attachment.progress, -// isLoudspeaker: false, -// duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, -// playbackTime: 0.0 -// ) -// -// cell.leftView.setup(with: model) -// cell.canReply = false -// cell.performReply = {} -// -// Bubbler.build(audioBubble: cell.leftView, with: item) -// -// voxophone.$state -// .sink { -// switch $0 { -// case .playing(url, _, time: let time, _): -// model.isPlaying = true -// model.playbackTime = time -// default: -// model.isPlaying = false -// model.playbackTime = 0.0 -// } -// -// model.isLoudspeaker = $0.isLoudspeaker -// -// cell.leftView.setup(with: model) -// }.store(in: &cell.leftView.cancellables) -// -// cell.leftView.didTapRight = { -// guard item.status != .receiving else { return } -// -// voxophone.toggleLoudspeaker() -// } -// -// cell.leftView.didTapLeft = { -// guard item.status != .receiving else { return } -// -// if case .playing(url, _, _, _) = voxophone.state { -// voxophone.reset() -// } else { -// voxophone.load(url) -// voxophone.play() -// } -// } -// -// return cell -// } -// ) + .init( + canBuild: { item in + guard (item.status == .received || item.status == .receiving), + item.replyMessageId == nil, + item.fileTransferId != nil else { return false } + + return transfer(item.fileTransferId!).type == "m4a" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: IncomingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + let url = FileManager.url(for: ft.name)! + + var model = AudioMessageCellState( + date: item.date, + audioURL: url, + isPlaying: false, + transferProgress: ft.progress, + isLoudspeaker: false, + duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, + playbackTime: 0.0 + ) + + cell.leftView.setup(with: model) + cell.canReply = false + cell.performReply = {} + + Bubbler.build(audioBubble: cell.leftView, with: item) + + voxophone.$state + .sink { + switch $0 { + case .playing(url, _, time: let time, _): + model.isPlaying = true + model.playbackTime = time + default: + model.isPlaying = false + model.playbackTime = 0.0 + } + + model.isLoudspeaker = $0.isLoudspeaker + + cell.leftView.setup(with: model) + }.store(in: &cell.leftView.cancellables) + + cell.leftView.didTapRight = { + guard item.status != .receiving else { return } + + voxophone.toggleLoudspeaker() + } + + cell.leftView.didTapLeft = { + guard item.status != .receiving else { return } + + if case .playing(url, _, _, _) = voxophone.state { + voxophone.reset() + } else { + voxophone.load(url) + voxophone.play() + } + } + + return cell + } + ) } static func outgoingAudio( - voxophone: Voxophone + voxophone: Voxophone, + transfer: @escaping (Data) -> FileTransfer ) -> Self { - fatalError() -// .init( -// canBuild: { item in -// (item.status == .sent || -// item.status == .sending || -// item.status == .sendingFailed || -// item.status == .sendingTimedOut) -// && item.payload.reply == nil -// && item.payload.attachment != nil -// && item.payload.attachment?._extension == .audio -// -// }, build: { item, collectionView, indexPath in -// guard let attachment = item.payload.attachment else { fatalError() } -// -// let cell: OutgoingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// let url = FileManager.url(for: "\(attachment.name).\(attachment._extension.written)")! -// var model = AudioMessageCellState( -// date: item.date, -// audioURL: url, -// isPlaying: false, -// transferProgress: attachment.progress, -// isLoudspeaker: false, -// duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, -// playbackTime: 0.0 -// ) -// -// cell.rightView.setup(with: model) -// cell.canReply = false -// cell.performReply = {} -// -// Bubbler.build(audioBubble: cell.rightView, with: item) -// -// voxophone.$state -// .sink { -// switch $0 { -// case .playing(url, _, time: let time, _): -// model.isPlaying = true -// model.playbackTime = time -// default: -// model.isPlaying = false -// model.playbackTime = 0.0 -// } -// -// model.isLoudspeaker = $0.isLoudspeaker -// -// cell.rightView.setup(with: model) -// }.store(in: &cell.rightView.cancellables) -// -// cell.rightView.didTapRight = { -// voxophone.toggleLoudspeaker() -// } -// -// cell.rightView.didTapLeft = { -// if case .playing(url, _, _, _) = voxophone.state { -// voxophone.reset() -// } else { -// voxophone.load(url) -// voxophone.play() -// } -// } -// -// return cell -// } -// ) + .init( + canBuild: { item in + guard (item.status == .sent || + item.status == .sending || + item.status == .sendingFailed || + item.status == .sendingTimedOut) + && item.replyMessageId == nil + && item.fileTransferId != nil else { + return false + } + + return transfer(item.fileTransferId!).type == "m4a" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: OutgoingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + let url = FileManager.url(for: ft.name)! + var model = AudioMessageCellState( + date: item.date, + audioURL: url, + isPlaying: false, + transferProgress: ft.progress, + isLoudspeaker: false, + duration: (try? AVAudioPlayer(contentsOf: url).duration) ?? 0.0, + playbackTime: 0.0 + ) + + cell.rightView.setup(with: model) + cell.canReply = false + cell.performReply = {} + + Bubbler.build(audioBubble: cell.rightView, with: item) + + voxophone.$state + .sink { + switch $0 { + case .playing(url, _, time: let time, _): + model.isPlaying = true + model.playbackTime = time + default: + model.isPlaying = false + model.playbackTime = 0.0 + } + + model.isLoudspeaker = $0.isLoudspeaker + + cell.rightView.setup(with: model) + }.store(in: &cell.rightView.cancellables) + + cell.rightView.didTapRight = { + voxophone.toggleLoudspeaker() + } + + cell.rightView.didTapLeft = { + if case .playing(url, _, _, _) = voxophone.state { + voxophone.reset() + } else { + voxophone.load(url) + voxophone.play() + } + } + + return cell + } + ) } } extension CellFactory { - static func outgoingImage() -> Self { - fatalError() -// .init( -// canBuild: { item in -// (item.status == .sent || -// item.status == .sending || -// item.status == .sendingFailed || -// item.status == .sendingTimedOut) -// && item.payload.reply == nil -// && item.payload.attachment != nil -// && item.payload.attachment?._extension == .image -// -// }, build: { item, collectionView, indexPath in -// guard let attachment = item.payload.attachment else { fatalError() } -// -// let cell: OutgoingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// -// Bubbler.build(imageBubble: cell.rightView, with: item) -// -// cell.canReply = false -// cell.performReply = {} -// -// if let image = UIImage(data: attachment.data!) { -// cell.rightView.imageView.image = UIImage(cgImage: image.cgImage!, scale: image.scale, orientation: .up) -// } -// -// return cell -// } -// ) + static func outgoingImage( + transfer: @escaping (Data) -> FileTransfer + ) -> Self { + .init( + canBuild: { item in + guard (item.status == .sent || + item.status == .sending || + item.status == .sendingFailed || + item.status == .sendingTimedOut) + && item.replyMessageId == nil + && item.fileTransferId != nil else { + return false + } + + return transfer(item.fileTransferId!).type == "png" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: OutgoingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(imageBubble: cell.rightView, with: item, with: transfer(item.fileTransferId!)) + cell.canReply = false + cell.performReply = {} + + if let image = UIImage(data: ft.data!) { + cell.rightView.imageView.image = UIImage(cgImage: image.cgImage!, scale: image.scale, orientation: .up) + } + + return cell + } + ) } - static func incomingImage() -> Self { - fatalError() -// .init( -// canBuild: { item in -// (item.status == .received || item.status == .receiving) -// && item.payload.reply == nil -// && item.payload.attachment != nil -// && item.payload.attachment?._extension == .image -// -// }, build: { item, collectionView, indexPath in -// guard let attachment = item.payload.attachment else { fatalError() } -// -// let cell: IncomingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) -// -// Bubbler.build(imageBubble: cell.leftView, with: item) -// cell.canReply = false -// cell.performReply = {} -// cell.leftView.imageView.image = UIImage(data: attachment.data!) -// return cell -// } -// ) + static func incomingImage( + transfer: @escaping (Data) -> FileTransfer + ) -> Self { + .init( + canBuild: { item in + guard (item.status == .received || item.status == .receiving) + && item.replyMessageId == nil + && item.fileTransferId != nil else { + return false + } + + return transfer(item.fileTransferId!).type == "png" + + }, build: { item, collectionView, indexPath in + let ft = transfer(item.fileTransferId!) + let cell: IncomingImageCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) + + Bubbler.build(imageBubble: cell.leftView, with: item, with: ft) + cell.canReply = false + cell.performReply = {} + cell.leftView.imageView.image = UIImage(data: ft.data!) + return cell + } + ) } } diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index c6809677..860e084b 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -93,13 +93,16 @@ final class SingleChatViewModel { // MARK: Public + func getFileTransferWith(id: Data) -> FileTransfer { + guard let transfer = try? session.dbManager.fetchFileTransfers(.init(id: [id])).first else { + fatalError() + } + + return transfer + } + func didSendAudio(url: URL) { -// let name = url.deletingPathExtension().lastPathComponent -// guard let file = FileManager.retrieve(name: name, type: Attachment.Extension.audio.written) else { return } -// -// let attachment = Attachment(name: name, data: file, _extension: .audio) -// let payload = Payload(text: "You sent a voice message", reply: nil, attachment: attachment) -// session.send(payload, toContact: contact) + session.sendFile(url: url, to: contact) } func didSend(image: UIImage) { diff --git a/Sources/ChatListFeature/Controller/ChatListController.swift b/Sources/ChatListFeature/Controller/ChatListController.swift index cd12b4ea..5fbb432a 100644 --- a/Sources/ChatListFeature/Controller/ChatListController.swift +++ b/Sources/ChatListFeature/Controller/ChatListController.swift @@ -76,10 +76,10 @@ public final class ChatListController: UIViewController { } }.store(in: &cancellables) -// viewModel.badgeCountPublisher -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in topLeftView.updateBadge($0) } -// .store(in: &cancellables) + viewModel.badgeCountPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in topLeftView.updateBadge($0) } + .store(in: &cancellables) topLeftView.actionPublisher .receive(on: DispatchQueue.main) @@ -129,12 +129,12 @@ public final class ChatListController: UIViewController { screenView.listContainerView.collectionView.delegate = self screenView.listContainerView.collectionView.dataSource = collectionDataSource -// viewModel.recentsPublisher -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in -// collectionDataSource.apply($0) -// shouldBeShowingRecents = $0.numberOfItems > 0 -// }.store(in: &cancellables) + viewModel.recentsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + collectionDataSource.apply($0) + shouldBeShowingRecents = $0.numberOfItems > 0 + }.store(in: &cancellables) } private func setupBindings() { @@ -153,33 +153,33 @@ public final class ChatListController: UIViewController { screenView.searchListContainerView.emptyView.updateSearched(content: query) }.store(in: &cancellables) -// Publishers.CombineLatest( -// viewModel.searchPublisher, -// screenView.searchView.textPublisher.removeDuplicates() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] items, query in -// guard query.isEmpty == false else { -// screenView.searchListContainerView.isHidden = true -// screenView.listContainerView.isHidden = false -// screenView.bringSubviewToFront(screenView.listContainerView) -// return -// } -// -// screenView.listContainerView.isHidden = true -// screenView.searchListContainerView.isHidden = false -// -// guard items.numberOfItems > 0 else { -// screenView.searchListContainerView.emptyView.isHidden = false -// screenView.bringSubviewToFront(screenView.searchListContainerView) -// screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) -// return -// } -// -// screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) -// screenView.searchListContainerView.emptyView.isHidden = true -// } -// .store(in: &cancellables) + Publishers.CombineLatest( + viewModel.searchPublisher, + screenView.searchView.textPublisher.removeDuplicates() + ) + .receive(on: DispatchQueue.main) + .sink { [unowned self] items, query in + guard query.isEmpty == false else { + screenView.searchListContainerView.isHidden = true + screenView.listContainerView.isHidden = false + screenView.bringSubviewToFront(screenView.listContainerView) + return + } + + screenView.listContainerView.isHidden = true + screenView.searchListContainerView.isHidden = false + + guard items.numberOfItems > 0 else { + screenView.searchListContainerView.emptyView.isHidden = false + screenView.bringSubviewToFront(screenView.searchListContainerView) + screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) + return + } + + screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) + screenView.searchListContainerView.emptyView.isHidden = true + } + .store(in: &cancellables) screenView.searchView .isEditingPublisher @@ -188,19 +188,19 @@ public final class ChatListController: UIViewController { .sink { [unowned self] in isEditingSearch = $0 } .store(in: &cancellables) -// viewModel.chatsPublisher -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in -// guard $0.isEmpty == false else { -// screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) -// screenView.listContainerView.emptyView.isHidden = false -// return -// } -// -// screenView.listContainerView.bringSubviewToFront(tableController.view) -// screenView.listContainerView.emptyView.isHidden = true -// } -// .store(in: &cancellables) + viewModel.chatsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard $0.isEmpty == false else { + screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) + screenView.listContainerView.emptyView.isHidden = false + return + } + + screenView.listContainerView.bringSubviewToFront(tableController.view) + screenView.listContainerView.emptyView.isHidden = true + } + .store(in: &cancellables) screenView.searchListContainerView .emptyView.searchButton diff --git a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift index dbef4946..e8f07fbf 100644 --- a/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListSearchTableController.swift @@ -4,16 +4,16 @@ import Models import Combine import DependencyInjection -//class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { -// override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { -// switch snapshot().sectionIdentifiers[section] { -// case .chats: -// return "CHATS" -// case .connections: -// return "CONNECTIONS" -// } -// } -//} +class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch snapshot().sectionIdentifiers[section] { + case .chats: + return "CHATS" + case .connections: + return "CONNECTIONS" + } + } +} final class ChatSearchTableController: UITableViewController { @Dependency private var coordinator: ChatListCoordinating @@ -21,65 +21,52 @@ final class ChatSearchTableController: UITableViewController { private let viewModel: ChatListViewModel private let cellHeight: CGFloat = 83.0 private var cancellables = Set<AnyCancellable>() -// private var tableDataSource: ChatSearchListTableViewDiffableDataSource? + private var tableDataSource: ChatSearchListTableViewDiffableDataSource? init(_ viewModel: ChatListViewModel) { self.viewModel = viewModel super.init(style: .grouped) -// tableDataSource = ChatSearchListTableViewDiffableDataSource( -// tableView: tableView -// ) { table, indexPath, item in -// let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) -// switch item { -// case .chat(let subitem): -// if case .contact(let info) = subitem { -// cell.setupContact( -// name: info.contact.nickname ?? info.contact.username, -// image: info.contact.photo, -// date: Date.fromTimestamp(info.lastMessage!.timestamp), -// hasUnread: info.lastMessage!.unread, -// preview: info.lastMessage!.payload.text -// ) -// } -// -// if case .group(let info) = subitem { -// let date: Date = { -// guard let lastMessage = info.lastMessage else { -// return info.group.createdAt -// } -// -// return Date.fromTimestamp(lastMessage.timestamp) -// }() -// -// let hasUnread: Bool = { -// guard let lastMessage = info.lastMessage else { -// return false -// } -// -// return lastMessage.unread -// }() -// -// cell.setupGroup( -// name: info.group.name, -// date: date, -// preview: info.lastMessage?.payload.text, -// hasUnread: hasUnread -// ) -// } -// -// case .connection(let contact): -// cell.setupContact( -// name: contact.nickname ?? contact.username!, -// image: contact.photo, -// date: nil, -// hasUnread: false, -// preview: contact.username! -// ) -// } -// -// return cell -// } + tableDataSource = ChatSearchListTableViewDiffableDataSource( + tableView: tableView + ) { table, indexPath, item in + let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) + switch item { + case .chat(let info): + switch info { + case .group: + fatalError() + + case .groupChat(let groupChatInfo): + cell.setupGroup( + name: groupChatInfo.group.name, + date: groupChatInfo.lastMessage.date, + preview: groupChatInfo.lastMessage.text, + hasUnread: groupChatInfo.lastMessage.isUnread + ) + + case .contactChat(let contactChatInfo): + cell.setupContact( + name: (contactChatInfo.contact.nickname ?? contactChatInfo.contact.username) ?? "", + image: contactChatInfo.contact.photo, + date: contactChatInfo.lastMessage.date, + hasUnread: contactChatInfo.lastMessage.isUnread, + preview: contactChatInfo.lastMessage.text + ) + } + + case .connection(let contact): + cell.setupContact( + name: contact.nickname ?? contact.username!, + image: contact.photo, + date: nil, + hasUnread: false, + preview: contact.username ?? "" + ) + } + + return cell + } } required init?(coder: NSCoder) { nil } @@ -91,13 +78,13 @@ final class ChatSearchTableController: UITableViewController { tableView.tableFooterView = UIView() tableView.sectionIndexColor = .blue tableView.register(ChatListCell.self) -// tableView.dataSource = tableDataSource + tableView.dataSource = tableDataSource view.backgroundColor = Asset.neutralWhite.color -// viewModel.searchPublisher -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } -// .store(in: &cancellables) + viewModel.searchPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } + .store(in: &cancellables) } } @@ -110,19 +97,25 @@ extension ChatSearchTableController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// if let item = tableDataSource?.itemIdentifier(for: indexPath) { -// switch item { -// case .chat(let chat): -// switch chat { -// case .contact(let info): -// guard info.contact.status == .friend else { return } -// coordinator.toSingleChat(with: info.contact, from: self) -// case .group(let info): -// coordinator.toGroupChat(with: info, from: self) -// } -// case .connection(let contact): -// coordinator.toContact(contact, from: self) -// } -// } + if let item = tableDataSource?.itemIdentifier(for: indexPath) { + switch item { + case .chat(let chatInfo): + switch chatInfo { + case .group: + fatalError() + + case .groupChat: + fatalError() + + //coordinator.toGroupChat(with: info, from: self) + case .contactChat(let info): + guard info.contact.authStatus == .friend else { return } + coordinator.toSingleChat(with: info.contact, from: self) + } + + case .connection(let contact): + coordinator.toContact(contact, from: self) + } + } } } diff --git a/Sources/ChatListFeature/Controller/ChatListTableController.swift b/Sources/ChatListFeature/Controller/ChatListTableController.swift index ab8f5ec4..f20902ad 100644 --- a/Sources/ChatListFeature/Controller/ChatListTableController.swift +++ b/Sources/ChatListFeature/Controller/ChatListTableController.swift @@ -2,14 +2,19 @@ import UIKit import Shared import Models import Combine +import XXModels import DifferenceKit import DrawerFeature import DependencyInjection +extension ChatInfo: Differentiable { + public var differenceIdentifier: ChatInfo.ID { id } +} + final class ChatListTableController: UITableViewController { @Dependency private var coordinator: ChatListCoordinating - private var rows = [Any]() + private var rows = [ChatInfo]() private let viewModel: ChatListViewModel private let cellHeight: CGFloat = 83.0 private var cancellables = Set<AnyCancellable>() @@ -31,28 +36,28 @@ final class ChatListTableController: UITableViewController { tableView.register(ChatListCell.self) tableView.tableFooterView = UIView() -// viewModel -// .chatsPublisher -// .receive(on: DispatchQueue.main) -// .sink { [unowned self] in -// guard !self.rows.isEmpty else { -// self.rows = $0 -// tableView.reloadData() -// return -// } -// -// self.tableView.reload( -// using: StagedChangeset(source: self.rows, target: $0), -// deleteSectionsAnimation: .automatic, -// insertSectionsAnimation: .automatic, -// reloadSectionsAnimation: .none, -// deleteRowsAnimation: .automatic, -// insertRowsAnimation: .automatic, -// reloadRowsAnimation: .none -// ) { [unowned self] in -// self.rows = $0 -// } -// }.store(in: &cancellables) + viewModel + .chatsPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] in + guard !self.rows.isEmpty else { + self.rows = $0 + tableView.reloadData() + return + } + + self.tableView.reload( + using: StagedChangeset(source: self.rows, target: $0), + deleteSectionsAnimation: .automatic, + insertSectionsAnimation: .automatic, + reloadSectionsAnimation: .none, + deleteRowsAnimation: .automatic, + insertRowsAnimation: .automatic, + reloadRowsAnimation: .none + ) { [unowned self] in + self.rows = $0 + } + }.store(in: &cancellables) } } @@ -78,7 +83,7 @@ extension ChatListTableController { let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in guard let self = self else { return } -// self.didRequestDeletionOf(self.rows[indexPath.row]) + self.didRequestDeletionOf(self.rows[indexPath.row]) complete(true) } @@ -88,13 +93,18 @@ extension ChatListTableController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// switch rows[indexPath.row] { -// case .contact(let info): -// guard info.contact.status == .friend else { return } -// coordinator.toSingleChat(with: info.contact, from: self) -// case .group(let info): -// coordinator.toGroupChat(with: info, from: self) -// } + switch rows[indexPath.row] { + case .group: + fatalError() + + case .groupChat: + fatalError() + //coordinator.toGroupChat(with: info, from: self) + + case .contactChat(let info): + guard info.contact.authStatus == .friend else { return } + coordinator.toSingleChat(with: info.contact, from: self) + } } override func tableView( @@ -102,97 +112,86 @@ extension ChatListTableController { cellForRowAt indexPath: IndexPath ) -> UITableViewCell { -// let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) -// -// if case .contact(let info) = rows[indexPath.row] { -// cell.setupContact( -// name: info.contact.nickname ?? info.contact.username, -// image: info.contact.photo, -// date: Date.fromTimestamp(info.lastMessage!.timestamp), -// hasUnread: info.lastMessage!.unread, -// preview: info.lastMessage!.payload.text -// ) -// } -// -// if case .group(let info) = rows[indexPath.row] { -// let date: Date = { -// guard let lastMessage = info.lastMessage else { -// return info.group.createdAt -// } -// -// return Date.fromTimestamp(lastMessage.timestamp) -// }() -// -// let hasUnread: Bool = { -// guard let lastMessage = info.lastMessage else { -// return false -// } -// -// return lastMessage.unread -// }() -// -// cell.setupGroup( -// name: info.group.name, -// date: date, -// preview: info.lastMessage?.payload.text, -// hasUnread: hasUnread -// ) -// } -// -// return cell - fatalError() + let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) + + switch rows[indexPath.row] { + case .group: + fatalError() + + case .groupChat(let info): + cell.setupGroup( + name: info.group.name, + date: info.lastMessage.date, + preview: info.lastMessage.text, + hasUnread: info.lastMessage.isUnread + ) + + case .contactChat(let info): + cell.setupContact( + name: (info.contact.nickname ?? info.contact.username) ?? "", + image: info.contact.photo, + date: info.lastMessage.date, + hasUnread: info.lastMessage.isUnread, + preview: info.lastMessage.text + ) + } + + return cell } -// private func didRequestDeletionOf(_ item: Chat) { -// let title: String -// let subtitle: String -// let actionTitle: String -// let actionClosure: () -> Void -// -// switch item { -// case .group(let info): -// title = Localized.ChatList.DeleteGroup.title -// subtitle = Localized.ChatList.DeleteGroup.subtitle -// actionTitle = Localized.ChatList.DeleteGroup.action -// actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } -// -// case .contact(let info): -// title = Localized.ChatList.Delete.title -// subtitle = Localized.ChatList.Delete.subtitle -// actionTitle = Localized.ChatList.Delete.delete -// actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } -// } -// -// let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) -// -// let drawer = DrawerController(with: [ -// DrawerText( -// font: Fonts.Mulish.bold.font(size: 26.0), -// text: title, -// color: Asset.neutralActive.color, -// alignment: .left, -// spacingAfter: 19 -// ), -// DrawerText( -// font: Fonts.Mulish.regular.font(size: 16.0), -// text: subtitle, -// color: Asset.neutralBody.color, -// alignment: .left, -// lineHeightMultiple: 1.1, -// spacingAfter: 39 -// ), -// actionButton -// ]) -// -// actionButton.action.receive(on: DispatchQueue.main) -// .sink { -// drawer.dismiss(animated: true) { [weak self] in -// guard let self = self else { return } -// self.drawerCancellables.removeAll() -// actionClosure() -// } -// }.store(in: &drawerCancellables) -// -// coordinator.toDrawer(drawer, from: self) -// } + private func didRequestDeletionOf(_ item: ChatInfo) { + let title: String + let subtitle: String + let actionTitle: String + let actionClosure: () -> Void + + switch item { + case .group: + fatalError() + + case .contactChat(let info): + title = Localized.ChatList.Delete.title + subtitle = Localized.ChatList.Delete.subtitle + actionTitle = Localized.ChatList.Delete.delete + actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } + + case .groupChat(let info): + title = Localized.ChatList.DeleteGroup.title + subtitle = Localized.ChatList.DeleteGroup.subtitle + actionTitle = Localized.ChatList.DeleteGroup.action + actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } + } + + let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) + + let drawer = DrawerController(with: [ + DrawerText( + font: Fonts.Mulish.bold.font(size: 26.0), + text: title, + color: Asset.neutralActive.color, + alignment: .left, + spacingAfter: 19 + ), + DrawerText( + font: Fonts.Mulish.regular.font(size: 16.0), + text: subtitle, + color: Asset.neutralBody.color, + alignment: .left, + lineHeightMultiple: 1.1, + spacingAfter: 39 + ), + actionButton + ]) + + actionButton.action.receive(on: DispatchQueue.main) + .sink { + drawer.dismiss(animated: true) { [weak self] in + guard let self = self else { return } + self.drawerCancellables.removeAll() + actionClosure() + } + }.store(in: &drawerCancellables) + + coordinator.toDrawer(drawer, from: self) + } } diff --git a/Sources/ChatListFeature/Models/Chat.swift b/Sources/ChatListFeature/Models/Chat.swift deleted file mode 100644 index 80106ee6..00000000 --- a/Sources/ChatListFeature/Models/Chat.swift +++ /dev/null @@ -1,35 +0,0 @@ -//import Models -//import XXModels -//import Foundation -//import DifferenceKit -// -//enum Chat: Equatable, Differentiable, Hashable { -// case group(GroupChatInfo) -// case contact(SingleChatInfo) -// -// var differenceIdentifier: Data { -// switch self { -// case .contact(let info): -// return info.contact.userId -// case .group(let info): -// return info.group.groupId -// } -// } -// -// var orderingDate: Date { -// switch self { -// case .group(let info): -// if let lastMessage = info.lastMessage { -// return Date.fromTimestamp(lastMessage.timestamp) -// } else { -// return info.group.createdAt -// } -// case .contact(let info): -// guard let lastMessage = info.lastMessage else { -// fatalError("Should have an E2E chat without a last message") -// } -// -// return Date.fromTimestamp(lastMessage.timestamp) -// } -// } -//} diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index a7cffa12..2f8817aa 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -13,13 +13,13 @@ enum SearchSection { case connections } -//enum SearchItem: Equatable, Hashable { -// case chat(Chat) -// case connection(Contact) -//} +enum SearchItem: Equatable, Hashable { + case chat(ChatInfo) + case connection(Contact) +} typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact> -//typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> +typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> final class ChatListViewModel { @Dependency private var session: SessionType @@ -28,96 +28,105 @@ final class ChatListViewModel { session.isOnline } -// var chatsPublisher: AnyPublisher<[Chat], Never> { -// chatsSubject.eraseToAnyPublisher() -// } + var chatsPublisher: AnyPublisher<[ChatInfo], Never> { + chatsSubject.eraseToAnyPublisher() + } var hudPublisher: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() } -// var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { -// session.contacts(.isRecent).map { -// let section = SectionId() -// var snapshot = RecentsSnapshot() -// snapshot.appendSections([section]) -// snapshot.appendItems($0, toSection: section) -// return snapshot -// }.eraseToAnyPublisher() -// } - -// var searchPublisher: AnyPublisher<SearchSnapshot, Never> { -// Publishers.CombineLatest3( -// session.contacts(.all), -// chatsPublisher, -// searchSubject -// .removeDuplicates() -// .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) -// .eraseToAnyPublisher() -// ) -// .map { (contacts, chats, query) in -// let connectionItems = contacts.filter { -// let username = $0.username.lowercased().contains(query.lowercased()) -// let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false -// return username || nickname -// }.map(SearchItem.connection) -// -// let chatItems = chats.filter { -// switch $0 { -// case .contact(let info): -// let username = info.contact.username.lowercased().contains(query.lowercased()) -// let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false -// let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false -// return username || nickname || lastMessage -// -// case .group(let info): -// let name = info.group.name.lowercased().contains(query.lowercased()) -// let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false -// return name || last -// } -// }.map(SearchItem.chat) -// -// var snapshot = SearchSnapshot() -// -// if connectionItems.count > 0 { -// snapshot.appendSections([.connections]) -// snapshot.appendItems(connectionItems, toSection: .connections) -// } -// -// if chatItems.count > 0 { -// snapshot.appendSections([.chats]) -// snapshot.appendItems(chatItems, toSection: .chats) -// } -// -// return snapshot -// }.eraseToAnyPublisher() -// } -// -// var badgeCountPublisher: AnyPublisher<Int, Never> { -// Publishers.CombineLatest( -// session.contacts(.received), -// session.groups(.pending) -// ) -// .map { $0.0.count + $0.1.count } -// .eraseToAnyPublisher() -// } + var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { + session.dbManager.fetchContactsPublisher(.init(isRecent: true)) + .assertNoFailure() + .map { + let section = SectionId() + var snapshot = RecentsSnapshot() + snapshot.appendSections([section]) + snapshot.appendItems($0, toSection: section) + return snapshot + }.eraseToAnyPublisher() + } + + var searchPublisher: AnyPublisher<SearchSnapshot, Never> { + Publishers.CombineLatest3( + session.dbManager.fetchContactsPublisher(.init()).assertNoFailure(), + chatsPublisher, + searchSubject + .removeDuplicates() + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) + .eraseToAnyPublisher() + ) + .map { (contacts, chats, query) in + let connectionItems = contacts.filter { + let username = $0.username?.lowercased().contains(query.lowercased()) ?? false + let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false + return username || nickname + }.map(SearchItem.connection) + + let chatItems = chats.filter { + switch $0 { + case .group: + fatalError() + + case .groupChat(let info): + let name = info.group.name.lowercased().contains(query.lowercased()) + let last = info.lastMessage.text.lowercased().contains(query.lowercased()) + return name || last + + case .contactChat(let info): + let username = info.contact.username?.lowercased().contains(query.lowercased()) ?? false + let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false + let lastMessage = info.lastMessage.text.lowercased().contains(query.lowercased()) + return username || nickname || lastMessage + + } + }.map(SearchItem.chat) + + var snapshot = SearchSnapshot() + + if connectionItems.count > 0 { + snapshot.appendSections([.connections]) + snapshot.appendItems(connectionItems, toSection: .connections) + } + + if chatItems.count > 0 { + snapshot.appendSections([.chats]) + snapshot.appendItems(chatItems, toSection: .chats) + } + + return snapshot + }.eraseToAnyPublisher() + } + + var badgeCountPublisher: AnyPublisher<Int, Never> { + let groupQuery = Group.Query(authStatus: [.pending]) + let contactsQuery = Contact.Query(authStatus: [ + .verified, + .confirming, + .confirmationFailed, + .verificationFailed, + .verificationInProgress + ]) + + return Publishers.CombineLatest( + session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure(), + session.dbManager.fetchGroupsPublisher(groupQuery).assertNoFailure() + ) + .map { $0.0.count + $0.1.count } + .eraseToAnyPublisher() + } private var cancellables = Set<AnyCancellable>() private let searchSubject = CurrentValueSubject<String, Never>("") -// private let chatsSubject = CurrentValueSubject<[Chat], Never>([]) + private let chatsSubject = CurrentValueSubject<[ChatInfo], Never>([]) private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) init() { -// Publishers.CombineLatest( -// session.singleChats(.all), -// session.groupChats(.accepted) -// ).map { -// let groups = $0.1.map(Chat.group) -// let chats = $0.0.map(Chat.contact) -// return (chats + groups).sorted { $0.orderingDate > $1.orderingDate } -// } -// .sink { [unowned self] in chatsSubject.send($0) } -// .store(in: &cancellables) + session.dbManager.fetchChatInfosPublisher(.init(userId: session.myId)) + .assertNoFailure() + .sink { [unowned self] in chatsSubject.send($0) } + .store(in: &cancellables) } func updateSearch(query: String) { diff --git a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift index 3ef0466b..04674460 100644 --- a/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift +++ b/Sources/ContactListFeature/ViewModels/ContactListViewModel.swift @@ -10,6 +10,7 @@ final class ContactListViewModel { var contacts: AnyPublisher<[Contact], Never> { session.dbManager.fetchContactsPublisher(.init(authStatus: [.friend])) .assertNoFailure() + .map { $0.filter { $0.id != self.session.myId }} .eraseToAnyPublisher() } diff --git a/Sources/Integration/Client.swift b/Sources/Integration/Client.swift index 34b4cc4f..28ed1a64 100644 --- a/Sources/Integration/Client.swift +++ b/Sources/Integration/Client.swift @@ -34,6 +34,7 @@ public class Client { private let eventsSubject = PassthroughSubject<BackendEvent, Never>() private let requestsSentSubject = PassthroughSubject<Contact, Never>() private let confirmationsSubject = PassthroughSubject<Contact, Never>() + private let transfersSubject = PassthroughSubject<FileTransfer, Never>() private let groupRequestsSubject = PassthroughSubject<(Group, [Data], String?), Never>() private var isBackupInitialization = false @@ -176,31 +177,33 @@ public class Client { } private func instantiateTransferManager() throws { -// transferManager = try bindings.generateTransferManager { [weak transfersSubject] tid, name, type, sender in -// -// /// Someone transfered something to me -// /// but I haven't received yet. I'll store an -// /// IncomingTransfer object so later on I can -// /// pull up whatever this contact has sent me. -// /// -// guard let name = name, -// let type = type, -// let contact = sender, -// let _extension = Attachment.Extension.from(type) else { -// log(string: "Transfer of \(name ?? "nil").\(type ?? "nil") is being dismissed", type: .error) -// return -// } -// -// transfersSubject?.send( -// FileTransfer( -// tid: tid, -// contact: contact, -// fileName: name, -// fileType: _extension.written, -// isIncoming: true -// ) -// ) -// } + transferManager = try bindings.generateTransferManager { [weak transfersSubject] tid, name, type, sender in + + /// Someone transfered something to me + /// but I haven't received yet. I'll store an + /// IncomingTransfer object so later on I can + /// pull up whatever this contact has sent me. + /// + guard let name = name, + let type = type, + let contactId = sender else { + log(string: "Transfer of \(name ?? "nil").\(type ?? "nil") is being dismissed", type: .error) + return + } + + transfersSubject?.send( + FileTransfer( + id: tid, + contactId: contactId, + name: name, + type: type, + data: nil, + progress: 0.0, + isIncoming: true, + createdAt: Date() + ) + ) + } } private func instantiateUserDiscovery() throws { diff --git a/Sources/Integration/Extensions.swift b/Sources/Integration/Extensions.swift index 6fd221f6..f2afdcb9 100644 --- a/Sources/Integration/Extensions.swift +++ b/Sources/Integration/Extensions.swift @@ -20,13 +20,13 @@ extension Contact { } extension Message { - init(with message: BindingsMessage, meMarshalled: Data) { + init(with message: BindingsMessage, myId: Data) { guard let payload = try? Payload(with: message.getPayload()!) else { fatalError() } self.init( networkId: message.getID()!, senderId: message.getSender()!, - recipientId: meMarshalled, + recipientId: myId, groupId: nil, date: Date.fromTimestamp(Int(message.getTimestampNano())), status: .received, diff --git a/Sources/Integration/Implementations/TransferManager.swift b/Sources/Integration/Implementations/TransferManager.swift index cdf6e874..6b161800 100644 --- a/Sources/Integration/Implementations/TransferManager.swift +++ b/Sources/Integration/Implementations/TransferManager.swift @@ -38,24 +38,28 @@ extension BindingsFileTransfer: TransferManagerInterface { try receive(id) } -// public func uploadFile( -// _ file: Attachment, -// to recipient: Data, -// _ callback: @escaping (Bool, Int, Int, Int, Error?) -> Void -// ) throws -> Data { -// let cb = OutgoingTransferProgressCallback { completed, sent, arrived, total, error in -// callback(completed, sent, arrived, total, error) -// } -// -// return try send( -// file.name, -// fileType: file._extension.written, -// fileData: file.data!, -// recipientID: recipient, -// retry: 1, -// preview: nil, -// progressFunc: cb, -// periodMS: 1000 -// ) -// } + public func uploadFile( + url: URL, + to recipient: Data, + _ callback: @escaping (Bool, Int, Int, Int, Error?) -> Void + ) throws -> Data { + let cb = OutgoingTransferProgressCallback { completed, sent, arrived, total, error in + callback(completed, sent, arrived, total, error) + } + + guard let file = FileManager.retrieve(name: url.lastPathComponent, type: url.pathExtension) else { + fatalError() + } + + return try send( + url.lastPathComponent, + fileType: url.pathExtension, + fileData: file, + recipientID: recipient, + retry: 1, + preview: nil, + progressFunc: cb, + periodMS: 1000 + ) + } } diff --git a/Sources/Integration/Interfaces/TransferManagerInterface.swift b/Sources/Integration/Interfaces/TransferManagerInterface.swift index 6c0fa09d..b95d1ee3 100644 --- a/Sources/Integration/Interfaces/TransferManagerInterface.swift +++ b/Sources/Integration/Interfaces/TransferManagerInterface.swift @@ -1,4 +1,3 @@ -import Models import Foundation public protocol TransferManagerInterface { @@ -20,9 +19,9 @@ public protocol TransferManagerInterface { with: Data ) throws -> Data -// func uploadFile( -// _: Attachment, -// to: Data, -// _: @escaping (Bool, Int, Int, Int, Error?) -> Void -// ) throws -> Data + func uploadFile( + url: URL, + to: Data, + _: @escaping (Bool, Int, Int, Int, Error?) -> Void + ) throws -> Data } diff --git a/Sources/Integration/Listeners.swift b/Sources/Integration/Listeners.swift index 9ade68b4..7ab87760 100644 --- a/Sources/Integration/Listeners.swift +++ b/Sources/Integration/Listeners.swift @@ -42,7 +42,7 @@ public extension BindingsClient { let listener = TextListener { bindingsMessage in guard let message = bindingsMessage else { return } - let domainModel = Message(with: message, meMarshalled: self.meMarshalled) + let domainModel = Message(with: message, myId: self.myId) callback(domainModel) } diff --git a/Sources/Integration/Mocks/BindingsMock.swift b/Sources/Integration/Mocks/BindingsMock.swift index 8161105f..61ccceb4 100644 --- a/Sources/Integration/Mocks/BindingsMock.swift +++ b/Sources/Integration/Mocks/BindingsMock.swift @@ -120,7 +120,7 @@ public final class BindingsMock: BindingsInterface { self?.requestsSubject.send(.carlRequested) self?.requestsSubject.send(.angelinaRequested) self?.requestsSubject.send(.elonRequested) - //self?.groupRequestsSubject.send(.mockGroup) + self?.groupRequestsSubject.send(.mockGroup) DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in self?.confirmationsSubject.send(.georgeDiscovered) diff --git a/Sources/Integration/Mocks/TransferManagerMock.swift b/Sources/Integration/Mocks/TransferManagerMock.swift index ccf16c1b..9b87da5d 100644 --- a/Sources/Integration/Mocks/TransferManagerMock.swift +++ b/Sources/Integration/Mocks/TransferManagerMock.swift @@ -1,4 +1,3 @@ -import Models import Foundation final class TransferManagerMock: TransferManagerInterface { @@ -24,11 +23,11 @@ final class TransferManagerMock: TransferManagerInterface { fatalError() } -// func uploadFile( -// _: Attachment, -// to: Data, -// _: @escaping (Bool, Int, Int, Int, Error?) -> Void -// ) throws -> Data { -// Data() -// } + func uploadFile( + url: URL, + to: Data, + _: @escaping (Bool, Int, Int, Int, Error?) -> Void + ) throws -> Data { + Data() + } } diff --git a/Sources/Integration/Session/Session+Chat.swift b/Sources/Integration/Session/Session+Chat.swift index 1d60e9f2..b53018f9 100644 --- a/Sources/Integration/Session/Session+Chat.swift +++ b/Sources/Integration/Session/Session+Chat.swift @@ -6,30 +6,82 @@ import Foundation extension Session { public func send(imageData: Data, to contact: Contact, completion: @escaping (Result<Void, Error>) -> Void) { -// client.bindings.compress(image: imageData) { [weak self] in -// guard let self = self else { -// completion(.success(())) -// return -// } -// -// switch $0 { -// case .success(let compressed): -// let name = "image_\(Date.asTimestamp)" -// try! FileManager.store(data: compressed, name: name, type: Attachment.Extension.image.written) -// let attachment = Attachment(name: name, data: compressed, _extension: .image) -// self.send(Payload(text: "You sent an image", reply: nil, attachment: attachment), toContact: contact) -// completion(.success(())) -// case .failure(let error): -// completion(.failure(error)) -// log(string: "Error when compressing image: \(error.localizedDescription)", type: .error) -// } -// } + client.bindings.compress(image: imageData) { [weak self] result in + guard let self = self else { + completion(.success(())) + return + } + + switch result { + case .success(let compressedImage): + do { + let url = try FileManager.store( + data: compressedImage, + name: "image_\(Date.asTimestamp)", + type: "png" + ) + + self.sendFile(url: url, to: contact) + completion(.success(())) + } catch { + completion(.failure(error)) + } + + case .failure(let error): + completion(.failure(error)) + log(string: "Error when compressing image: \(error.localizedDescription)", type: .error) + } + } + } + + public func sendFile(url: URL, to contact: Contact) { + guard let manager = client.transferManager else { fatalError("A transfer manager was not created") } + + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + var tid: Data! + + do { + tid = try manager.uploadFile(url: url, to: contact.id) { completed, send, arrived, total, error in + if completed { + self.endTransferWith(tid: tid) + } else { + if error != nil { + self.failTransferWith(tid: tid) + } else { + self.progressTransferWith(tid: tid, arrived: Float(arrived), total: Float(total)) + } + } + } + + let content = url.pathExtension == "m4a" ? "a voice message" : "an image" + + let message = Message( + networkId: nil, + senderId: self.client.bindings.myId, + recipientId: contact.id, + groupId: nil, + date: Date(), + status: .sending, + isUnread: false, + text: "You sent \(content)", + replyMessageId: nil, + roundURL: nil, + fileTransferId: tid + ) + + _ = try? self.dbManager.saveMessage(message) + } catch { + print(error.localizedDescription) + } + } } public func send(_ payload: Payload, toContact contact: Contact) { var message = Message( networkId: nil, - senderId: client.bindings.meMarshalled, + senderId: client.bindings.myId, recipientId: contact.id, groupId: nil, date: Date(), @@ -54,11 +106,8 @@ extension Session { message.status = .sending message.date = Date() - do { - message = try dbManager.saveMessage(message) + if let message = try? dbManager.saveMessage(message) { send(message: message) - } catch { - print(error.localizedDescription) } } } @@ -66,11 +115,6 @@ extension Session { private func send(message: Message) { var message = message -// if let _ = message.payload.attachment { -// sendAttachment(message: message) -// return -// } - DispatchQueue.global().async { [weak self] in guard let self = self else { return } switch self.client.bindings.send(message.text.data(using: .utf8)!, to: message.recipientId!) { @@ -117,158 +161,78 @@ extension Session { } } -// private func sendAttachment(message: Message) { -// guard let manager = client.transferManager else { fatalError("A transfer manager was not created") } -// -// var message = message -// let attachment = message.payload.attachment! -// -// DispatchQueue.global().async { [weak self] in -// guard let self = self else { return } -// -// do { -// let tid = try manager.uploadFile(attachment, to: message.receiver) { completed, send, arrived, total, error in -// if completed { -// self.endTransferFrom(message: message) -// message.status = .sent -// message.payload.attachment?.progress = 1.0 -// log(string: "FT Up finished", type: .info) -// } else { -// if let error = error { -// log(string: error.localizedDescription, type: .error) -// message.status = .failedToSend -// } else { -// let progress = Float(arrived)/Float(total) -// message.payload.attachment?.progress = progress -// log(string: "FT Up: \(progress)", type: .crumbs) -// } -// } -// -// do { -// _ = try self.dbManager.save(message) // If it fails here, means the chat was cleared. -// } catch { -// log(string: error.localizedDescription, type: .error) -// } -// } -// -// let transfer = FileTransfer( -// tid: tid, -// contact: message.receiver, -// fileName: attachment.name, -// fileType: attachment._extension.written, -// isIncoming: false -// ) -// -// message.payload.attachment?.transferId = tid -// message.status = .sending -// -// do { -// _ = try self.dbManager.saveMessage(message) -// _ = try self.dbManager.save(transfer) -// } catch { -// log(string: error.localizedDescription, type: .error) -// } -// } catch { -// message.status = .sendingFailed -// log(string: error.localizedDescription, type: .error) -// -// do { -// _ = try self.dbManager.saveMessage(message) -// } catch let otherError { -// log(string: otherError.localizedDescription, type: .error) -// } -// } -// } -// } -// -// private func endTransferFrom(message: Message) { -// guard let manager = client.transferManager else { fatalError("A transfer manager was not created") } -// guard let tid = message.payload.attachment?.transferId else { fatalError("Tried to finish a transfer that had no TID") } -// -// do { -// try manager.endTransferUpload(with: tid) -// -// if let transfer: FileTransfer = try? dbManager.fetch(.withTID(tid)).first { -// try dbManager.delete(transfer) -// } -// } catch { -// log(string: error.localizedDescription, type: .error) -// } -// } -// -// func handle(incomingTransfer transfer: FileTransfer) { -// guard let manager = client.transferManager else { fatalError("A transfer manager was not created") } -// -// let fileExtension: Attachment.Extension = transfer.fileType == "m4a" ? .audio : .image -// let name = "\(Date.asTimestamp)_\(transfer.fileName)" -// -// var fakeContent: Data -// -// if fileExtension == .image { -// fakeContent = Asset.transferImagePlaceholder.image.jpegData(compressionQuality: 0.1)! -// } else { -// fakeContent = FileManager.dummyAudio() -// } -// -// let attachment = Attachment(name: name, data: fakeContent, transferId: transfer.tid, _extension: fileExtension) -// -// var message = Message( -// sender: transfer.contact, -// receiver: client.bindings.meMarshalled, -// payload: .init(text: "Sent you a \(fileExtension.writtenExtended)", reply: nil, attachment: attachment), -// unread: true, -// timestamp: Date.asTimestamp, -// uniqueId: nil, -// status: .receivingAttachment -// ) -// -// do { -// message = try self.dbManager.saveMessage(message) -// try self.dbManager.save(transfer) -// } catch { -// log(string: "Failed to save message/transfer to the database. Will not start listening to transfer... \(error.localizedDescription)", type: .info) -// return -// } -// -// log(string: "FT Down starting", type: .info) -// -// try! manager.listenDownloadFromTransfer(with: transfer.tid) { completed, arrived, total, error in -// if let error = error { -// fatalError(error.localizedDescription) -// } -// -// if completed { -// log(string: "FT Down finished", type: .info) -// -// guard let rawFile = try? manager.downloadFileFromTransfer(with: transfer.tid) else { -// log(string: "Received finalized transfer, file was nil. Ignoring...", type: .error) -// return -// } -// -// try! FileManager.store(data: rawFile, name: name, type: fileExtension.written) -// var realAttachment = Attachment(name: name, data: rawFile, transferId: transfer.tid, _extension: fileExtension) -// realAttachment.progress = 1.0 -// message.payload = .init(text: "Sent you a \(transfer.fileType)", reply: nil, attachment: realAttachment) -// message.status = .received -// -// if let toDelete: FileTransfer = try? self.dbManager.fetch(.withTID(transfer.tid)).first { -// do { -// try self.dbManager.delete(toDelete) -// } catch { -// log(string: error.localizedDescription, type: .error) -// } -// } -// } else { -// let progress = Float(arrived)/Float(total) -// log(string: "FT Down: \(progress)", type: .crumbs) -// message.payload.attachment?.progress = progress -// } -// -// do { -// try self.dbManager.save(message) // If it fails here, means the chat was cleared. -// } catch { -// log(string: "Failed to update message model from an incoming transfer. Probably chat was cleared: \(error.localizedDescription)", type: .error) -// } -// } -// } + private func endTransferWith(tid: Data) { + guard let manager = client.transferManager else { + fatalError("A transfer manager was not created") + } + + try? manager.endTransferUpload(with: tid) + + if var message = try? dbManager.fetchMessages(.init(fileTransferId: tid)).first { + message.status = .sent + _ = try? dbManager.saveMessage(message) + } + + if var transfer = try? dbManager.fetchFileTransfers(.init(id: [tid])).first { + transfer.progress = 1.0 + _ = try? dbManager.saveFileTransfer(transfer) + } + } + + private func failTransferWith(tid: Data) { + if var message = try? dbManager.fetchMessages(.init(fileTransferId: tid)).first { + message.status = .sendingFailed + _ = try? dbManager.saveMessage(message) + } + } + + private func progressTransferWith(tid: Data, arrived: Float, total: Float) { + if var transfer = try? dbManager.fetchFileTransfers(.init(id: [tid])).first { + transfer.progress = arrived/total + _ = try? dbManager.saveFileTransfer(transfer) + } + } + + func handle(incomingTransfer transfer: FileTransfer) { + guard let manager = client.transferManager else { + fatalError("A transfer manager was not created") + } + + let content = transfer.type == "m4a" ? "a voice message" : "an image" + + var message = Message( + networkId: nil, + senderId: transfer.contactId, + recipientId: myId, + groupId: nil, + date: transfer.createdAt, + status: .receiving, + isUnread: true, + text: "Sent you \(content)", + replyMessageId: nil, + roundURL: nil, + fileTransferId: transfer.id + ) + + message = try! self.dbManager.saveMessage(message) + + try! manager.listenDownloadFromTransfer(with: transfer.id) { completed, arrived, total, error in + if let error = error { fatalError(error.localizedDescription) } + + if completed { + guard let rawFile = try? manager.downloadFileFromTransfer(with: transfer.id) else { return } + _ = try! FileManager.store(data: rawFile, name: transfer.name, type: transfer.type) + + var transfer = transfer + transfer.data = rawFile + transfer.progress = 1.0 + _ = try? self.dbManager.saveFileTransfer(transfer) + + message.status = .received + _ = try? self.dbManager.saveMessage(message) + } else { + self.progressTransferWith(tid: transfer.id, arrived: Float(arrived), total: Float(total)) + } + } + } } diff --git a/Sources/Integration/Session/Session+Contacts.swift b/Sources/Integration/Session/Session+Contacts.swift index e24d1496..08787e2c 100644 --- a/Sources/Integration/Session/Session+Contacts.swift +++ b/Sources/Integration/Session/Session+Contacts.swift @@ -195,13 +195,9 @@ extension Session { } public func deleteContact(_ contact: Contact) throws { - // TODO: If there's an ongoing FT with this contact, it cannot be deleted. - -// if let _: FileTransfer = try? dbManager.fetch(.withContactId(contact.userId)).first { -// throw NSError.create("There is an ongoing file transfer with this contact as you are receiving or sending a file, please try again later once it’s done") -// } else { -// print("No pending transfer with this contact. Free to delete") -// } + if !(try dbManager.fetchFileTransfers(.init(contactId: contact.id))).isEmpty { + throw NSError.create("There is an ongoing file transfer with this contact as you are receiving or sending a file, please try again later once it’s done") + } try client.bindings.removeContact(contact.marshaled!) try dbManager.deleteContact(contact) diff --git a/Sources/Integration/Session/Session+Group.swift b/Sources/Integration/Session/Session+Group.swift index b0bd6efd..e7b96b3b 100644 --- a/Sources/Integration/Session/Session+Group.swift +++ b/Sources/Integration/Session/Session+Group.swift @@ -58,7 +58,7 @@ extension Session { _ = try? dbManager.saveMessage(.init( networkId: nil, senderId: group.leaderId, - recipientId: client.bindings.meMarshalled, + recipientId: client.bindings.myId, groupId: group.id, date: Date(), status: .received, @@ -72,7 +72,7 @@ extension Session { /// Buzz if the group was not created by me /// - if group.leaderId != client.bindings.meMarshalled, inappnotifications { + if group.leaderId != client.bindings.myId, inappnotifications { DeviceFeedback.sound(.contactAdded) DeviceFeedback.shake(.notification) } @@ -92,7 +92,7 @@ extension Session { extension Session { public func send(_ payload: Payload, toGroup group: Group) { var message = Message( - senderId: client.bindings.meMarshalled, + senderId: client.bindings.myId, recipientId: nil, groupId: group.id, date: Date(), @@ -151,51 +151,31 @@ extension Session { } public func scanStrangers(_ completion: @escaping () -> Void) { - // TODO: Needs a request for Contacts without username set - -// DispatchQueue.global().async { [weak self] in -// guard let self = self, let ud = self.client.userDiscovery else { return } -// -// guard let strangers = try? self.dbManager.fetchContacts(.init(authStatus: [.stranger])), -// strangers.isEmpty == false else { -// DispatchQueue.main.async { completion() } -// return -// } -// -// let ids = strangers.map { $0.id } -// -// var updatedStrangers: [GroupMember] = [] -// -// ud.lookup(idList: ids) { -// switch $0 { -// case .success(let contacts): -// strangers.forEach { stranger in -// if let found = contacts.first(where: { contact in contact.id == stranger.id }) { -// var updatedStranger = stranger -// updatedStranger.username = found.username -// updatedStrangers.append(updatedStranger) -// } -// } -// -// DispatchQueue.main.async { -// updatedStrangers.forEach { -// do { -// try self.dbManager.saveContact($0) -// } catch { -// log(string: error.localizedDescription, type:.error) -// } -// } -// -// log(string: "Scanned unknown group members", type: .info) -// completion() -// } -// case .failure(let error): -// DispatchQueue.main.async { -// log(string: error.localizedDescription, type: .error) -// completion() -// } -// } -// } -// } + DispatchQueue.global().async { [weak self] in + guard let self = self, + let ud = self.client.userDiscovery, + let strangers = try? self.dbManager.fetchContacts(.init(username: .some(nil))), + !strangers.isEmpty else { return } + + ud.lookup(idList: strangers.map(\.id)) { result in + switch result { + case .success(let strangersWithUsernames): + let acquaintances = strangers.map { stranger -> Contact in + var exStranger = stranger + exStranger.username = strangersWithUsernames.first(where: { $0.id == stranger.id })?.username + return exStranger + } + + DispatchQueue.main.async { + acquaintances.forEach { _ = try? self.dbManager.saveContact($0) } + } + + completion() + case .failure(let error): + print(error.localizedDescription) + DispatchQueue.main.async { completion() } + } + } + } } } diff --git a/Sources/Integration/Session/Session+UD.swift b/Sources/Integration/Session/Session+UD.swift index 57422a7d..6dd7472f 100644 --- a/Sources/Integration/Session/Session+UD.swift +++ b/Sources/Integration/Session/Session+UD.swift @@ -41,6 +41,19 @@ extension Session { switch $0 { case .success(_): + _ = try? self.dbManager.saveContact(.init( + id: self.client.bindings.myId, + marshaled: self.client.bindings.meMarshalled, + username: value, + email: nil, + phone: nil, + nickname: nil, + photo: nil, + authStatus: .friend, + isRecent: false, + createdAt: Date() + )) + self.username = value completion(.success(nil)) case .failure(let error): diff --git a/Sources/Integration/Session/Session.swift b/Sources/Integration/Session/Session.swift index 0bad0077..7077acec 100644 --- a/Sources/Integration/Session/Session.swift +++ b/Sources/Integration/Session/Session.swift @@ -170,38 +170,49 @@ public final class Session: SessionType { } private func registerUnfinishedTransfers() { -// guard let unfinisheds: [Message] = try? dbManager.fetch(.sendingAttachment), !unfinisheds.isEmpty else { return } -// -// for var message in unfinisheds { -// guard let tid = message.payload.attachment?.transferId else { return } -// -// do { -// try client.transferManager?.listenUploadFromTransfer(with: tid) { completed, sent, arrived, total, error in -// if completed { -// message.status = .sent -// message.payload.attachment?.progress = 1.0 -// -// if let transfer: FileTransfer = try? self.dbManager.fetch(.withTID(tid)).first { -// try? self.dbManager.delete(transfer) -// } -// } else { -// if let error = error { -// log(string: error.localizedDescription, type: .error) -// message.status = .failedToSend -// } else { -// let progress = Float(arrived)/Float(total) -// message.payload.attachment?.progress = progress -// return -// } -// } -// -// _ = try? self.dbManager.save(message) -// } -// } catch { -// message.status = .sent -// _ = try? self.dbManager.save(message) -// } -// } + guard let unfinishedSendingMessages = try? dbManager.fetchMessages(.init(status: [.sending])), + let unfinishedSendingTransfers = try? dbManager.fetchFileTransfers(.init( + id: Set(unfinishedSendingMessages + .filter { $0.fileTransferId != nil } + .compactMap(\.fileTransferId)))) + else { return } + + // What would be a good way to do this? + + let pairs = unfinishedSendingMessages.map { message -> (Message, FileTransfer) in + let transfer = unfinishedSendingTransfers.first { ft in + ft.id == message.fileTransferId + } + + return (message, transfer!) + } + + pairs.forEach { message, transfer in + var message = message + var transfer = transfer + + do { + try client.transferManager?.listenUploadFromTransfer(with: transfer.id) { completed, sent, arrived, total, error in + if completed { + transfer.progress = 1.0 + message.status = .sent + + } else { + if error != nil { + message.status = .sendingFailed + } else { + transfer.progress = Float(arrived)/Float(total) + } + } + + _ = try? self.dbManager.saveFileTransfer(transfer) + _ = try? self.dbManager.saveMessage(message) + } + } catch { + message.status = .sendingFailed + _ = try? self.dbManager.saveMessage(message) + } + } } func updateFactsOnBackup() { diff --git a/Sources/Integration/Session/SessionType.swift b/Sources/Integration/Session/SessionType.swift index fbb531ed..e99c6f37 100644 --- a/Sources/Integration/Session/SessionType.swift +++ b/Sources/Integration/Session/SessionType.swift @@ -15,6 +15,7 @@ public protocol SessionType { func deleteMyself() throws func getId(from: Data) -> Data? + func sendFile(url: URL, to: Contact) func send(imageData: Data, to: Contact, completion: @escaping (Result<Void, Error>) -> Void) func verify(contact: Contact) diff --git a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift index 52a4fe66..78c9c7b9 100644 --- a/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift +++ b/Sources/RequestsFeature/ViewModels/RequestsReceivedViewModel.swift @@ -166,48 +166,35 @@ final class RequestsReceivedViewModel { _ group: Group, _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void ) { -// session.scanStrangers { [weak self] in -// guard let self = self else { return } -// -// Publishers.CombineLatest( -// self.session.dbManager.fetchContactsPublisher(.init()).assertNoFailure(), -// self.session.dbManager.fetchGroupInfosPublisher(.init(groupId: group.id)).assertNoFailure() -// ) -// .sink { (allContacts, groupMembers) in -// -// guard !groupMembers.map(\.authStatus).contains(.pendingUsername) else { -// completion(.failure(NSError.create(""))) // Some members are still pending username lookup... -// return -// } -// -// // Now that all members are set with their usernames lets find our friends: -// // -// let contactsAlsoMembers = allContacts.filter { groupMembers.map(\.userId).contains($0.userId) } -// let membersNonContacts = groupMembers.filter { !contactsAlsoMembers.map(\.userId).contains($0.userId) } -// -// var models = [DrawerTableCellModel]() -// -// contactsAlsoMembers.forEach { -// models.append(.init( -// title: $0.nickname ?? $0.username, -// image: $0.photo, -// isCreator: $0.userId == group.leaderId, -// isConnection: true -// )) -// } -// -// membersNonContacts.forEach { -// models.append(.init( -// title: $0.username, -// image: nil, -// isCreator: $0.userId == group.leaderId, -// isConnection: false -// )) -// } -// -// completion(.success(models)) -// }.store(in: &self.cancellables) -// } + if let info = try? session.dbManager.fetchGroupInfos(.init(groupId: group.id)).first { + session.dbManager.fetchContactsPublisher(.init(id: Set(info.members.map(\.id)))) + .assertNoFailure() + .sink { members in + let withUsername = members + .filter { $0.username != nil } + .map { + DrawerTableCellModel( + title: $0.nickname ?? $0.username!, + image: $0.photo, + isCreator: $0.id == group.leaderId, + isConnection: $0.authStatus == .friend + ) + } + + let withoutUsername = members + .filter { $0.username == nil } + .map { + DrawerTableCellModel( + title: "Fetching username...", + image: $0.photo, + isCreator: $0.id == group.leaderId, + isConnection: $0.authStatus == .friend + ) + } + + completion(.success(withUsername + withoutUsername)) + }.store(in: &cancellables) + } } func didRequestHide(contact: Contact) { diff --git a/Sources/RestoreFeature/Service/RestoreService.swift b/Sources/RestoreFeature/Service/RestoreService.swift deleted file mode 100644 index 9bd5b80e..00000000 --- a/Sources/RestoreFeature/Service/RestoreService.swift +++ /dev/null @@ -1,38 +0,0 @@ -//import UIKit -//import Models -//import Combine -// -//import DependencyInjection -// -//public struct RestoreService: RestoreServiceType { -// -// -// -// @Dependency private var coordinator: RestoreCoordinating -// -// public var inProgress: AnyPublisher<Void, Never> { inProgressSubject.eraseToAnyPublisher() } -// public var settings: AnyPublisher<RestoreSettings, Never> { settingsSubject.eraseToAnyPublisher() } -// -// private let inProgressSubject = PassthroughSubject<Void, Never>() -// private let settingsSubject = PassthroughSubject<RestoreSettings, Never>() -// -// private var cancellables = Set<AnyCancellable>() -// -// public init() {} -// -// public func authorize(service: CloudService, from controller: UIViewController) { -// } -// } -// -// public func download( -// from settings: RestoreSettings, -// progress: @escaping RestoreProgress, -// whenFinished: @escaping RestoreDownloadFinished -// ) { -// drive.downloadBackup( -// settings.backup!.id, -// progressCallback: progress, -// whenFinished -// ) -// } -//} diff --git a/Sources/Shared/Extensions/FileManager.swift b/Sources/Shared/Extensions/FileManager.swift index a6512bbb..c8639b5f 100644 --- a/Sources/Shared/Extensions/FileManager.swift +++ b/Sources/Shared/Extensions/FileManager.swift @@ -36,12 +36,13 @@ public extension FileManager { root.appendingPathComponent("\(fileName)") } - static func store(data: Data, name: String, type: String) throws { + static func store(data: Data, name: String, type: String) throws -> URL { guard let url = Self.url(for: "\(name).\(type)") else { throw NSError.create("The file path could not be retrieved") } try data.write(to: url) + return url } static func delete(name: String, type: String) { -- GitLab