Skip to content
Snippets Groups Projects
CellConfigurator.swift 15 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 {
        .init(
            canBuild: { item in
                (item.status == .received || item.status == .read || item.status == .receivingAttachment)
                && 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: 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 = item.status.canReply
                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 != .receivingAttachment else { return }

                    voxophone.toggleLoudspeaker()
                }

                cell.leftView.didTapLeft = {
                    guard item.status != .receivingAttachment else { return }

                    if case .playing(url, _, _, _) = voxophone.state {
                        voxophone.reset()
                    } else {
                        voxophone.load(url)
                        voxophone.play()
                    }
                }

                return cell
            }
        )
    }

    static func outgoingAudio(
        voxophone: Voxophone
    ) -> Self {
        .init(
            canBuild: { item in
                (item.status == .sent ||
                 item.status == .failedToSend ||
                 item.status == .sendingAttachment ||
                 item.status == .timedOut)
Bruno Muniz's avatar
Bruno Muniz committed
                && 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 = item.status.canReply
                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 {
        .init(
            canBuild: { item in
                (item.status == .sent ||
                 item.status == .failedToSend ||
                 item.status == .sendingAttachment ||
                 item.status == .timedOut)
Bruno Muniz's avatar
Bruno Muniz committed
                && 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 = item.status.canReply
                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 incomingImage() -> Self {
        .init(
            canBuild: { item in
                (item.status == .received || item.status == .read || item.status == .receivingAttachment)
                && 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 = item.status.canReply
                cell.performReply = {}
                cell.leftView.imageView.image = UIImage(data: attachment.data!)
                return cell
            }
        )
    }
}

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
                && item.payload.attachment == 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.canReply
                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 || item.status == .read)
                && item.payload.reply != nil
                && item.payload.attachment == 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.canReply
                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 == .failedToSend || item.status == .timedOut)
Bruno Muniz's avatar
Bruno Muniz committed
                && item.payload.reply != nil
                && item.payload.attachment == 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 = item.status.canReply
                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.status == .read)
                && item.payload.reply == nil
                && item.payload.attachment == nil

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

                Bubbler.build(bubble: cell.leftView, with: item)
                cell.canReply = item.status.canReply
                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
                && item.payload.attachment == nil

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

                Bubbler.build(bubble: cell.rightView, with: item)
                cell.canReply = item.status.canReply
                cell.performReply = performReply
                cell.rightView.didTapShowRound = { showRound(item.roundURL) }

                return cell
            }
        )
    }

    static func outgoingFailedText(performReply: @escaping () -> Void) -> Self {
        .init(
            canBuild: { item in
                (item.status == .failedToSend || item.status == .timedOut)
Bruno Muniz's avatar
Bruno Muniz committed
                && item.payload.reply == nil
                && item.payload.attachment == nil

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

                Bubbler.build(bubble: cell.rightView, with: item)
                cell.canReply = item.status.canReply
                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? {

        guard item.payload.attachment == nil else { return nil }

        switch action {
        case .reply:
            guard item.status == .read || item.status == .received || item.status == .sent else { return nil }
        case .retry:
            guard item.status == .failedToSend || item.status == .timedOut 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) }