Skip to content
Snippets Groups Projects
CellConfigurator.swift 14.5 KiB
Newer Older
Bruno Muniz's avatar
Bruno Muniz committed
import UIKit
import Shared
import Combine
import XXModels
Bruno Muniz's avatar
Bruno Muniz committed
import Voxophone
import AVFoundation

struct CellFactory {
    var canBuild: (Message) -> Bool
Bruno Muniz's avatar
Bruno Muniz committed

    var build: (Message, UICollectionView, IndexPath) -> UICollectionViewCell
Bruno Muniz's avatar
Bruno Muniz committed

    func callAsFunction(
        item: Message,
Bruno Muniz's avatar
Bruno Muniz committed
        collectionView: UICollectionView,
        indexPath: IndexPath
    ) -> UICollectionViewCell {
        build(item, collectionView, indexPath)
    }
}

extension CellFactory {
    static func combined(factories: [CellFactory]) -> Self {
        .init(
            canBuild: { _ in true },
            build: { item, collectionView, indexPath in
                guard let factory = factories.first(where: { $0.canBuild(item)}) else {
                    fatalError("Couldn't find a factory for \(item). Did you forget to implement?")
                }

                return factory(
                    item: item,
                    collectionView: collectionView,
                    indexPath: indexPath
                )
            }
        )
    }
}

extension CellFactory {
    static func incomingAudio(
        voxophone: Voxophone,
        transfer: @escaping (Data) -> FileTransfer
Bruno Muniz's avatar
Bruno Muniz committed
    ) -> 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 == "m4a"

            }, build: { item, collectionView, indexPath in
                let ft = transfer(item.fileTransferId!)
                let cell: IncomingAudioCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
Bruno Muniz's avatar
Bruno Muniz committed
                let url = FileManager.url(for: "\(ft.name).\(ft.type)")!

                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
            }
        )
Bruno Muniz's avatar
Bruno Muniz committed
    }

    static func outgoingAudio(
        voxophone: Voxophone,
        transfer: @escaping (Data) -> FileTransfer
Bruno Muniz's avatar
Bruno Muniz committed
    ) -> 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 == "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
            }
        )
Bruno Muniz's avatar
Bruno Muniz committed
    }
}

extension CellFactory {
    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
                }

Bruno Muniz's avatar
Bruno Muniz committed
                return transfer(item.fileTransferId!).type == "jpeg"

            }, 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(
        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
                }

Bruno Muniz's avatar
Bruno Muniz committed
                return transfer(item.fileTransferId!).type == "jpeg"

            }, 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 = {}
Bruno Muniz's avatar
Bruno Muniz committed

                if let data = ft.data {
                    cell.leftView.imageView.image = UIImage(data: data)
                } else {
                    cell.leftView.imageView.image = Asset.transferImagePlaceholder.image
                }

Bruno Muniz's avatar
Bruno Muniz committed
    }
}

extension CellFactory {
    static func outgoingReply(
        performReply: @escaping () -> Void,
        replyContent: @escaping (Data) -> (String, String),
Bruno Muniz's avatar
Bruno Muniz committed
        showRound: @escaping (String?) -> Void
    ) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sent || item.status == .sending)
                && item.replyMessageId != nil
Bruno Muniz's avatar
Bruno Muniz committed

            }, build: { item, collectionView, indexPath in
                let cell: OutgoingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildReply(
                    bubble: cell.rightView,
                    with: item,
                    reply: replyContent(item.replyMessageId!)
                cell.canReply = item.status == .sent
Bruno Muniz's avatar
Bruno Muniz committed
                cell.performReply = performReply
                cell.rightView.didTapShowRound = { showRound(item.roundURL) }
                return cell
            }
        )
    }

    static func incomingReply(
        performReply: @escaping () -> Void,
        replyContent: @escaping (Data) -> (String, String),
Bruno Muniz's avatar
Bruno Muniz committed
        showRound: @escaping (String?) -> Void
    ) -> Self {
        .init(
            canBuild: { item in
                item.status == .received
                && item.replyMessageId != nil
Bruno Muniz's avatar
Bruno Muniz committed

            }, build: { item, collectionView, indexPath in
                let cell: IncomingReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildReply(
                    bubble: cell.leftView,
                    with: item,
                    reply: replyContent(item.replyMessageId!)
Bruno Muniz's avatar
Bruno Muniz committed
                )
                cell.canReply = item.status == .received
Bruno Muniz's avatar
Bruno Muniz committed
                cell.performReply = performReply
                cell.leftView.didTapShowRound = { showRound(item.roundURL) }
                cell.leftView.revertBottomStackOrder()
                return cell
            }
        )
    }

    static func outgoingFailedReply(
        performReply: @escaping () -> Void,
        replyContent: @escaping (Data) -> (String, String)
Bruno Muniz's avatar
Bruno Muniz committed
    ) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sendingFailed || item.status == .sendingTimedOut)
                && item.replyMessageId != nil
Bruno Muniz's avatar
Bruno Muniz committed

            }, build: { item, collectionView, indexPath in
                let cell: OutgoingFailedReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.buildReply(
                    bubble: cell.rightView,
                    with: item,
                    reply: replyContent(item.replyMessageId!)
                cell.canReply = false
Bruno Muniz's avatar
Bruno Muniz committed
                cell.performReply = performReply
                return cell
            }
        )
    }
}

extension CellFactory {
    static func incomingText(
        performReply: @escaping () -> Void,
        showRound: @escaping (String?) -> Void
    ) -> Self {
        .init(
            canBuild: { item in
                item.status == .received
                && item.replyMessageId == nil
Bruno Muniz's avatar
Bruno Muniz committed

            }, build: { item, collectionView, indexPath in
                let cell: IncomingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.build(bubble: cell.leftView, with: item)
                cell.canReply = item.status == .received
Bruno Muniz's avatar
Bruno Muniz committed
                cell.performReply = performReply
                cell.leftView.didTapShowRound = { showRound(item.roundURL) }
                cell.leftView.revertBottomStackOrder()
                return cell
            }
        )
    }

    static func outgoingText(
        performReply: @escaping () -> Void,
        showRound: @escaping (String?) -> Void
    ) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sending || item.status == .sent)
                && item.replyMessageId == nil
Bruno Muniz's avatar
Bruno Muniz committed

            }, build: { item, collectionView, indexPath in
                let cell: OutgoingTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.build(bubble: cell.rightView, with: item)
                cell.canReply = item.status == .sent
Bruno Muniz's avatar
Bruno Muniz committed
                cell.performReply = performReply
                cell.rightView.didTapShowRound = { showRound(item.roundURL) }

                return cell
            }
        )
    }

    static func outgoingFailedText(performReply: @escaping () -> Void) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sendingFailed || item.status == .sendingTimedOut)
                && item.replyMessageId == nil
Bruno Muniz's avatar
Bruno Muniz committed

            }, build: { item, collectionView, indexPath in
                let cell: OutgoingFailedTextCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)

                Bubbler.build(bubble: cell.rightView, with: item)
                cell.canReply = false
Bruno Muniz's avatar
Bruno Muniz committed
                cell.performReply = performReply
                return cell
            }
        )
    }
}

struct ActionFactory {
    enum Action {
        case copy
        case retry
        case reply
        case delete

        var title: String {
            switch self {

            case .copy:
                return Localized.Chat.BubbleMenu.copy
            case .retry:
                return Localized.Chat.BubbleMenu.retry
            case .reply:
                return Localized.Chat.BubbleMenu.reply
            case .delete:
                return Localized.Chat.BubbleMenu.delete
            }
        }
    }

    static func build(
        from item: Message,
Bruno Muniz's avatar
Bruno Muniz committed
        action: Action,
        closure: @escaping (Message) -> Void
Bruno Muniz's avatar
Bruno Muniz committed
    ) -> UIAction? {

        switch action {
        case .reply:
            guard item.status == .received || item.status == .sent else { return nil }
Bruno Muniz's avatar
Bruno Muniz committed
        case .retry:
            guard item.status == .sendingFailed || item.status == .sendingTimedOut else { return nil }
Bruno Muniz's avatar
Bruno Muniz committed
        case .delete, .copy:
            break
        }

        return UIAction(
            title: action.title,
            state: .off,
            handler: { _ in closure(item) }