From 95a6df64960803f71d39a23a0fd4a60037441e43 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 26 Sep 2018 16:41:36 -0600 Subject: [PATCH 1/3] Generic SheetViewController --- Signal.xcodeproj/project.pbxproj | 4 + .../ViewControllers/SheetViewController.swift | 213 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 SignalMessaging/ViewControllers/SheetViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8415a442c..ad158c345 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -429,6 +429,7 @@ 4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */; }; 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; }; 4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; }; + 4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */; }; 4C2F454F214C00E1004871FF /* AvatarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */; }; 4C3EF7FD2107DDEE0007EBF7 /* ParamParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */; }; 4C3EF802210918740007EBF7 /* SSKProtoEnvelopeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */; }; @@ -1115,6 +1116,7 @@ 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = ""; }; 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = ""; }; + 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetViewController.swift; sourceTree = ""; }; 4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = ""; }; 4C3EF7FC2107DDEE0007EBF7 /* ParamParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamParserTest.swift; sourceTree = ""; }; 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.swift; sourceTree = ""; }; @@ -1972,6 +1974,7 @@ 34AC09DC211B39B100997B47 /* SharingThreadPickerViewController.m */, 34AC09BF211B39AE00997B47 /* ViewControllerUtils.h */, 34AC09D1211B39B000997B47 /* ViewControllerUtils.m */, + 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -3219,6 +3222,7 @@ 346129C91FD2072E00532771 /* NSString+OWS.m in Sources */, 347850691FD9B78A007B8332 /* AppSetup.m in Sources */, 346941A3215D2EE400B5BFAD /* Theme.m in Sources */, + 4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */, 34AC0A14211B39EA00997B47 /* ContactCellView.m in Sources */, 34AC0A15211B39EA00997B47 /* ContactsViewHelper.m in Sources */, 346129FF1FD5F31400532771 /* OWS103EnableVideoCalling.m in Sources */, diff --git a/SignalMessaging/ViewControllers/SheetViewController.swift b/SignalMessaging/ViewControllers/SheetViewController.swift new file mode 100644 index 000000000..78d16bba8 --- /dev/null +++ b/SignalMessaging/ViewControllers/SheetViewController.swift @@ -0,0 +1,213 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc(OWSSheetViewControllerDelegate) +public protocol SheetViewControllerDelegate: class { + func sheetViewControllerRequestedDismiss(_ sheetViewController: SheetViewController) +} + +@objc(OWSSheetViewController) +public class SheetViewController: UIViewController { + + @objc + weak var delegate: SheetViewControllerDelegate? + + @objc + public let contentView: UIView = UIView() + + private let sheetView: SheetView = SheetView() + + deinit { + Logger.verbose("") + } + + @objc + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + self.transitioningDelegate = self + self.modalPresentationStyle = .overCurrentContext + } + + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + // MARK: View LifeCycle + + var sheetViewVerticalConstraint: NSLayoutConstraint? + + override public func loadView() { + self.view = UIView() + + sheetView.preservesSuperviewLayoutMargins = true + + sheetView.addSubview(contentView) + contentView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .bottom) + contentView.autoPinEdge(toSuperviewMargin: .bottom) + + view.addSubview(sheetView) + sheetView.autoPinWidthToSuperview() + sheetView.setContentHuggingVerticalHigh() + sheetView.setCompressionResistanceHigh() + self.sheetViewVerticalConstraint = sheetView.autoPinEdge(.top, to: .bottom, of: self.view) + + handleView.backgroundColor = Theme.backgroundColor + let kHandleViewHeight: CGFloat = 5 + handleView.autoSetDimensions(to: CGSize(width: 40, height: kHandleViewHeight)) + handleView.layer.cornerRadius = kHandleViewHeight / 2 + view.addSubview(handleView) + handleView.autoAlignAxis(.vertical, toSameAxisOf: sheetView) + handleView.autoPinEdge(.bottom, to: .top, of: sheetView, withOffset: -6) + + // Gestures + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBackground)) + self.view.addGestureRecognizer(tapGesture) + } + + // MARK: Present / Dismiss animations + + fileprivate func animatePresentation(completion: @escaping (Bool) -> Void) { + guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else { + owsFailDebug("sheetViewVerticalConstraint was unexpectedly nil") + return + } + + let backgroundDuration: TimeInterval = 0.1 + UIView.animate(withDuration: backgroundDuration) { + let alpha: CGFloat = Theme.isDarkThemeEnabled ? 0.7 : 0.6 + self.view.backgroundColor = UIColor.black.withAlphaComponent(alpha) + } + + self.sheetView.superview?.layoutIfNeeded() + + NSLayoutConstraint.deactivate([sheetViewVerticalConstraint]) + self.sheetViewVerticalConstraint = self.sheetView.autoPinEdge(toSuperviewEdge: .bottom) + UIView.animate(withDuration: 0.2, + delay: backgroundDuration, + options: .curveEaseOut, + animations: { + self.sheetView.superview?.layoutIfNeeded() + }, + completion: completion) + } + + fileprivate func animateDismiss(completion: @escaping (Bool) -> Void) { + guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else { + owsFailDebug("sheetVerticalConstraint was unexpectedly nil") + // self.delegate?.sheetViewDidHide(self) + return + } + + self.sheetView.superview?.layoutIfNeeded() + NSLayoutConstraint.deactivate([sheetViewVerticalConstraint]) + + let dismissDuration: TimeInterval = 0.2 + self.sheetViewVerticalConstraint = self.sheetView.autoPinEdge(.top, to: .bottom, of: self.view) + UIView.animate(withDuration: dismissDuration, + delay: 0, + options: .curveEaseOut, + animations: { + self.view.backgroundColor = UIColor.clear + self.sheetView.superview?.layoutIfNeeded() + }, + completion: completion) + } + + // MARK: Actions + + @objc + func didTapBackground() { + // inform delegate to + delegate?.sheetViewControllerRequestedDismiss(self) + } +} + +extension SheetViewController: UIViewControllerTransitioningDelegate { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SheetViewPresentationController(sheetViewController: self) + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SheetViewDismissalController(sheetViewController: self) + } +} + +private class SheetViewPresentationController: NSObject, UIViewControllerAnimatedTransitioning { + + let sheetViewController: SheetViewController + init(sheetViewController: SheetViewController) { + self.sheetViewController = sheetViewController + } + + // This is used for percent driven interactive transitions, as well as for + // container controllers that have companion animations that might need to + // synchronize with the main animation. + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.3 + } + + // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + Logger.debug("") + transitionContext.containerView.addSubview(sheetViewController.view) + sheetViewController.view.autoPinEdgesToSuperviewEdges() + sheetViewController.animatePresentation { didComplete in + Logger.debug("completed: \(didComplete)") + transitionContext.completeTransition(didComplete) + } + } +} + +private class SheetViewDismissalController: NSObject, UIViewControllerAnimatedTransitioning { + + let sheetViewController: SheetViewController + init(sheetViewController: SheetViewController) { + self.sheetViewController = sheetViewController + } + + // This is used for percent driven interactive transitions, as well as for + // container controllers that have companion animations that might need to + // synchronize with the main animation. + public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.3 + } + + // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. + public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + Logger.debug("") + sheetViewController.animateDismiss { didComplete in + Logger.debug("completed: \(didComplete)") + transitionContext.completeTransition(didComplete) + } + } +} + +private class SheetView: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + self.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_gray90 + : UIColor.ows_gray05 + } + + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + override var bounds: CGRect { + didSet { + updateMask() + } + } + + private func updateMask() { + let cornerRadius: CGFloat = 16 + let path: UIBezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + self.layer.mask = mask + } +} From 4765ed9a067038df3d029a797321176f5808f334 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 26 Sep 2018 19:10:28 -0600 Subject: [PATCH 2/3] Color picker TODO -[x] tap to select/deselect -[x] initially selected -[x] integrate into conversation settings -[x] colorPickerDelegate -[x] translate strings -[] reorder colors -[x] SheetView: add top handle Nice to have: -[] SheetView: interactively swipe/unswipe to dismiss? -[] preview color in bubbles --- .../ColorPickerViewController.swift | 228 +++++++++++------- .../OWSConversationSettingsViewController.m | 34 ++- .../translations/en.lproj/Localizable.strings | 3 + .../ViewControllers/SheetViewController.swift | 2 +- .../appearance/OWSConversationColor.h | 9 +- .../appearance/OWSConversationColor.m | 24 +- .../categories/Collection+OWS.swift | 8 + SignalMessaging/categories/UIColor+OWS.m | 3 +- 8 files changed, 198 insertions(+), 113 deletions(-) diff --git a/Signal/src/ViewControllers/ColorPickerViewController.swift b/Signal/src/ViewControllers/ColorPickerViewController.swift index 79340f3f4..b3cb1a631 100644 --- a/Signal/src/ViewControllers/ColorPickerViewController.swift +++ b/Signal/src/ViewControllers/ColorPickerViewController.swift @@ -4,138 +4,184 @@ import Foundation -let colorSwatchHeight: CGFloat = 40 +protocol ColorViewDelegate: class { + func colorViewWasTapped(_ colorView: ColorView) +} class ColorView: UIView { - let color: UIColor - let swatchView: UIView + public weak var delegate: ColorViewDelegate? + public let conversationColor: OWSConversationColor - required init(color: UIColor) { - self.color = color + private let swatchView: UIView + private let selectedRing: UIView + public var isSelected: Bool = false { + didSet { + self.selectedRing.isHidden = !isSelected + } + } + + required init(conversationColor: OWSConversationColor) { + self.conversationColor = conversationColor self.swatchView = UIView() + self.selectedRing = UIView() super.init(frame: .zero) - - swatchView.backgroundColor = color - - self.swatchView.layer.cornerRadius = colorSwatchHeight / 2 - + self.addSubview(selectedRing) self.addSubview(swatchView) - swatchView.autoVCenterInSuperview() - swatchView.autoSetDimension(.height, toSize: colorSwatchHeight) - swatchView.autoPinEdge(toSuperviewMargin: .top, relation: .greaterThanOrEqual) - swatchView.autoPinEdge(toSuperviewMargin: .bottom, relation: .greaterThanOrEqual) - swatchView.autoPinLeadingToSuperviewMargin() - swatchView.autoPinTrailingToSuperviewMargin() + let cellHeight: CGFloat = 64 + + selectedRing.autoSetDimensions(to: CGSize(width: cellHeight, height: cellHeight)) + selectedRing.layer.cornerRadius = cellHeight / 2 + selectedRing.layer.borderColor = Theme.secondaryColor.cgColor + selectedRing.layer.borderWidth = 2 + selectedRing.autoPinEdgesToSuperviewEdges() + selectedRing.isHidden = true + + swatchView.backgroundColor = conversationColor.primaryColor + let swatchSize: CGFloat = 48 + self.swatchView.layer.cornerRadius = swatchSize / 2 + 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 colorPickerDidCancel(_ colorPicker: ColorPickerViewController) - func colorPicker(_ colorPicker: ColorPickerViewController, didPickColorName colorName: String) + func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor) } -@objc -class ColorPickerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { - - private let pickerView: UIPickerView - private let thread: TSThread - private let colorNames: [String] - - @objc public weak var delegate: ColorPickerDelegate? +@objc(OWSColorPicker) +class ColorPicker: NSObject, ColorPickerViewDelegate { @objc - required init(thread: TSThread) { - self.thread = thread - self.pickerView = UIPickerView() - self.colorNames = OWSConversationColor.conversationColorNames + public weak var delegate: ColorPickerDelegate? - super.init(nibName: nil, bundle: nil) + @objc + let sheetViewController: SheetViewController - self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel)) - self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(didTapSave)) + private let currentConversationColor: OWSConversationColor - pickerView.dataSource = self - pickerView.delegate = self + @objc + init(currentConversationColor: OWSConversationColor) { + self.currentConversationColor = currentConversationColor + sheetViewController = SheetViewController() + + super.init() + + let colorPickerView = ColorPickerView() + 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] + weak var delegate: ColorPickerViewDelegate? + + override init(frame: CGRect) { + let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) } + + self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) } + + super.init(frame: frame) + + colorViews.forEach { $0.delegate = self } + + let headerView = self.buildHeaderView() + let paletteView = self.buildPaletteView(colorViews: colorViews) + + let rowsStackView = UIStackView(arrangedSubviews: [headerView, paletteView]) + rowsStackView.axis = .vertical + addSubview(rowsStackView) + rowsStackView.autoPinEdgesToSuperviewEdges() } required init?(coder aDecoder: NSCoder) { notImplemented() } - override func loadView() { - self.view = UIView() - view.backgroundColor = Theme.backgroundColor - view.addSubview(pickerView) + // MARK: ColorViewDelegate - pickerView.autoVCenterInSuperview() - pickerView.autoPinLeadingToSuperviewMargin() - pickerView.autoPinTrailingToSuperviewMargin() + func colorViewWasTapped(_ colorView: ColorView) { + self.select(conversationColor: colorView.conversationColor) + self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor) } - override func viewDidLoad() { - super.viewDidLoad() - - let colorName = thread.conversationColorName - if let index = colorNames.index(of: colorName) { - pickerView.selectRow(index, inComponent: 0, animated: false) + fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) { + colorViews.forEach { colorView in + colorView.isSelected = colorView.conversationColor == selectedConversationColor } } - // MARK: UIPickerViewDataSource + // MARK: View Building - public func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 + 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 } - public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return self.colorNames.count - } + private func buildPaletteView(colorViews: [ColorView]) -> UIView { + let paletteView = UIView() + paletteView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) - // MARK: UIPickerViewDelegate - - public func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { - let vMargin: CGFloat = 16 - return colorSwatchHeight + vMargin * 2 - } - - public func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { - guard let colorName = colorNames[safe: row] else { - owsFailDebug("color was unexpectedly nil") - return ColorView(color: .white) + let kRowLength = 4 + let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in + let row = UIStackView(arrangedSubviews: colorViewsInRow) + row.distribution = UIStackViewDistribution.equalSpacing + return row } - guard let colors = OWSConversationColor.conversationColor(colorName: colorName) else { - owsFailDebug("unknown color name") - return ColorView(color: OWSConversationColor.default().themeColor) - } - return ColorView(color: colors.themeColor) - } + let rowsStackView = UIStackView(arrangedSubviews: rows) + rowsStackView.axis = .vertical + rowsStackView.spacing = ScaleFromIPhone5To7Plus(16, 50) - // MARK: Actions - - var currentColorName: String { - let index = pickerView.selectedRow(inComponent: 0) - guard let colorName = colorNames[safe: index] else { - owsFailDebug("index was unexpectedly nil") - return OWSConversationColor.defaultConversationColorName() - } - return colorName - } - - @objc - public func didTapSave() { - let colorName = self.currentColorName - self.delegate?.colorPicker(self, didPickColorName: colorName) - } - - @objc - public func didTapCancel() { - self.delegate?.colorPickerDidCancel(self) + paletteView.addSubview(rowsStackView) + rowsStackView.ows_autoPinToSuperviewMargins() + return paletteView } } diff --git a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m index 2478223bc..41356ba96 100644 --- a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m +++ b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m @@ -22,6 +22,7 @@ #import #import #import +#import #import #import #import @@ -41,7 +42,8 @@ const CGFloat kIconViewLength = 24; @interface OWSConversationSettingsViewController () + ColorPickerDelegate, + OWSSheetViewControllerDelegate> @property (nonatomic) TSThread *thread; @property (nonatomic) YapDatabaseConnection *uiDatabaseConnection; @@ -57,6 +59,7 @@ const CGFloat kIconViewLength = 24; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @property (nonatomic, readonly) UIImageView *avatarView; @property (nonatomic, readonly) UILabel *disappearingMessagesDurationLabel; +@property (nonatomic) OWSColorPicker *colorPicker; @end @@ -247,6 +250,11 @@ 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.delegate = self; + [self updateTableContents]; } @@ -1299,18 +1307,22 @@ const CGFloat kIconViewLength = 24; - (void)showColorPicker { - ColorPickerViewController *pickerController = [[ColorPickerViewController alloc] initWithThread:self.thread]; - pickerController.delegate = self; - OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:pickerController]; + OWSSheetViewController *sheetViewController = self.colorPicker.sheetViewController; + sheetViewController.delegate = self; - [self presentViewController:modal animated:YES completion:nil]; + [self presentViewController:sheetViewController + animated:YES + completion:^() { + OWSLogInfo(@"presented sheet view"); + }]; } -- (void)colorPicker:(ColorPickerViewController *)colorPicker didPickColorName:(NSString *)colorName +- (void)colorPicker:(OWSColorPicker *)colorPicker + didPickConversationColor:(OWSConversationColor *_Nonnull)conversationColor { - OWSLogDebug(@"picked color: %@", colorName); + OWSLogDebug(@"picked color: %@", conversationColor.name); [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [self.thread updateConversationColorName:colorName transaction:transaction]; + [self.thread updateConversationColorName:conversationColor.name transaction:transaction]; }]; [self.contactsManager.avatarCache removeAllImages]; @@ -1323,11 +1335,11 @@ const CGFloat kIconViewLength = 24; OWSAssertDebug(operation.isReady); [operation start]; }); - - [self dismissViewControllerAnimated:YES completion:nil]; } -- (void)colorPickerDidCancel:(ColorPickerViewController *)colorPicker +#pragma mark - OWSSheetViewController + +- (void)sheetViewControllerRequestedDismiss:(OWSSheetViewController *)sheetViewController { [self dismissViewControllerAnimated:YES completion:nil]; } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 0267986af..0abea05b8 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -386,6 +386,9 @@ /* 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."; +/* Modal Sheet title when picking a conversation color. */ +"COLOR_PICKER_SHEET_TITLE" = "Conversation Color"; + /* Activity Sheet label */ "COMPARE_SAFETY_NUMBER_ACTION" = "Compare with Clipboard"; diff --git a/SignalMessaging/ViewControllers/SheetViewController.swift b/SignalMessaging/ViewControllers/SheetViewController.swift index 78d16bba8..4de6bef26 100644 --- a/SignalMessaging/ViewControllers/SheetViewController.swift +++ b/SignalMessaging/ViewControllers/SheetViewController.swift @@ -19,6 +19,7 @@ public class SheetViewController: UIViewController { public let contentView: UIView = UIView() private let sheetView: SheetView = SheetView() + private let handleView: UIView = UIView() deinit { Logger.verbose("") @@ -97,7 +98,6 @@ public class SheetViewController: UIViewController { fileprivate func animateDismiss(completion: @escaping (Bool) -> Void) { guard let sheetViewVerticalConstraint = self.sheetViewVerticalConstraint else { owsFailDebug("sheetVerticalConstraint was unexpectedly nil") - // self.delegate?.sheetViewDidHide(self) return } diff --git a/SignalMessaging/appearance/OWSConversationColor.h b/SignalMessaging/appearance/OWSConversationColor.h index 66f0cf9bd..a8eceb6a3 100644 --- a/SignalMessaging/appearance/OWSConversationColor.h +++ b/SignalMessaging/appearance/OWSConversationColor.h @@ -8,16 +8,17 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSConversationColor : NSObject +@property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) UIColor *primaryColor; @property (nonatomic, readonly) UIColor *shadeColor; @property (nonatomic, readonly) UIColor *tintColor; @property (nonatomic, readonly) UIColor *themeColor; -+ (OWSConversationColor *)conversationColorWithPrimaryColor:(UIColor *)primaryColor - shadeColor:(UIColor *)shadeColor - tintColor:(UIColor *)tintColor; - ++ (OWSConversationColor *)conversationColorWithName:(NSString *)name + primaryColor:(UIColor *)primaryColor + shadeColor:(UIColor *)shadeColor + tintColor:(UIColor *)tintColor; #pragma mark - Conversation Colors @property (class, readonly, nonatomic) UIColor *ows_crimsonColor; diff --git a/SignalMessaging/appearance/OWSConversationColor.m b/SignalMessaging/appearance/OWSConversationColor.m index d5692fcbc..122d984f2 100644 --- a/SignalMessaging/appearance/OWSConversationColor.m +++ b/SignalMessaging/appearance/OWSConversationColor.m @@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSConversationColor () +@property (nonatomic) NSString *name; @property (nonatomic) UIColor *primaryColor; @property (nonatomic) UIColor *shadeColor; @property (nonatomic) UIColor *tintColor; @@ -20,22 +21,36 @@ NS_ASSUME_NONNULL_BEGIN @implementation OWSConversationColor -+ (OWSConversationColor *)conversationColorWithPrimaryColor:(UIColor *)primaryColor - shadeColor:(UIColor *)shadeColor - tintColor:(UIColor *)tintColor ++ (OWSConversationColor *)conversationColorWithName:(NSString *)name + primaryColor:(UIColor *)primaryColor + shadeColor:(UIColor *)shadeColor + tintColor:(UIColor *)tintColor { OWSConversationColor *instance = [OWSConversationColor new]; + instance.name = name; instance.primaryColor = primaryColor; instance.shadeColor = shadeColor; instance.tintColor = tintColor; return instance; } +#pragma mark - + - (UIColor *)themeColor { return Theme.isDarkThemeEnabled ? self.shadeColor : self.primaryColor; } +- (BOOL)isEqual:(id)other +{ + if (![other isKindOfClass:[OWSConversationColor class]]) { + return NO; + } + + OWSConversationColor *otherColor = (OWSConversationColor *)other; + return [self.name isEqual:otherColor.name]; +} + #pragma mark - Conversation Colors + (UIColor *)ows_crimsonColor @@ -350,8 +365,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(primaryColor); OWSAssertDebug(shadeColor); OWSAssertDebug(tintColor); - return - [OWSConversationColor conversationColorWithPrimaryColor:primaryColor shadeColor:shadeColor tintColor:tintColor]; + return [OWSConversationColor conversationColorWithName:conversationColorName primaryColor:primaryColor shadeColor:shadeColor tintColor:tintColor]; } + (OWSConversationColor *)conversationColorOrDefaultForColorName:(NSString *)conversationColorName diff --git a/SignalMessaging/categories/Collection+OWS.swift b/SignalMessaging/categories/Collection+OWS.swift index c769d2444..5f1f1c8a8 100644 --- a/SignalMessaging/categories/Collection+OWS.swift +++ b/SignalMessaging/categories/Collection+OWS.swift @@ -9,3 +9,11 @@ public extension Collection { return indices.contains(index) ? self[index] : nil } } + +public extension Array { + func chunked(by chunkSize: Int) -> [[Element]] { + return stride(from: 0, to: self.count, by: chunkSize).map { + Array(self[$0.. NS_ASSUME_NONNULL_BEGIN From acd042c35a9ae737493b9ef98277eb1a165602ff Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 27 Sep 2018 11:31:33 -0600 Subject: [PATCH 3/3] Sort conversation colors --- .../appearance/OWSConversationColor.m | 140 +++++++++--------- 1 file changed, 69 insertions(+), 71 deletions(-) diff --git a/SignalMessaging/appearance/OWSConversationColor.m b/SignalMessaging/appearance/OWSConversationColor.m index 122d984f2..ce2f2611a 100644 --- a/SignalMessaging/appearance/OWSConversationColor.m +++ b/SignalMessaging/appearance/OWSConversationColor.m @@ -51,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN return [self.name isEqual:otherColor.name]; } -#pragma mark - Conversation Colors +#pragma mark - Conversation Color (Primary) + (UIColor *)ows_crimsonColor { @@ -237,75 +237,78 @@ NS_ASSUME_NONNULL_BEGIN return [UIColor colorWithRGBHex:0x5A5A63]; } -+ (NSDictionary *)conversationColorMap ++ (NSArray *)allConversationColors { - static NSDictionary *colorMap; + static NSArray *allConversationColors; static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ - colorMap = @{ - @"crimson" : self.ows_crimsonColor, - @"vermilion" : self.ows_vermilionColor, - @"burlap" : self.ows_burlapColor, - @"forest" : self.ows_forestColor, - @"wintergreen" : self.ows_wintergreenColor, - @"teal" : self.ows_tealColor, - @"blue" : self.ows_blueColor, - @"indigo" : self.ows_indigoColor, - @"violet" : self.ows_violetColor, - @"plum" : self.ows_plumColor, - @"taupe" : self.ows_taupeColor, - @"steel" : self.ows_steelColor, - }; + // Order here affects the order in the conversation color picker. + allConversationColors = @[ + [OWSConversationColor conversationColorWithName:@"crimson" + primaryColor:self.ows_crimsonColor + shadeColor:self.ows_crimsonShadeColor + tintColor:self.ows_crimsonTintColor], + [OWSConversationColor conversationColorWithName:@"vermilion" + primaryColor:self.ows_vermilionColor + shadeColor:self.ows_vermilionShadeColor + tintColor:self.ows_vermilionTintColor], + [OWSConversationColor conversationColorWithName:@"burlap" + primaryColor:self.ows_burlapColor + shadeColor:self.ows_burlapShadeColor + tintColor:self.ows_burlapTintColor], + [OWSConversationColor conversationColorWithName:@"forest" + primaryColor:self.ows_forestColor + shadeColor:self.ows_forestShadeColor + tintColor:self.ows_forestTintColor], + [OWSConversationColor conversationColorWithName:@"wintergreen" + primaryColor:self.ows_wintergreenColor + shadeColor:self.ows_wintergreenShadeColor + tintColor:self.ows_wintergreenTintColor], + [OWSConversationColor conversationColorWithName:@"teal" + primaryColor:self.ows_tealColor + shadeColor:self.ows_tealShadeColor + tintColor:self.ows_tealTintColor], + [OWSConversationColor conversationColorWithName:@"blue" + primaryColor:self.ows_blueColor + shadeColor:self.ows_blueShadeColor + tintColor:self.ows_blueTintColor], + [OWSConversationColor conversationColorWithName:@"indigo" + primaryColor:self.ows_indigoColor + shadeColor:self.ows_indigoShadeColor + tintColor:self.ows_indigoTintColor], + [OWSConversationColor conversationColorWithName:@"violet" + primaryColor:self.ows_violetColor + shadeColor:self.ows_violetShadeColor + tintColor:self.ows_violetTintColor], + [OWSConversationColor conversationColorWithName:@"plum" + primaryColor:self.ows_plumColor + shadeColor:self.ows_plumShadeColor + tintColor:self.ows_plumTintColor], + [OWSConversationColor conversationColorWithName:@"taupe" + primaryColor:self.ows_taupeColor + shadeColor:self.ows_taupeShadeColor + tintColor:self.ows_taupeTintColor], + [OWSConversationColor conversationColorWithName:@"steel" + primaryColor:self.ows_steelColor + shadeColor:self.ows_steelShadeColor + tintColor:self.ows_steelTintColor], + ]; }); - return colorMap; + return allConversationColors; } -+ (NSDictionary *)conversationColorMapShade ++ (NSDictionary *)conversationColorMap { - static NSDictionary *colorMap; + static NSDictionary *colorMap; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - colorMap = @{ - @"crimson" : self.ows_crimsonShadeColor, - @"vermilion" : self.ows_vermilionShadeColor, - @"burlap" : self.ows_burlapShadeColor, - @"forest" : self.ows_forestShadeColor, - @"wintergreen" : self.ows_wintergreenShadeColor, - @"teal" : self.ows_tealShadeColor, - @"blue" : self.ows_blueShadeColor, - @"indigo" : self.ows_indigoShadeColor, - @"violet" : self.ows_violetShadeColor, - @"plum" : self.ows_plumShadeColor, - @"taupe" : self.ows_taupeShadeColor, - @"steel" : self.ows_steelShadeColor, - }; - OWSAssertDebug([self.conversationColorMap.allKeys isEqualToArray:colorMap.allKeys]); - }); - - return colorMap; -} - -+ (NSDictionary *)conversationColorMapTint -{ - static NSDictionary *colorMap; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - colorMap = @{ - @"crimson" : self.ows_crimsonTintColor, - @"vermilion" : self.ows_vermilionTintColor, - @"burlap" : self.ows_burlapTintColor, - @"forest" : self.ows_forestTintColor, - @"wintergreen" : self.ows_wintergreenTintColor, - @"teal" : self.ows_tealTintColor, - @"blue" : self.ows_blueTintColor, - @"indigo" : self.ows_indigoTintColor, - @"violet" : self.ows_violetTintColor, - @"plum" : self.ows_plumTintColor, - @"taupe" : self.ows_taupeTintColor, - @"steel" : self.ows_steelTintColor, - }; - OWSAssertDebug([self.conversationColorMap.allKeys isEqualToArray:colorMap.allKeys]); + NSMutableDictionary *mutableColorMap = [NSMutableDictionary new]; + for (OWSConversationColor *conversationColor in self.allConversationColors) { + mutableColorMap[conversationColor.name] = conversationColor; + } + colorMap = [mutableColorMap copy]; }); return colorMap; @@ -344,7 +347,11 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)conversationColorNames { - return self.conversationColorMap.allKeys; + NSMutableArray *names = [NSMutableArray new]; + for (OWSConversationColor *conversationColor in self.allConversationColors) { + [names addObject:conversationColor.name]; + } + return [names copy]; } + (nullable OWSConversationColor *)conversationColorForColorName:(NSString *)conversationColorName @@ -356,16 +363,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(self.conversationColorMap[conversationColorName] != nil); } - UIColor *_Nullable primaryColor = self.conversationColorMap[conversationColorName]; - UIColor *_Nullable shadeColor = self.conversationColorMapShade[conversationColorName]; - UIColor *_Nullable tintColor = self.conversationColorMapTint[conversationColorName]; - if (!primaryColor || !shadeColor || !tintColor) { - return nil; - } - OWSAssertDebug(primaryColor); - OWSAssertDebug(shadeColor); - OWSAssertDebug(tintColor); - return [OWSConversationColor conversationColorWithName:conversationColorName primaryColor:primaryColor shadeColor:shadeColor tintColor:tintColor]; + return self.conversationColorMap[conversationColorName]; } + (OWSConversationColor *)conversationColorOrDefaultForColorName:(NSString *)conversationColorName