session-ios/Session/Signal/ColorPickerViewController.s...

533 lines
18 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
class OWSColorPickerAccessoryView: NeverClearView {
override var intrinsicContentSize: CGSize {
return CGSize(width: kSwatchSize, height: kSwatchSize)
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
return self.intrinsicContentSize
}
let kSwatchSize: CGFloat = 24
@objc
required init(color: UIColor) {
super.init(frame: .zero)
let circleView = CircleView()
circleView.backgroundColor = color
addSubview(circleView)
circleView.autoSetDimensions(to: CGSize(width: kSwatchSize, height: kSwatchSize))
circleView.autoPinEdgesToSuperviewEdges()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@objc (OWSCircleView)
class CircleView: UIView {
override var bounds: CGRect {
didSet {
self.layer.cornerRadius = self.bounds.size.height / 2
}
}
}
protocol ColorViewDelegate: class {
func colorViewWasTapped(_ colorView: ColorView)
}
class ColorView: UIView {
public weak var delegate: ColorViewDelegate?
public let conversationColor: OWSConversationColor
private let swatchView: CircleView
private let selectedRing: CircleView
public var isSelected: Bool = false {
didSet {
self.selectedRing.isHidden = !isSelected
}
}
required init(conversationColor: OWSConversationColor) {
self.conversationColor = conversationColor
self.swatchView = CircleView()
self.selectedRing = CircleView()
super.init(frame: .zero)
self.addSubview(selectedRing)
self.addSubview(swatchView)
// Selected Ring
let cellHeight: CGFloat = ScaleFromIPhone5(60)
selectedRing.autoSetDimensions(to: CGSize(width: cellHeight, height: cellHeight))
selectedRing.layer.borderColor = Theme.secondaryColor.cgColor
selectedRing.layer.borderWidth = 2
selectedRing.autoPinEdgesToSuperviewEdges()
selectedRing.isHidden = true
// Color Swatch
swatchView.backgroundColor = conversationColor.primaryColor
let swatchSize: CGFloat = ScaleFromIPhone5(46)
swatchView.autoSetDimensions(to: CGSize(width: swatchSize, height: swatchSize))
swatchView.autoCenterInSuperview()
// gestures
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
self.addGestureRecognizer(tapGesture)
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: Actions
@objc
func didTap() {
delegate?.colorViewWasTapped(self)
}
}
@objc
protocol ColorPickerDelegate: class {
func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor)
}
@objc(OWSColorPicker)
class ColorPicker: NSObject, ColorPickerViewDelegate {
@objc
public weak var delegate: ColorPickerDelegate?
@objc
let sheetViewController: SheetViewController
@objc
init(thread: TSThread) {
let colorName = thread.conversationColorName
let currentConversationColor = OWSConversationColor.conversationColorOrDefault(colorName: colorName)
sheetViewController = SheetViewController()
super.init()
let colorPickerView = ColorPickerView(thread: thread)
colorPickerView.delegate = self
colorPickerView.select(conversationColor: currentConversationColor)
sheetViewController.contentView.addSubview(colorPickerView)
colorPickerView.autoPinEdgesToSuperviewEdges()
}
// MARK: ColorPickerViewDelegate
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor) {
self.delegate?.colorPicker(self, didPickConversationColor: conversationColor)
}
}
protocol ColorPickerViewDelegate: class {
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor)
}
class ColorPickerView: UIView, ColorViewDelegate {
private let colorViews: [ColorView]
let conversationStyle: ConversationStyle
var outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
var incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
weak var delegate: ColorPickerViewDelegate?
// This is mostly a developer convenience - OWSMessageCell asserts at some point
// that the available method width is greater than 0.
// We ultimately use the width of the picker view which will be larger.
let kMinimumConversationWidth: CGFloat = 300
override var bounds: CGRect {
didSet {
updateMockConversationView()
}
}
let mockConversationView: UIView = UIView()
init(thread: TSThread) {
let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) }
self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) }
self.conversationStyle = ConversationStyle(thread: thread)
super.init(frame: .zero)
colorViews.forEach { $0.delegate = self }
let headerView = self.buildHeaderView()
mockConversationView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
mockConversationView.backgroundColor = Theme.backgroundColor
self.updateMockConversationView()
let paletteView = self.buildPaletteView(colorViews: colorViews)
let rowsStackView = UIStackView(arrangedSubviews: [headerView, mockConversationView, paletteView])
rowsStackView.axis = .vertical
addSubview(rowsStackView)
rowsStackView.autoPinEdgesToSuperviewEdges()
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: ColorViewDelegate
func colorViewWasTapped(_ colorView: ColorView) {
self.select(conversationColor: colorView.conversationColor)
self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor)
updateMockConversationView()
}
fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) {
colorViews.forEach { colorView in
colorView.isSelected = colorView.conversationColor == selectedConversationColor
}
}
// MARK: View Building
private func buildHeaderView() -> UIView {
let headerView = UIView()
headerView.layoutMargins = UIEdgeInsets(top: 15, left: 16, bottom: 15, right: 16)
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("COLOR_PICKER_SHEET_TITLE", comment: "Modal Sheet title when picking a conversation color.")
titleLabel.textAlignment = .center
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
titleLabel.textColor = Theme.primaryColor
headerView.addSubview(titleLabel)
titleLabel.ows_autoPinToSuperviewMargins()
let bottomBorderView = UIView()
bottomBorderView.backgroundColor = Theme.hairlineColor
headerView.addSubview(bottomBorderView)
bottomBorderView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
bottomBorderView.autoSetDimension(.height, toSize: CGHairlineWidth())
return headerView
}
private func updateMockConversationView() {
/*
conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth)
mockConversationView.subviews.forEach { $0.removeFromSuperview() }
// outgoing
outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
let outgoingItem = MockConversationViewItem()
let outgoingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_1", comment: "The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble.")
outgoingItem.interaction = MockOutgoingMessage(messageBody: outgoingText)
outgoingItem.displayableBodyText = DisplayableText.displayableText(outgoingText)
outgoingItem.interactionType = .outgoingMessage
outgoingMessageView.viewItem = outgoingItem
outgoingMessageView.cellMediaCache = NSCache()
outgoingMessageView.conversationStyle = conversationStyle
outgoingMessageView.configureViews()
outgoingMessageView.loadContent()
let outgoingCell = UIView()
outgoingCell.addSubview(outgoingMessageView)
outgoingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
let outgoingSize = outgoingMessageView.measureSize()
outgoingMessageView.autoSetDimensions(to: outgoingSize)
// incoming
incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
let incomingItem = MockConversationViewItem()
let incomingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_2", comment: "The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble.")
incomingItem.interaction = MockIncomingMessage(messageBody: incomingText)
incomingItem.displayableBodyText = DisplayableText.displayableText(incomingText)
incomingItem.interactionType = .incomingMessage
incomingMessageView.viewItem = incomingItem
incomingMessageView.cellMediaCache = NSCache()
incomingMessageView.conversationStyle = conversationStyle
incomingMessageView.configureViews()
incomingMessageView.loadContent()
let incomingCell = UIView()
incomingCell.addSubview(incomingMessageView)
incomingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
let incomingSize = incomingMessageView.measureSize()
incomingMessageView.autoSetDimensions(to: incomingSize)
let messagesStackView = UIStackView(arrangedSubviews: [outgoingCell, incomingCell])
messagesStackView.axis = .vertical
messagesStackView.spacing = 12
mockConversationView.addSubview(messagesStackView)
messagesStackView.autoPinEdgesToSuperviewMargins()
*/
}
private func buildPaletteView(colorViews: [ColorView]) -> UIView {
let paletteView = UIView()
let paletteMargin = ScaleFromIPhone5(12)
paletteView.layoutMargins = UIEdgeInsets(top: paletteMargin, left: paletteMargin, bottom: 0, right: paletteMargin)
let kRowLength = 4
let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in
let row = UIStackView(arrangedSubviews: colorViewsInRow)
row.distribution = UIStackView.Distribution.equalSpacing
return row
}
let rowsStackView = UIStackView(arrangedSubviews: rows)
rowsStackView.axis = .vertical
rowsStackView.spacing = ScaleFromIPhone5To7Plus(12, 30)
paletteView.addSubview(rowsStackView)
rowsStackView.ows_autoPinToSuperviewMargins()
// no-op gesture to keep taps from dismissing SheetView
paletteView.addGestureRecognizer(UITapGestureRecognizer(target: nil, action: nil))
return paletteView
}
}
// MARK: Mock Classes for rendering demo conversation
/*
@objc
private class MockConversationViewItem: NSObject, ConversationViewItem {
var userCanDeleteGroupMessage: Bool = false
var isRSSFeed: Bool = false
var interaction: TSInteraction = TSMessage()
var interactionType: OWSInteractionType = OWSInteractionType.unknown
var quotedReply: OWSQuotedReplyModel?
var isGroupThread: Bool = false
var hasBodyText: Bool = true
var isQuotedReply: Bool = false
var hasQuotedAttachment: Bool = false
var hasQuotedText: Bool = false
var hasCellHeader: Bool = false
var isExpiringMessage: Bool = false
var shouldShowDate: Bool = false
var shouldShowSenderAvatar: Bool = false
var senderName: NSAttributedString?
var shouldHideFooter: Bool = false
var isFirstInCluster: Bool = true
var isLastInCluster: Bool = true
var unreadIndicator: OWSUnreadIndicator?
var lastAudioMessageView: OWSAudioMessageView?
var audioDurationSeconds: CGFloat = 0
var audioProgressSeconds: CGFloat = 0
var messageCellType: OWSMessageCellType = .textOnlyMessage
var displayableBodyText: DisplayableText?
var attachmentStream: TSAttachmentStream?
var attachmentPointer: TSAttachmentPointer?
var mediaSize: CGSize = .zero
var displayableQuotedText: DisplayableText?
var quotedAttachmentMimetype: String?
var quotedRecipientId: String?
var didCellMediaFailToLoad: Bool = false
var contactShare: ContactShareViewModel?
var systemMessageText: String?
var authorConversationColorName: String?
var hasBodyTextActionContent: Bool = false
var hasMediaActionContent: Bool = false
var mediaAlbumItems: [ConversationMediaAlbumItem]?
var hasCachedLayoutState: Bool = false
var linkPreview: OWSLinkPreview?
var linkPreviewAttachment: TSAttachment?
override init() {
super.init()
}
func itemId() -> String {
return interaction.uniqueId!
}
func dequeueCell(for collectionView: UICollectionView, indexPath: IndexPath) -> ConversationViewCell {
owsFailDebug("unexpected invocation")
return ConversationViewCell(forAutoLayout: ())
}
func replace(_ interaction: TSInteraction, transaction: YapDatabaseReadTransaction) {
owsFailDebug("unexpected invocation")
return
}
func clearCachedLayoutState() {
owsFailDebug("unexpected invocation")
return
}
func copyMediaAction() {
owsFailDebug("unexpected invocation")
return
}
func copyTextAction() {
owsFailDebug("unexpected invocation")
return
}
func shareMediaAction() {
owsFailDebug("unexpected invocation")
return
}
func shareTextAction() {
owsFailDebug("unexpected invocation")
return
}
func saveMediaAction() {
owsFailDebug("unexpected invocation")
return
}
func deleteAction() {
owsFailDebug("unexpected invocation")
return
}
func canCopyMedia() -> Bool {
owsFailDebug("unexpected invocation")
return false
}
func canSaveMedia() -> Bool {
owsFailDebug("unexpected invocation")
return false
}
func audioPlaybackState() -> AudioPlaybackState {
owsFailDebug("unexpected invocation")
return AudioPlaybackState.paused
}
func setAudioPlaybackState(_ state: AudioPlaybackState) {
owsFailDebug("unexpected invocation")
return
}
func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
owsFailDebug("unexpected invocation")
return
}
func cellSize() -> CGSize {
owsFailDebug("unexpected invocation")
return CGSize.zero
}
func vSpacing(withPreviousLayoutItem previousLayoutItem: ConversationViewLayoutItem) -> CGFloat {
owsFailDebug("unexpected invocation")
return 2
}
func firstValidAlbumAttachment() -> TSAttachmentStream? {
owsFailDebug("unexpected invocation")
return nil
}
func mediaAlbumHasFailedAttachment() -> Bool {
owsFailDebug("unexpected invocation")
return false
}
}
*/
private class MockIncomingMessage: TSIncomingMessage {
init(messageBody: String) {
super.init(incomingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
in: TSThread(),
authorId: "+fake-id",
sourceDeviceId: 1,
messageBody: messageBody,
attachmentIds: [],
expiresInSeconds: 0,
quotedMessage: nil,
contactShare: nil,
linkPreview: nil,
serverTimestamp: nil,
wasReceivedByUD: false)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(dictionary dictionaryValue: [String: Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
override func save(with transaction: YapDatabaseReadWriteTransaction) {
// no - op
owsFailDebug("shouldn't save mock message")
}
}
private class MockOutgoingMessage: TSOutgoingMessage {
init(messageBody: String) {
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
in: nil,
messageBody: messageBody,
attachmentIds: [],
expiresInSeconds: 0,
expireStartedAt: 0,
isVoiceMessage: false,
groupMetaMessage: .unspecified,
quotedMessage: nil,
contactShare: nil,
linkPreview: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init(dictionary dictionaryValue: [String: Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
override func save(with transaction: YapDatabaseReadWriteTransaction) {
// no - op
owsFailDebug("shouldn't save mock message")
}
class MockOutgoingMessageRecipientState: TSOutgoingMessageRecipientState {
override var state: OWSOutgoingMessageRecipientState {
return OWSOutgoingMessageRecipientState.sent
}
override var deliveryTimestamp: NSNumber? {
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
}
override var readTimestamp: NSNumber? {
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
}
}
override func readRecipientIds() -> [String] {
// makes message appear as read
return ["fake-non-empty-id"]
}
override func recipientState(forRecipientId recipientId: String) -> TSOutgoingMessageRecipientState? {
return MockOutgoingMessageRecipientState()
}
}