Skip to content
Snippets Groups Projects
CellConfigurator.swift 14.3 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
    ) -> 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
//            }
//        )
Bruno Muniz's avatar
Bruno Muniz committed
    }

    static func outgoingAudio(
        voxophone: Voxophone
    ) -> 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
//            }
//        )
Bruno Muniz's avatar
Bruno Muniz committed
    }
}

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

    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
//            }
//        )
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) }