//
// C o p y r i g h t ( c ) 2 0 1 8 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
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 )
// S e l e c t e d R i n g
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
// C o l o r S w a t c h
swatchView . backgroundColor = conversationColor . primaryColor
let swatchSize : CGFloat = ScaleFromIPhone5 ( 46 )
swatchView . autoSetDimensions ( to : CGSize ( width : swatchSize , height : swatchSize ) )
swatchView . autoCenterInSuperview ( )
// g e s t u r e s
let tapGesture = UITapGestureRecognizer ( target : self , action : #selector ( didTap ) )
self . addGestureRecognizer ( tapGesture )
}
required init ? ( coder aDecoder : NSCoder ) {
notImplemented ( )
}
// MARK: A c t i o n s
@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: C o l o r P i c k e r V i e w D e l e g a t e
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 ?
// T h i s i s m o s t l y a d e v e l o p e r c o n v e n i e n c e - O W S M e s s a g e C e l l a s s e r t s a t s o m e p o i n t
// t h a t t h e a v a i l a b l e m e t h o d w i d t h i s g r e a t e r t h a n 0 .
// W e u l t i m a t e l y u s e t h e w i d t h o f t h e p i c k e r v i e w w h i c h w i l l b e l a r g e r .
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: C o l o r V i e w D e l e g a t e
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: V i e w B u i l d i n g
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 ( ) }
// o u t g o i n g
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 )
// i n c o m i n g
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 = UIStackViewDistribution . equalSpacing
return row
}
let rowsStackView = UIStackView ( arrangedSubviews : rows )
rowsStackView . axis = . vertical
rowsStackView . spacing = ScaleFromIPhone5To7Plus ( 12 , 30 )
paletteView . addSubview ( rowsStackView )
rowsStackView . ows_autoPinToSuperviewMargins ( )
// n o - o p g e s t u r e t o k e e p t a p s f r o m d i s m i s s i n g S h e e t V i e w
paletteView . addGestureRecognizer ( UITapGestureRecognizer ( target : nil , action : nil ) )
return paletteView
}
}
// MARK: M o c k C l a s s e s f o r r e n d e r i n g d e m o c o n v e r s a t i o n
@objc
private class MockConversationViewItem : NSObject , ConversationViewItem {
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 = . textMessage
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 mediaGalleryItems : [ ConversationMediaGalleryItem ] ?
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 firstValidGalleryAttachment ( ) -> TSAttachmentStream ? {
owsFailDebug ( " unexpected invocation " )
return nil
}
}
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 ,
serverTimestamp : nil ,
wasReceivedByUD : false )
}
required init ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
required init ( dictionary dictionaryValue : [ AnyHashable : Any ] ! ) throws {
fatalError ( " init(dictionary:) has not been implemented " )
}
override func save ( with transaction : YapDatabaseReadWriteTransaction ) {
// n o - o p
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 )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
required init ( dictionary dictionaryValue : [ AnyHashable : Any ] ! ) throws {
fatalError ( " init(dictionary:) has not been implemented " )
}
override func save ( with transaction : YapDatabaseReadWriteTransaction ) {
// n o - o p
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 ] {
// m a k e s m e s s a g e a p p e a r a s r e a d
return [ " fake-non-empty-id " ]
}
override func recipientState ( forRecipientId recipientId : String ) -> TSOutgoingMessageRecipientState ? {
return MockOutgoingMessageRecipientState ( )
}
}