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