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