From 56387f3574d7c71758656bdf863b97379292b6b5 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 27 Sep 2018 14:25:20 -0600 Subject: [PATCH] demo conversation colors when selecting --- Signal/src/Signal-Bridging-Header.h | 2 + .../ColorPickerViewController.swift | 292 +++++++++++++++++- .../OWSConversationSettingsViewController.m | 4 +- .../translations/en.lproj/Localizable.strings | 6 + 4 files changed, 291 insertions(+), 13 deletions(-) diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index f735d9fe2..c37dc03d9 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -9,6 +9,7 @@ #import "AppSettingsViewController.h" #import "ContactCellView.h" #import "ContactTableViewCell.h" +#import "ConversationViewCell.h" #import "ConversationViewItem.h" #import "DateUtil.h" #import "DebugUIPage.h" @@ -21,6 +22,7 @@ #import "NotificationsManager.h" #import "OWSAddToContactViewController.h" #import "OWSAnyTouchGestureRecognizer.h" +#import "OWSAudioMessageView.h" #import "OWSAudioPlayer.h" #import "OWSBackup.h" #import "OWSBackupIO.h" diff --git a/Signal/src/ViewControllers/ColorPickerViewController.swift b/Signal/src/ViewControllers/ColorPickerViewController.swift index b3cb1a631..b6978b956 100644 --- a/Signal/src/ViewControllers/ColorPickerViewController.swift +++ b/Signal/src/ViewControllers/ColorPickerViewController.swift @@ -75,18 +75,18 @@ class ColorPicker: NSObject, ColorPickerViewDelegate { @objc let sheetViewController: SheetViewController - private let currentConversationColor: OWSConversationColor - @objc - init(currentConversationColor: OWSConversationColor) { - self.currentConversationColor = currentConversationColor + init(thread: TSThread) { + let colorName = thread.conversationColorName + let currentConversationColor = OWSConversationColor.conversationColorOrDefault(colorName: colorName) sheetViewController = SheetViewController() super.init() - let colorPickerView = ColorPickerView() + let colorPickerView = ColorPickerView(thread: thread) colorPickerView.delegate = self colorPickerView.select(conversationColor: currentConversationColor) + sheetViewController.contentView.addSubview(colorPickerView) colorPickerView.autoPinEdgesToSuperviewEdges() } @@ -105,21 +105,42 @@ protocol ColorPickerViewDelegate: class { class ColorPickerView: UIView, ColorViewDelegate { private let colorViews: [ColorView] + let conversationStyle: ConversationStyle + var outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ()) + var incomingMessageView = OWSMessageBubbleView(forAutoLayout: ()) weak var delegate: ColorPickerViewDelegate? - override init(frame: CGRect) { + // 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) } - super.init(frame: frame) + 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, paletteView]) + let rowsStackView = UIStackView(arrangedSubviews: [headerView, mockConversationView, paletteView]) rowsStackView.axis = .vertical addSubview(rowsStackView) rowsStackView.autoPinEdgesToSuperviewEdges() @@ -134,6 +155,7 @@ class ColorPickerView: UIView, ColorViewDelegate { func colorViewWasTapped(_ colorView: ColorView) { self.select(conversationColor: colorView.conversationColor) self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor) + updateMockConversationView() } fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) { @@ -166,9 +188,59 @@ class ColorPickerView: UIView, ColorViewDelegate { 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() - paletteView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + paletteView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16) let kRowLength = 4 let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in @@ -178,10 +250,210 @@ class ColorPickerView: UIView, ColorViewDelegate { } let rowsStackView = UIStackView(arrangedSubviews: rows) rowsStackView.axis = .vertical - rowsStackView.spacing = ScaleFromIPhone5To7Plus(16, 50) + rowsStackView.spacing = ScaleFromIPhone5To7Plus(12, 50) 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 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 + + override init() { + super.init() + } + + 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 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 + } +} + +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) + } + + 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) { + // 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) + } + + 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) { + // 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() + } +} diff --git a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m index 58f8663dc..e55896674 100644 --- a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m +++ b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m @@ -250,9 +250,7 @@ const CGFloat kIconViewLength = 24; [[OWSDisappearingMessagesConfiguration alloc] initDefaultWithThreadId:self.thread.uniqueId]; } - NSString *colorName = self.thread.conversationColorName; - OWSConversationColor *currentConversationColor = [OWSConversationColor conversationColorOrDefaultForColorName:colorName]; - self.colorPicker = [[OWSColorPicker alloc] initWithCurrentConversationColor:currentConversationColor]; + self.colorPicker = [[OWSColorPicker alloc] initWithThread:self.thread]; self.colorPicker.delegate = self; [self updateTableContents]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 0abea05b8..63bb79e2c 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -386,6 +386,12 @@ /* Error indicating that the app was prevented from accessing the user's CloudKit account. */ "CLOUDKIT_STATUS_RESTRICTED" = "Signal was not allowed to access your iCloud account for backups."; +/* The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble. */ +"COLOR_PICKER_DEMO_MESSAGE_1" = "Choose the color of outgoing messages in this conversation."; + +/* The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble. */ +"COLOR_PICKER_DEMO_MESSAGE_2" = "Only you will see the color you choose."; + /* Modal Sheet title when picking a conversation color. */ "COLOR_PICKER_SHEET_TITLE" = "Conversation Color";