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

struct CellFactory {
    var canBuild: (ChatItem) -> Bool

    var build: (ChatItem, UICollectionView, IndexPath) -> UICollectionViewCell

    func callAsFunction(
        item: ChatItem,
        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,
        name: @escaping (Data) -> String,
        text: @escaping (Data) -> String,
        showRound: @escaping (String?) -> Void
    ) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sent || item.status == .sending)
                && item.payload.reply != nil

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

                Bubbler.buildReply(
                    bubble: cell.rightView,
                    with: item,
                    reply: .init(
                        text: text(item.payload.reply!.messageId),
                        sender: name(item.payload.reply!.senderId)
                    )
                )

                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,
        name: @escaping (Data) -> String,
        text: @escaping (Data) -> String,
        showRound: @escaping (String?) -> Void
    ) -> Self {
        .init(
            canBuild: { item in
                item.status == .received
Bruno Muniz's avatar
Bruno Muniz committed
                && item.payload.reply != nil

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

                Bubbler.buildReply(
                    bubble: cell.leftView,
                    with: item,
                    reply: .init(
                        text: text(item.payload.reply!.messageId),
                        sender: name(item.payload.reply!.senderId)
                    )
                )
                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,
        name: @escaping (Data) -> String,
        text: @escaping (Data) -> String
    ) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sendingFailed || item.status == .sendingTimedOut)
Bruno Muniz's avatar
Bruno Muniz committed
                && item.payload.reply != nil

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

                Bubbler.buildReply(
                    bubble: cell.rightView,
                    with: item,
                    reply: .init(
                        text: text(item.payload.reply!.messageId),
                        sender: name(item.payload.reply!.senderId)
                    )
                )

                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
Bruno Muniz's avatar
Bruno Muniz committed
                && item.payload.reply == nil

            }, 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.payload.reply == nil

            }, 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)
Bruno Muniz's avatar
Bruno Muniz committed
                && item.payload.reply == nil

            }, 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: ChatItem,
        action: Action,
        closure: @escaping (ChatItem) -> Void
    ) -> 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) }