Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/HomeView/Stories/MyStoryCell.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalServiceKit
import SignalUI
import UIKit

class MyStoryCell: UITableViewCell {
    static let reuseIdentifier = "MyStoryCell"

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = .dynamicTypeHeadline
        label.textColor = .Signal.label
        return label
    }()

    private let titleChevron: UIImageView = {
        let imageView = UIImageView()
        imageView.tintColor = .Signal.label
        return imageView
    }()

    private let subtitleLabel: UILabel = {
        let label = UILabel()
        label.font = .dynamicTypeSubheadline
        label.textColor = .Signal.secondaryLabel
        return label
    }()

    private let avatarView = ConversationAvatarView(sizeClass: .fiftySix, localUserDisplayMode: .asUser, badged: false, useAutolayout: true)
    private let attachmentThumbnail = UIView()

    private let failedIconView = UIImageView(image: Theme.iconImage(.error16))

    private let addStoryButton = OWSButton()
    private let plusIcon = PlusIconView()

    private let contentHStackView = UIStackView()

    /// If set to `true` background in `selected` state would have rounded corners.
    var useSidebarAppearance = false

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        titleLabel.text = OWSLocalizedString("MY_STORIES_TITLE", comment: "Title for the 'My Stories' view")

        titleChevron.image = UIImage(imageLiteralResourceName: "chevron-right-20")

        let titleStack = UIStackView(arrangedSubviews: [titleLabel, titleChevron])
        titleStack.axis = .horizontal
        titleStack.alignment = .center
        titleStack.spacing = 2

        failedIconView.autoSetDimension(.width, toSize: 16)
        failedIconView.contentMode = .scaleAspectFit
        failedIconView.tintColor = .ows_accentRed

        let subtitleStack = UIStackView(arrangedSubviews: [failedIconView, subtitleLabel])
        subtitleStack.axis = .horizontal
        subtitleStack.alignment = .center
        subtitleStack.spacing = 6

        let vStack = UIStackView(arrangedSubviews: [titleStack, subtitleStack])
        vStack.axis = .vertical
        vStack.alignment = .leading

        addStoryButton.addSubview(avatarView)
        avatarView.autoPinEdgesToSuperviewEdges()

        plusIcon.isUserInteractionEnabled = false

        addStoryButton.addSubview(plusIcon)
        plusIcon.autoPinEdge(toSuperviewEdge: .trailing, withInset: -3)
        plusIcon.autoPinEdge(toSuperviewEdge: .bottom, withInset: -3)

        contentHStackView.addArrangedSubviews([addStoryButton, vStack, .hStretchingSpacer(), attachmentThumbnail])
        contentHStackView.axis = .horizontal
        contentHStackView.alignment = .center
        contentHStackView.spacing = 16

        contentView.addSubview(contentHStackView)
        contentHStackView.autoPinEdgesToSuperviewMargins()

        attachmentThumbnail.autoSetDimensions(to: CGSize(width: 64, height: 84))
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func updateConfiguration(using state: UICellConfigurationState) {
        var configuration = UIBackgroundConfiguration.clear()
        if state.isSelected || state.isHighlighted {
            configuration.backgroundColor = Theme.tableCell2SelectedBackgroundColor
            if useSidebarAppearance {
                configuration.cornerRadius = 24
            }
        } else {
            configuration.backgroundColor = .Signal.background
        }
        backgroundConfiguration = configuration

        attachmentThumbnailDividerView?.backgroundColor = configuration.backgroundColor
        plusIcon.borderColor = configuration.backgroundColor
    }

    private var attachmentThumbnailDividerView: UIView?

    private var latestMessageRevealedSpoilerIds: Set<StyleIdType>?
    private var latestMessageAttachment: StoryThumbnailView.Attachment?
    private var secondLatestMessageRevealedSpoilerIds: Set<StyleIdType>?
    private var secondLatestMessageAttachment: StoryThumbnailView.Attachment?

    func configure(
        with model: MyStoryViewModel,
        spoilerState: SpoilerRenderState,
        addStoryAction: @escaping () -> Void,
    ) {
        configureSubtitle(with: model)

        titleChevron.isHiddenInStackView = model.messages.isEmpty

        addStoryButton.block = addStoryAction

        avatarView.updateWithSneakyTransactionIfNecessary { config in
            config.dataSource = .address(DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction!.aciAddress)
            // We reload the row when this state changes, so don't make the avatar auto update.
            config.storyConfiguration = .fixed(model.messages.isEmpty ? .noStories : .viewed)
            config.usePlaceholderImages()
        }

        let latestMessageRevealedSpoilerIds: Set<StyleIdType> = model.latestMessageIdentifier.map(
            spoilerState.revealState.revealedSpoilerIds(interactionIdentifier:),
        ) ?? Set()
        let secondLatestMessageRevealedSpoilerIds: Set<StyleIdType> = model.secondLatestMessageIdentifier.map(
            spoilerState.revealState.revealedSpoilerIds(interactionIdentifier:),
        ) ?? Set()

        if
            self.latestMessageAttachment != model.latestMessageAttachment ||
            self.secondLatestMessageAttachment != model.secondLatestMessageAttachment ||
            self.latestMessageRevealedSpoilerIds != latestMessageRevealedSpoilerIds ||
            self.secondLatestMessageRevealedSpoilerIds != secondLatestMessageRevealedSpoilerIds
        {
            self.latestMessageAttachment = model.latestMessageAttachment
            self.secondLatestMessageAttachment = model.secondLatestMessageAttachment
            self.latestMessageRevealedSpoilerIds = latestMessageRevealedSpoilerIds
            self.secondLatestMessageRevealedSpoilerIds = secondLatestMessageRevealedSpoilerIds

            attachmentThumbnail.removeAllSubviews()
            attachmentThumbnailDividerView = nil

            if let latestMessageAttachment = model.latestMessageAttachment, let latestMessageIdentifier = model.latestMessageIdentifier {
                attachmentThumbnail.isHiddenInStackView = false

                let latestThumbnailView = StoryThumbnailView(
                    attachment: latestMessageAttachment,
                    interactionIdentifier: latestMessageIdentifier,
                    spoilerState: spoilerState,
                )
                attachmentThumbnail.addSubview(latestThumbnailView)
                latestThumbnailView.autoPinHeightToSuperview()
                latestThumbnailView.autoSetDimensions(to: CGSize(width: 56, height: 84))
                latestThumbnailView.autoPinEdge(toSuperviewEdge: .trailing)

                if
                    let secondLatestMessageAttachment = model.secondLatestMessageAttachment,
                    let secondLatestMessageIdentifier = model.secondLatestMessageIdentifier
                {
                    let secondLatestThumbnailView = StoryThumbnailView(
                        attachment: secondLatestMessageAttachment,
                        interactionIdentifier: secondLatestMessageIdentifier,
                        spoilerState: spoilerState,
                    )
                    secondLatestThumbnailView.layer.cornerRadius = 6
                    secondLatestThumbnailView.transform = .init(rotationAngle: (CurrentAppContext().isRTL ? 1 : -1) * 0.18168878)
                    attachmentThumbnail.insertSubview(secondLatestThumbnailView, belowSubview: latestThumbnailView)
                    secondLatestThumbnailView.autoPinEdge(toSuperviewEdge: .top, withInset: 4)
                    secondLatestThumbnailView.autoSetDimensions(to: CGSize(width: 43, height: 64))
                    secondLatestThumbnailView.autoPinEdge(toSuperviewEdge: .leading)

                    let dividerView = UIView()
                    dividerView.backgroundColor = .Signal.background
                    dividerView.layer.cornerRadius = 12
                    attachmentThumbnail.insertSubview(dividerView, belowSubview: latestThumbnailView)
                    dividerView.autoSetDimensions(to: CGSize(width: 60, height: 88))
                    dividerView.autoPinEdge(toSuperviewEdge: .trailing, withInset: -2)
                    dividerView.autoPinEdge(toSuperviewEdge: .top, withInset: -2)
                    attachmentThumbnailDividerView = dividerView
                }
            } else {
                attachmentThumbnail.isHiddenInStackView = true
            }
        }
    }

    func configureSubtitle(with model: MyStoryViewModel) {
        if model.sendingCount > 0 {
            let format = OWSLocalizedString("STORY_SENDING_%d", tableName: "PluralAware", comment: "Indicates that N stories are currently sending")
            subtitleLabel.text = String.localizedStringWithFormat(format, model.sendingCount)
            failedIconView.isHiddenInStackView = model.failureState == .none
        } else if model.failureState != .none {
            switch model.failureState {
            case .complete:
                subtitleLabel.text = OWSLocalizedString("STORY_SEND_FAILED", comment: "Text indicating that the story send has failed")
            case .partial:
                subtitleLabel.text = OWSLocalizedString("STORY_SEND_PARTIALLY_FAILED", comment: "Text indicating that the story send has partially failed")
            case .none:
                owsFailDebug("Unexpected")
            }
            failedIconView.isHiddenInStackView = false
        } else if let latestMessageTimestamp = model.latestMessageTimestamp {
            subtitleLabel.text = DateUtil.formatTimestampRelatively(latestMessageTimestamp)
            failedIconView.isHiddenInStackView = true
        } else {
            subtitleLabel.text = OWSLocalizedString("MY_STORY_TAP_TO_ADD", comment: "Prompt to add to your story")
            failedIconView.isHiddenInStackView = true
        }
    }

    private class PlusIconView: UIView {

        var borderColor: UIColor? {
            get {
                return outerCircle.backgroundColor
            }
            set {
                outerCircle.backgroundColor = newValue
            }
        }

        let outerCircle = UIView()
        let iconView = UIImageView()

        init() {
            super.init(frame: .zero)

            addSubview(outerCircle)
            addSubview(iconView)

            iconView.image = UIImage(imageLiteralResourceName: "plus-20")
            iconView.tintColor = .white
            iconView.contentMode = .center
            iconView.autoSetDimensions(to: .square(20))
            iconView.layer.cornerRadius = 10
            iconView.autoCenterInSuperview()
            iconView.backgroundColor = .ows_accentBlue

            // NOTE: gets written over by the cell's theme application.
            outerCircle.backgroundColor = .Signal.background
            outerCircle.autoSetDimensions(to: .square(26))
            outerCircle.layer.cornerRadius = 13
            outerCircle.autoPinEdgesToSuperviewEdges()
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
}