From 16df4f589e513fefee666aa811a097779cb6805b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 28 Jun 2018 11:28:14 -0600 Subject: [PATCH] conversation colors // FREEBIE --- Signal.xcodeproj/project.pbxproj | 4 + .../ColorPickerViewController.swift | 145 ++++++++++++++++++ .../Cells/OWSMessageBubbleView.m | 6 +- .../ConversationView/Cells/OWSMessageCell.m | 1 + .../Cells/OWSQuotedMessageView.h | 5 +- .../Cells/OWSQuotedMessageView.m | 11 +- .../ConversationHeaderView.swift | 7 +- .../ConversationInputToolbar.h | 3 + .../ConversationInputToolbar.m | 10 +- .../ConversationViewController.m | 9 +- .../ViewControllers/DebugUI/DebugUIMessages.m | 24 +-- .../OWSConversationSettingsViewController.m | 91 ++++++++++- .../OWSConversationSettingsViewDelegate.h | 1 + Signal/src/views/QuotedReplyPreview.swift | 6 +- .../translations/en.lproj/Localizable.strings | 3 + SignalMessaging/Views/AvatarImageView.swift | 2 +- SignalMessaging/Views/ContactCellView.m | 9 +- SignalMessaging/categories/UIColor+OWS.h | 10 +- SignalMessaging/categories/UIColor+OWS.m | 80 +++++----- SignalMessaging/utils/ConversationStyle.swift | 43 ++++-- SignalMessaging/utils/OWSAvatarBuilder.m | 3 + .../utils/OWSContactAvatarBuilder.h | 3 +- .../utils/OWSContactAvatarBuilder.m | 20 ++- SignalServiceKit/src/Contacts/TSThread.h | 6 +- SignalServiceKit/src/Contacts/TSThread.m | 81 ++++++++-- 25 files changed, 491 insertions(+), 92 deletions(-) create mode 100644 Signal/src/ViewControllers/ColorPickerViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index cf152dce9..766001427 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -411,6 +411,7 @@ 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; }; 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; }; 4AC4EA13C8A444455DAB351F /* Pods_SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */; }; + 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 */; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; @@ -1066,6 +1067,7 @@ 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = ""; }; 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = ""; }; 45FDA43420A4D22700396358 /* OWSNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSNavigationBar.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 = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -1690,6 +1692,7 @@ 340FC897204DAC8D007AEB0F /* ThreadSettings */, 34D1F0BE1F8EC1760066283D /* Utils */, 452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */, + 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -3272,6 +3275,7 @@ 348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, 457C87B82032645C008D52D6 /* DebugUINotifications.swift in Sources */, + 4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */, 340FC8D0205BF2FA007AEB0F /* OWSBackupIO.m in Sources */, 458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */, 4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */, diff --git a/Signal/src/ViewControllers/ColorPickerViewController.swift b/Signal/src/ViewControllers/ColorPickerViewController.swift new file mode 100644 index 000000000..6684f0a3b --- /dev/null +++ b/Signal/src/ViewControllers/ColorPickerViewController.swift @@ -0,0 +1,145 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +let colorSwatchHeight: CGFloat = 60 + +class ColorView: UIView { + let color: UIColor + let swatchView: UIView + + required init(color: UIColor) { + self.color = color + self.swatchView = UIView() + + super.init(frame: .zero) + + swatchView.backgroundColor = color + + self.swatchView.layer.cornerRadius = colorSwatchHeight / 2 + + 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() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@objc +protocol ColorPickerDelegate: class { + func colorPickerDidCancel(_ colorPicker: ColorPickerViewController) + func colorPicker(_ colorPicker: ColorPickerViewController, didPickColorName colorName: String) +} + +@objc +class ColorPickerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { + + private let pickerView: UIPickerView + private let thread: TSThread + private let colors: [UIColor] + + @objc public weak var delegate: ColorPickerDelegate? + + @objc + required init(thread: TSThread) { + self.thread = thread + self.pickerView = UIPickerView() + self.colors = UIColor.ows_conversationColors + + super.init(nibName: nil, bundle: nil) + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(didTapSave)) + + pickerView.dataSource = self + pickerView.delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + self.view = UIView() + view.backgroundColor = .white + view.addSubview(pickerView) + + pickerView.autoVCenterInSuperview() + pickerView.autoPinLeadingToSuperviewMargin() + pickerView.autoPinTrailingToSuperviewMargin() + } + + override func viewDidLoad() { + super.viewDidLoad() + + if let colorName = thread.conversationColorName, + let currentColor = UIColor.ows_conversationColor(colorName: colorName), + let index = colors.index(of: currentColor) { + pickerView.selectRow(index, inComponent: 0, animated: false) + } + } + + // MARK: UIPickerViewDataSource + + public func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return self.colors.count + } + + // MARK: UIPickerViewDelegate + + public func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat { + let vMargin: CGFloat = 2 + return colorSwatchHeight + vMargin * 2 + } + + public func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + guard let color = colors[safe: row] else { + owsFail("\(logTag) in \(#function) color was unexpectedly nil") + return ColorView(color: .white) + } + + return ColorView(color: color) + } + + // MARK: Actions + + var currentColor: UIColor { + let index = pickerView.selectedRow(inComponent: 0) + guard let color = self.colors[safe: index] else { + owsFail("\(self.logTag) in \(#function) index was unexpectedly nil") + return UIColor.white + } + + return color + } + + @objc + public func didTapSave() { + guard let colorName = UIColor.ows_conversationColorName(color: self.currentColor) else { + owsFail("\(self.logTag) in \(#function) colorName was unexpectedly nil") + self.delegate?.colorPickerDidCancel(self) + return + } + + self.delegate?.colorPicker(self, didPickColorName: colorName) + } + + @objc + public func didTapCancel() { + self.delegate?.colorPickerDidCancel(self) + } +} diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 68c9cb572..0c34f262b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -277,6 +277,7 @@ NS_ASSUME_NONNULL_BEGIN OWSQuotedMessageView *quotedMessageView = [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply displayableQuotedText:displayableQuotedText + conversationStyle:self.conversationStyle isOutgoing:isOutgoing]; quotedMessageView.delegate = self; @@ -494,7 +495,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); TSMessage *message = (TSMessage *)self.viewItem.interaction; - return [ConversationStyle bubbleColorWithMessage:message]; + return [self.conversationStyle bubbleColorWithMessage:message]; } - (BOOL)hasBodyMediaWithThumbnail @@ -1084,6 +1085,7 @@ NS_ASSUME_NONNULL_BEGIN OWSQuotedMessageView *quotedMessageView = [OWSQuotedMessageView quotedMessageViewForConversation:self.viewItem.quotedReply displayableQuotedText:displayableQuotedText + conversationStyle:self.conversationStyle isOutgoing:isOutgoing]; CGSize result = [quotedMessageView sizeForMaxWidth:self.conversationStyle.maxMessageWidth]; return [NSValue valueWithCGSize:CGSizeCeil(result)]; @@ -1214,7 +1216,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); TSMessage *message = (TSMessage *)self.viewItem.interaction; - return [ConversationStyle bubbleTextColorWithMessage:message]; + return [self.conversationStyle bubbleTextColorWithMessage:message]; } - (BOOL)isMediaBeingSent diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 9da6dfdbd..8569900bc 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -296,6 +296,7 @@ NS_ASSUME_NONNULL_BEGIN TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction; OWSAvatarBuilder *avatarBuilder = [[OWSContactAvatarBuilder alloc] initWithSignalId:incomingMessage.authorId + color:self.conversationStyle.primaryColor diameter:self.avatarSize contactsManager:contactsManager]; self.avatarView.image = [avatarBuilder build]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h index f2b94469e..19f6b2f88 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.h @@ -4,6 +4,7 @@ NS_ASSUME_NONNULL_BEGIN +@class ConversationStyle; @class DisplayableText; @class OWSBubbleShapeView; @class OWSQuotedReplyModel; @@ -33,10 +34,12 @@ NS_ASSUME_NONNULL_BEGIN // Factory method for "message bubble" views. + (OWSQuotedMessageView *)quotedMessageViewForConversation:(OWSQuotedReplyModel *)quotedMessage displayableQuotedText:(nullable DisplayableText *)displayableQuotedText + conversationStyle:(ConversationStyle *)conversationStyle isOutgoing:(BOOL)isOutgoing; // Factory method for "message compose" views. -+ (OWSQuotedMessageView *)quotedMessageViewForPreview:(OWSQuotedReplyModel *)quotedMessage; ++ (OWSQuotedMessageView *)quotedMessageViewForPreview:(OWSQuotedReplyModel *)quotedMessage + conversationStyle:(ConversationStyle *)conversationStyle; @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m index 621f9f4e4..ef0acc6d8 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSQuotedMessageView.m @@ -21,6 +21,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage; @property (nonatomic, nullable, readonly) DisplayableText *displayableQuotedText; +@property (nonatomic, readonly) ConversationStyle *conversationStyle; @property (nonatomic, nullable) OWSBubbleShapeView *boundsStrokeView; @property (nonatomic, readonly) BOOL isForPreview; @@ -37,17 +38,20 @@ NS_ASSUME_NONNULL_BEGIN + (OWSQuotedMessageView *)quotedMessageViewForConversation:(OWSQuotedReplyModel *)quotedMessage displayableQuotedText:(nullable DisplayableText *)displayableQuotedText + conversationStyle:(ConversationStyle *)conversationStyle isOutgoing:(BOOL)isOutgoing { OWSAssert(quotedMessage); return [[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage displayableQuotedText:displayableQuotedText + conversationStyle:conversationStyle isForPreview:NO isOutgoing:isOutgoing]; } + (OWSQuotedMessageView *)quotedMessageViewForPreview:(OWSQuotedReplyModel *)quotedMessage + conversationStyle:(ConversationStyle *)conversationStyle { OWSAssert(quotedMessage); @@ -58,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN OWSQuotedMessageView *instance = [[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage displayableQuotedText:displayableQuotedText + conversationStyle:conversationStyle isForPreview:YES isOutgoing:YES]; [instance createContents]; @@ -66,6 +71,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithQuotedMessage:(OWSQuotedReplyModel *)quotedMessage displayableQuotedText:(nullable DisplayableText *)displayableQuotedText + conversationStyle:(ConversationStyle *)conversationStyle isForPreview:(BOOL)isForPreview isOutgoing:(BOOL)isOutgoing { @@ -80,6 +86,7 @@ NS_ASSUME_NONNULL_BEGIN _quotedMessage = quotedMessage; _displayableQuotedText = displayableQuotedText; _isForPreview = isForPreview; + _conversationStyle = conversationStyle; _isOutgoing = isOutgoing; _quotedAuthorLabel = [UILabel new]; @@ -104,7 +111,7 @@ NS_ASSUME_NONNULL_BEGIN - (UIColor *)highlightColor { BOOL isQuotingSelf = [NSObject isNullableObject:self.quotedMessage.authorId equalTo:TSAccountManager.localNumber]; - return (isQuotingSelf ? ConversationStyle.bubbleColorOutgoingSent : [UIColor colorWithRGBHex:0xB5B5B5]); + return (isQuotingSelf ? self.conversationStyle.bubbleColorOutgoingSent : [UIColor colorWithRGBHex:0xB5B5B5]); } #pragma mark - @@ -120,7 +127,7 @@ NS_ASSUME_NONNULL_BEGIN self.clipsToBounds = YES; self.boundsStrokeView = [OWSBubbleShapeView new]; - self.boundsStrokeView.strokeColor = ConversationStyle.bubbleColorIncoming; + self.boundsStrokeView.strokeColor = [self.conversationStyle primaryColor]; self.boundsStrokeView.strokeThickness = 1.f; [self addSubview:self.boundsStrokeView]; [self.boundsStrokeView autoPinToSuperviewEdges]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift b/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift index 17d306ed1..202737872 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift @@ -53,7 +53,7 @@ public class ConversationHeaderView: UIStackView { private let titleLabel: UILabel private let subtitleLabel: UILabel - private let avatarView: AvatarImageView + private let avatarView: ConversationAvatarImageView @objc public required init(thread: TSThread, contactsManager: OWSContactsManager) { @@ -115,6 +115,11 @@ public class ConversationHeaderView: UIStackView { return UILayoutFittingExpandedSize } + @objc + public func updateAvatar() { + self.avatarView.updateImage() + } + // MARK: Delegate Methods @objc func didTapView(tapGesture: UITapGestureRecognizer) { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h index f1ee7eeba..f4bd94382 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.h @@ -4,6 +4,7 @@ NS_ASSUME_NONNULL_BEGIN +@class ConversationStyle; @class OWSQuotedReplyModel; @class SignalAttachment; @@ -33,6 +34,8 @@ NS_ASSUME_NONNULL_BEGIN @interface ConversationInputToolbar : UIView +- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle NS_DESIGNATED_INITIALIZER; + @property (nonatomic, weak) id inputToolbarDelegate; - (void)beginEditingTextMessage; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 0571a1e42..bf322458d 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -27,6 +27,8 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; ConversationTextViewToolbarDelegate, QuotedReplyPreviewDelegate> +@property (nonatomic, readonly) ConversationStyle *conversationStyle; + @property (nonatomic, readonly) UIView *composeContainer; @property (nonatomic, readonly) ConversationInputTextView *inputTextView; @property (nonatomic, readonly) UIStackView *contentStackView; @@ -66,12 +68,16 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; @implementation ConversationInputToolbar -- (instancetype)init +- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle { self = [super init]; + + _conversationStyle = conversationStyle; + if (self) { [self createContents]; } + return self; } @@ -268,7 +274,7 @@ static const CGFloat ConversationInputToolbarBorderViewHeight = 0.5; return; } - self.quotedMessagePreview = [[QuotedReplyPreview alloc] initWithQuotedReply:quotedReply]; + self.quotedMessagePreview = [[QuotedReplyPreview alloc] initWithQuotedReply:quotedReply conversationStyle:self.conversationStyle]; self.quotedMessagePreview.delegate = self; // TODO animate diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 800e9fbc2..d534e8c07 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -564,7 +564,7 @@ typedef enum : NSUInteger { [self.collectionView applyScrollViewInsetsFix]; - _inputToolbar = [ConversationInputToolbar new]; + _inputToolbar = [[ConversationInputToolbar alloc] initWithConversationStyle:self.conversationStyle]; self.inputToolbar.inputToolbarDelegate = self; self.inputToolbar.inputTextViewDelegate = self; [self.collectionView autoPinToBottomLayoutGuideOfViewController:self withInset:0]; @@ -4326,6 +4326,13 @@ typedef enum : NSUInteger { }]; } +- (void)conversationColorWasUpdated +{ + [self.conversationStyle updateProperties]; + [self.headerView updateAvatar]; + [self.collectionView reloadData]; +} + - (void)groupWasUpdated:(TSGroupModel *)groupModel { OWSAssert(groupModel); diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 06a9f12dc..b62ccfc4a 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -1297,6 +1297,8 @@ NS_ASSUME_NONNULL_BEGIN messageState:TSOutgoingMessageStateSent text:@"⚠️ Outgoing Reserved Color Png ⚠️"]]; } + + ConversationStyle *conversationStyle = [[ConversationStyle alloc] initWithThread:thread]; [actions addObjectsFromArray:@[ [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing White Png" @@ -1326,7 +1328,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingUnsent] + backgroundColor:[conversationStyle bubbleColorOutgoingUnsent] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateFailed @@ -1334,7 +1336,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingUnsent] + backgroundColor:[conversationStyle bubbleColorOutgoingUnsent] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateSending @@ -1342,7 +1344,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingUnsent] + backgroundColor:[conversationStyle bubbleColorOutgoingUnsent] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateSent @@ -1351,7 +1353,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingSending] + backgroundColor:[conversationStyle bubbleColorOutgoingSending] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateFailed @@ -1359,7 +1361,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingSending] + backgroundColor:[conversationStyle bubbleColorOutgoingSending] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateSending @@ -1367,7 +1369,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingSending] + backgroundColor:[conversationStyle bubbleColorOutgoingSending] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateSent @@ -1376,7 +1378,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingSent] + backgroundColor:[conversationStyle bubbleColorOutgoingSent] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateFailed @@ -1384,7 +1386,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingSent] + backgroundColor:[conversationStyle bubbleColorOutgoingSent] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateSending @@ -1392,7 +1394,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingPngAction:thread actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorOutgoingSent] + backgroundColor:[conversationStyle bubbleColorOutgoingSent] textColor:[UIColor whiteColor] imageLabel:@"W" messageState:TSOutgoingMessageStateSent @@ -1560,7 +1562,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeIncomingPngAction:thread actionLabel:@"Fake Incoming 'Incoming' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorIncoming] + backgroundColor:[conversationStyle primaryColor] textColor:[UIColor whiteColor] imageLabel:@"W" isAttachmentDownloaded:YES @@ -1568,7 +1570,7 @@ NS_ASSUME_NONNULL_BEGIN [self fakeIncomingPngAction:thread actionLabel:@"Fake Incoming 'Incoming' Png" imageSize:CGSizeMake(200.f, 200.f) - backgroundColor:[ConversationStyle bubbleColorIncoming] + backgroundColor:[conversationStyle primaryColor] textColor:[UIColor whiteColor] imageLabel:@"W" isAttachmentDownloaded:NO diff --git a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m index 6fc0c9730..016c4ce52 100644 --- a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m +++ b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewController.m @@ -37,10 +37,13 @@ NS_ASSUME_NONNULL_BEGIN -@interface OWSConversationSettingsViewController () +@interface OWSConversationSettingsViewController () @property (nonatomic) TSThread *thread; @property (nonatomic) YapDatabaseConnection *uiDatabaseConnection; +@property (nonatomic, readonly) YapDatabaseConnection *editingDatabaseConnection; @property (nonatomic) NSArray *disappearingMessagesDurations; @property (nonatomic) OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration; @@ -123,6 +126,11 @@ NS_ASSUME_NONNULL_BEGIN object:nil]; } +- (YapDatabaseConnection *)editingDatabaseConnection +{ + return [OWSPrimaryStorage sharedManager].dbReadWriteConnection; +} + - (NSString *)threadName { NSString *threadName = self.thread.name; @@ -271,6 +279,19 @@ NS_ASSUME_NONNULL_BEGIN [weakSelf showMediaGallery]; }]]; + + [mainSection addItem:[OWSTableItem + itemWithCustomCellBlock:^{ + NSString *colorName = self.thread.conversationColorName; + UIColor *currentColor = [UIColor ows_conversationColorForColorName:colorName]; + NSString *title = NSLocalizedString(@"CONVERSATION_SETTINGS_CONVERSATION_COLOR", + @"Label for table cell which leads to picking a new conversation color"); + return [weakSelf disclosureCellWithName:title iconColor:currentColor]; + } + actionBlock:^{ + [weakSelf showColorPicker]; + }]]; + if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing && !self.hasExistingContact) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ @@ -623,6 +644,39 @@ NS_ASSUME_NONNULL_BEGIN return 12.f; } +- (UITableViewCell *)disclosureCellWithName:(NSString *)name iconColor:(UIColor *)iconColor +{ + OWSAssert(name.length > 0); + + UITableViewCell *cell = [UITableViewCell new]; + cell.preservesSuperviewLayoutMargins = YES; + cell.contentView.preservesSuperviewLayoutMargins = YES; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + + UIView *swatchView = [NeverClearView new]; + const CGFloat kSwatchWidth = 24; + [swatchView autoSetDimension:ALDimensionWidth toSize:kSwatchWidth]; + [swatchView autoSetDimension:ALDimensionHeight toSize:kSwatchWidth]; + swatchView.layer.cornerRadius = kSwatchWidth / 2; + swatchView.backgroundColor = iconColor; + + [cell.contentView addSubview:swatchView]; + [swatchView autoVCenterInSuperview]; + [swatchView autoPinLeadingToSuperviewMargin]; + + UILabel *rowLabel = [UILabel new]; + rowLabel.text = name; + rowLabel.textColor = [UIColor blackColor]; + rowLabel.font = [UIFont ows_regularFontWithSize:17.f]; + rowLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [cell.contentView addSubview:rowLabel]; + [rowLabel autoVCenterInSuperview]; + [rowLabel autoPinLeadingToTrailingEdgeOfView:swatchView offset:self.iconSpacing]; + [rowLabel autoPinTrailingToSuperviewMargin]; + + return cell; +} + - (UITableViewCell *)cellWithName:(NSString *)name iconName:(NSString *)iconName { OWSAssert(name.length > 0); @@ -1158,7 +1212,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)setThreadMutedUntilDate:(nullable NSDate *)value { - [self.thread updateWithMutedUntilDate:value]; + [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) { + [self.thread updateWithMutedUntilDate:value transaction:transaction]; + }]; + [self updateTableContents]; } @@ -1200,6 +1257,36 @@ NS_ASSUME_NONNULL_BEGIN } } +#pragma mark - ColorPickerDelegate + +- (void)showColorPicker +{ + ColorPickerViewController *pickerController = [[ColorPickerViewController alloc] initWithThread:self.thread]; + pickerController.delegate = self; + OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:pickerController]; + + [self presentViewController:modal animated:YES completion:nil]; +} + +- (void)colorPicker:(ColorPickerViewController *)colorPicker didPickColorName:(NSString *)colorName +{ + DDLogDebug(@"%@ in %s picked color: %@", self.logTag, __PRETTY_FUNCTION__, colorName); + [self.editingDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [self.thread updateConversationColorName:colorName transaction:transaction]; + + [self.contactsManager.avatarCache removeAllImages]; + [self updateTableContents]; + [self.conversationSettingsViewDelegate conversationColorWasUpdated]; + + [self dismissViewControllerAnimated:YES completion:nil]; + }]; +} + +- (void)colorPickerDidCancel:(ColorPickerViewController *)colorPicker +{ + [self dismissViewControllerAnimated:YES completion:nil]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewDelegate.h b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewDelegate.h index 6cfccb721..167ef038f 100644 --- a/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewDelegate.h +++ b/Signal/src/ViewControllers/ThreadSettings/OWSConversationSettingsViewDelegate.h @@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol OWSConversationSettingsViewDelegate +- (void)conversationColorWasUpdated; - (void)groupWasUpdated:(TSGroupModel *)groupModel; - (void)popAllConversationSettingsViews; diff --git a/Signal/src/views/QuotedReplyPreview.swift b/Signal/src/views/QuotedReplyPreview.swift index bddd21694..bda592916 100644 --- a/Signal/src/views/QuotedReplyPreview.swift +++ b/Signal/src/views/QuotedReplyPreview.swift @@ -15,6 +15,7 @@ class QuotedReplyPreview: UIView { public weak var delegate: QuotedReplyPreviewDelegate? private let quotedReply: OWSQuotedReplyModel + private let conversationStyle: ConversationStyle private var quotedMessageView: OWSQuotedMessageView? private var heightConstraint: NSLayoutConstraint! @@ -24,8 +25,9 @@ class QuotedReplyPreview: UIView { } @objc - init(quotedReply: OWSQuotedReplyModel) { + init(quotedReply: OWSQuotedReplyModel, conversationStyle: ConversationStyle) { self.quotedReply = quotedReply + self.conversationStyle = conversationStyle super.init(frame: .zero) @@ -42,7 +44,7 @@ class QuotedReplyPreview: UIView { // We instantiate quotedMessageView late to ensure that it is updated // every time contentSizeCategoryDidChange (i.e. when dynamic type // sizes changes). - let quotedMessageView = OWSQuotedMessageView(forPreview: quotedReply) + let quotedMessageView = OWSQuotedMessageView(forPreview: quotedReply, conversationStyle: conversationStyle) self.quotedMessageView = quotedMessageView quotedMessageView.backgroundColor = .clear diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 674cc0466..34780cb68 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -511,6 +511,9 @@ /* Navbar title when viewing settings for a 1-on-1 thread */ "CONVERSATION_SETTINGS_CONTACT_INFO_TITLE" = "Contact Info"; +/* Indicates that user's profile has been shared with a group. */ +"CONVERSATION_SETTINGS_CONVERSATION_COLOR" = "Color"; + /* Navbar title when viewing settings for a group thread */ "CONVERSATION_SETTINGS_GROUP_INFO_TITLE" = "Group Info"; diff --git a/SignalMessaging/Views/AvatarImageView.swift b/SignalMessaging/Views/AvatarImageView.swift index 8008cffbc..760590dfe 100644 --- a/SignalMessaging/Views/AvatarImageView.swift +++ b/SignalMessaging/Views/AvatarImageView.swift @@ -150,7 +150,7 @@ public class ConversationAvatarImageView: AvatarImageView { self.updateImage() } - func updateImage() { + public func updateImage() { Logger.debug("\(self.logTag) in \(#function) updateImage") self.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: diameter, contactsManager: contactsManager) diff --git a/SignalMessaging/Views/ContactCellView.m b/SignalMessaging/Views/ContactCellView.m index 56b2026f8..3e7bf7f8d 100644 --- a/SignalMessaging/Views/ContactCellView.m +++ b/SignalMessaging/Views/ContactCellView.m @@ -29,6 +29,7 @@ const CGFloat kContactCellAvatarTextMargin = 12; @property (nonatomic) UIView *accessoryViewContainer; @property (nonatomic) OWSContactsManager *contactsManager; +@property (nonatomic) TSThread *thread; @property (nonatomic) NSString *recipientId; @end @@ -139,7 +140,8 @@ const CGFloat kContactCellAvatarTextMargin = 12; - (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager { OWSAssert(thread); - + self.thread = thread; + // Update fonts to reflect changes to dynamic type. [self configureFonts]; @@ -194,7 +196,11 @@ const CGFloat kContactCellAvatarTextMargin = 12; return; } + NSString *colorName = self.thread.conversationColorName; + UIColor *color = [UIColor ows_conversationColorForColorName:colorName]; + self.avatarView.image = [[[OWSContactAvatarBuilder alloc] initWithSignalId:recipientId + color:color diameter:kContactCellAvatarSize contactsManager:contactsManager] build]; } @@ -230,6 +236,7 @@ const CGFloat kContactCellAvatarTextMargin = 12; { [[NSNotificationCenter defaultCenter] removeObserver:self]; + self.thread = nil; self.accessoryMessage = nil; self.nameLabel.text = nil; self.subtitleLabel.text = nil; diff --git a/SignalMessaging/categories/UIColor+OWS.h b/SignalMessaging/categories/UIColor+OWS.h index 838227f8c..f65f219f6 100644 --- a/SignalMessaging/categories/UIColor+OWS.h +++ b/SignalMessaging/categories/UIColor+OWS.h @@ -24,9 +24,17 @@ NS_ASSUME_NONNULL_BEGIN @property (class, readonly, nonatomic) UIColor *ows_toolbarBackgroundColor; @property (class, readonly, nonatomic) UIColor *ows_messageBubbleLightGrayColor; -+ (UIColor *)backgroundColorForContact:(NSString *)contactIdentifier; + (UIColor *)colorWithRGBHex:(unsigned long)value; +#pragma mark - ConversationColor + ++ (nullable UIColor *)ows_conversationColorForColorName:(NSString *)colorName NS_SWIFT_NAME(ows_conversationColor(colorName:)); ++ (nullable NSString *)ows_conversationColorNameForColor:(UIColor *)color + NS_SWIFT_NAME(ows_conversationColorName(color:)); + +@property (class, readonly, nonatomic) NSArray *ows_conversationColorNames; +@property (class, readonly, nonatomic) NSArray *ows_conversationColors; + - (UIColor *)blendWithColor:(UIColor *)otherColor alpha:(CGFloat)alpha; #pragma mark - diff --git a/SignalMessaging/categories/UIColor+OWS.m b/SignalMessaging/categories/UIColor+OWS.m index edbbdedc3..a456c680e 100644 --- a/SignalMessaging/categories/UIColor+OWS.m +++ b/SignalMessaging/categories/UIColor+OWS.m @@ -2,8 +2,8 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "OWSMath.h" #import "UIColor+OWS.h" +#import "OWSMath.h" #import NS_ASSUME_NONNULL_BEGIN @@ -98,40 +98,6 @@ NS_ASSUME_NONNULL_BEGIN return [UIColor colorWithHue:240.0f / 360.0f saturation:0.02f brightness:0.92f alpha:1.0f]; } -+ (UIColor *)backgroundColorForContact:(NSString *)contactIdentifier -{ - NSArray *colors = @[ - [UIColor colorWithRed:204.f / 255.f green:148.f / 255.f blue:102.f / 255.f alpha:1.f], - [UIColor colorWithRed:187.f / 255.f green:104.f / 255.f blue:62.f / 255.f alpha:1.f], - [UIColor colorWithRed:145.f / 255.f green:78.f / 255.f blue:48.f / 255.f alpha:1.f], - [UIColor colorWithRed:122.f / 255.f green:63.f / 255.f blue:41.f / 255.f alpha:1.f], - [UIColor colorWithRed:80.f / 255.f green:46.f / 255.f blue:27.f / 255.f alpha:1.f], - [UIColor colorWithRed:57.f / 255.f green:45.f / 255.f blue:19.f / 255.f alpha:1.f], - [UIColor colorWithRed:37.f / 255.f green:38.f / 255.f blue:13.f / 255.f alpha:1.f], - [UIColor colorWithRed:23.f / 255.f green:31.f / 255.f blue:10.f / 255.f alpha:1.f], - [UIColor colorWithRed:6.f / 255.f green:19.f / 255.f blue:10.f / 255.f alpha:1.f], - [UIColor colorWithRed:13.f / 255.f green:4.f / 255.f blue:16.f / 255.f alpha:1.f], - [UIColor colorWithRed:27.f / 255.f green:12.f / 255.f blue:44.f / 255.f alpha:1.f], - [UIColor colorWithRed:18.f / 255.f green:17.f / 255.f blue:64.f / 255.f alpha:1.f], - [UIColor colorWithRed:20.f / 255.f green:42.f / 255.f blue:77.f / 255.f alpha:1.f], - [UIColor colorWithRed:18.f / 255.f green:55.f / 255.f blue:68.f / 255.f alpha:1.f], - [UIColor colorWithRed:18.f / 255.f green:68.f / 255.f blue:61.f / 255.f alpha:1.f], - [UIColor colorWithRed:19.f / 255.f green:73.f / 255.f blue:26.f / 255.f alpha:1.f], - [UIColor colorWithRed:13.f / 255.f green:48.f / 255.f blue:15.f / 255.f alpha:1.f], - [UIColor colorWithRed:44.f / 255.f green:165.f / 255.f blue:137.f / 255.f alpha:1.f], - [UIColor colorWithRed:137.f / 255.f green:181.f / 255.f blue:48.f / 255.f alpha:1.f], - [UIColor colorWithRed:208.f / 255.f green:204.f / 255.f blue:78.f / 255.f alpha:1.f], - [UIColor colorWithRed:227.f / 255.f green:162.f / 255.f blue:150.f / 255.f alpha:1.f] - ]; - NSData *contactData = [contactIdentifier dataUsingEncoding:NSUTF8StringEncoding]; - - NSUInteger hashingLength = 8; - unsigned long long choose; - NSData *hashData = [Cryptography computeSHA256Digest:contactData truncatedToBytes:hashingLength]; - [hashData getBytes:&choose length:hashingLength]; - return [colors objectAtIndex:(choose % [colors count])]; -} - + (UIColor *)colorWithRGBHex:(unsigned long)value { CGFloat red = ((value >> 16) & 0xff) / 255.f; @@ -304,6 +270,50 @@ NS_ASSUME_NONNULL_BEGIN return [UIColor colorWithRGBHex:0x757575]; } ++ (NSDictionary *)ows_conversationColorMap +{ + static NSDictionary *colorMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + colorMap = @{ + @"red" : self.ows_red700Color, + @"pink": self.ows_pink600Color, + @"purple": self.ows_purple600Color, + @"indigo": self.ows_indigo600Color, + @"blue": self.ows_blue700Color, + @"cyan": self.ows_cyan800Color, + @"teal": self.ows_teal700Color, + @"green": self.ows_green800Color, + @"deep_orange": self.ows_deepOrange900Color, + @"grey": self.ows_grey600Color + }; + }); + + return colorMap; +} + ++ (NSArray *)ows_conversationColorNames +{ + return self.ows_conversationColorMap.allKeys; +} + ++ (NSArray *)ows_conversationColors +{ + return self.ows_conversationColorMap.allValues; +} + ++ (nullable UIColor *)ows_conversationColorForColorName:(NSString *)colorName +{ + OWSAssert(colorName.length > 0); + + return [self.ows_conversationColorMap objectForKey:colorName]; +} + ++ (nullable NSString *)ows_conversationColorNameForColor:(UIColor *)color +{ + return [self.ows_conversationColorMap allKeysForObject:color].firstObject; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/ConversationStyle.swift b/SignalMessaging/utils/ConversationStyle.swift index a454a4989..d0d9d50e1 100644 --- a/SignalMessaging/utils/ConversationStyle.swift +++ b/SignalMessaging/utils/ConversationStyle.swift @@ -55,6 +55,7 @@ public class ConversationStyle: NSObject { self.thread = thread self.isRTL = CurrentAppContext().isRTL + self.primaryColor = ConversationStyle.primaryColor(thread: thread) super.init() @@ -78,7 +79,8 @@ public class ConversationStyle: NSObject { // MARK: - - private func updateProperties() { + @objc + public func updateProperties() { if thread.isGroupThread() { gutterLeading = 40 gutterTrailing = 20 @@ -106,37 +108,52 @@ public class ConversationStyle: NSObject { textInsetHorizontal = 12 lastTextLineAxis = CGFloat(round(12 + messageTextFont.capHeight * 0.5)) + + self.primaryColor = ConversationStyle.primaryColor(thread: thread) } // MARK: Colors - // TODO: Remove this! Incoming bubble colors are now dynamic. - @objc - public static let bubbleColorIncoming = UIColor.ows_messageBubbleLightGray + private class func primaryColor(thread: TSThread) -> UIColor { + guard let colorName = thread.conversationColorName else { + return self.defaultBubbleColorIncoming + } + + guard let color = UIColor.ows_conversationColor(colorName: colorName) else { + return self.defaultBubbleColorIncoming + } + + return color + } + + private static let defaultBubbleColorIncoming = UIColor.ows_messageBubbleLightGray // TODO: @objc - public static let bubbleColorOutgoingUnsent = UIColor.ows_red + public let bubbleColorOutgoingUnsent = UIColor.ows_red // TODO: @objc - public static let bubbleColorOutgoingSending = UIColor.ows_light35 + public let bubbleColorOutgoingSending = UIColor.ows_light35 + + @objc + public let bubbleColorOutgoingSent = UIColor.ows_light10 @objc - public static let bubbleColorOutgoingSent = UIColor.ows_light10 + public var primaryColor: UIColor @objc - public static func bubbleColor(message: TSMessage) -> UIColor { + public func bubbleColor(message: TSMessage) -> UIColor { if message is TSIncomingMessage { - return ConversationStyle.bubbleColorIncoming + return primaryColor } else if let outgoingMessage = message as? TSOutgoingMessage { switch outgoingMessage.messageState { case .failed: - return ConversationStyle.bubbleColorOutgoingUnsent + return self.bubbleColorOutgoingUnsent case .sending: - return ConversationStyle.bubbleColorOutgoingSending + return self.bubbleColorOutgoingSending default: - return ConversationStyle.bubbleColorOutgoingSent + return self.bubbleColorOutgoingSent } } else { owsFail("Unexpected message type: \(message)") @@ -145,7 +162,7 @@ public class ConversationStyle: NSObject { } @objc - public static func bubbleTextColor(message: TSMessage) -> UIColor { + public func bubbleTextColor(message: TSMessage) -> UIColor { if message is TSIncomingMessage { return UIColor.ows_white } else if let outgoingMessage = message as? TSOutgoingMessage { diff --git a/SignalMessaging/utils/OWSAvatarBuilder.m b/SignalMessaging/utils/OWSAvatarBuilder.m index 49433c15c..85c8c3598 100644 --- a/SignalMessaging/utils/OWSAvatarBuilder.m +++ b/SignalMessaging/utils/OWSAvatarBuilder.m @@ -27,7 +27,10 @@ NS_ASSUME_NONNULL_BEGIN OWSAvatarBuilder *avatarBuilder; if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; + NSString *colorName = thread.conversationColorName; + UIColor *color = [UIColor ows_conversationColorForColorName:colorName]; avatarBuilder = [[OWSContactAvatarBuilder alloc] initWithSignalId:contactThread.contactIdentifier + color:color diameter:diameter contactsManager:contactsManager]; } else if ([thread isKindOfClass:[TSGroupThread class]]) { diff --git a/SignalMessaging/utils/OWSContactAvatarBuilder.h b/SignalMessaging/utils/OWSContactAvatarBuilder.h index e7d76d673..f3fef522e 100644 --- a/SignalMessaging/utils/OWSContactAvatarBuilder.h +++ b/SignalMessaging/utils/OWSContactAvatarBuilder.h @@ -14,7 +14,9 @@ NS_ASSUME_NONNULL_BEGIN /** * Build an avatar for a Signal recipient */ + - (instancetype)initWithSignalId:(NSString *)signalId + color:(UIColor *)color diameter:(NSUInteger)diameter contactsManager:(OWSContactsManager *)contactsManager; @@ -26,7 +28,6 @@ NS_ASSUME_NONNULL_BEGIN diameter:(NSUInteger)diameter contactsManager:(OWSContactsManager *)contactsManager; - @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/OWSContactAvatarBuilder.m b/SignalMessaging/utils/OWSContactAvatarBuilder.m index 1b4c7ec86..add15038f 100644 --- a/SignalMessaging/utils/OWSContactAvatarBuilder.m +++ b/SignalMessaging/utils/OWSContactAvatarBuilder.m @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) OWSContactsManager *contactsManager; @property (nonatomic, readonly) NSString *signalId; @property (nonatomic, readonly) NSString *contactName; +@property (nonatomic, readonly) UIColor *color; @property (nonatomic, readonly) NSUInteger diameter; @end @@ -32,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithContactId:(NSString *)contactId name:(NSString *)name + color:(UIColor *)color diameter:(NSUInteger)diameter contactsManager:(OWSContactsManager *)contactsManager { @@ -42,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN _signalId = contactId; _contactName = name; + _color = color; _diameter = diameter; _contactsManager = contactsManager; @@ -49,6 +52,7 @@ NS_ASSUME_NONNULL_BEGIN } - (instancetype)initWithSignalId:(NSString *)signalId + color:(UIColor *)color diameter:(NSUInteger)diameter contactsManager:(OWSContactsManager *)contactsManager { @@ -60,7 +64,7 @@ NS_ASSUME_NONNULL_BEGIN if (name.length == 0) { name = signalId; } - return [self initWithContactId:signalId name:name diameter:diameter contactsManager:contactsManager]; + return [self initWithContactId:signalId name:name color:color diameter:diameter contactsManager:contactsManager]; } - (instancetype)initWithNonSignalName:(NSString *)nonSignalName @@ -68,7 +72,15 @@ NS_ASSUME_NONNULL_BEGIN diameter:(NSUInteger)diameter contactsManager:(OWSContactsManager *)contactsManager { - return [self initWithContactId:colorSeed name:nonSignalName diameter:diameter contactsManager:contactsManager]; + + NSString *colorName = [TSThread stableConversationColorNameForString:colorSeed]; + UIColor *color = [UIColor ows_conversationColorForColorName:colorName]; + OWSAssert(color); + return [self initWithContactId:colorSeed + name:nonSignalName + color:color + diameter:diameter + contactsManager:contactsManager]; } #pragma mark - Instance methods @@ -113,9 +125,9 @@ NS_ASSUME_NONNULL_BEGIN } CGFloat fontSize = (CGFloat)self.diameter / 2.8; - UIColor *backgroundColor = [UIColor backgroundColorForContact:self.signalId]; + UIImage *image = [[JSQMessagesAvatarImageFactory avatarImageWithUserInitials:initials - backgroundColor:backgroundColor + backgroundColor:self.color textColor:[UIColor whiteColor] font:[UIFont ows_boldFontWithSize:fontSize] diameter:self.diameter] avatarImage]; diff --git a/SignalServiceKit/src/Contacts/TSThread.h b/SignalServiceKit/src/Contacts/TSThread.h index e09aff411..e6bf79886 100644 --- a/SignalServiceKit/src/Contacts/TSThread.h +++ b/SignalServiceKit/src/Contacts/TSThread.h @@ -33,6 +33,10 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSString *)name; +@property (readonly, nullable) NSString *conversationColorName; +- (void)updateConversationColorName:(NSString *)colorName transaction:(YapDatabaseReadWriteTransaction *)transaction; ++ (NSString *)stableConversationColorNameForString:(NSString *)colorSeed; + /** * @returns * Signal Id (e164) of the contact if it's a contact thread. @@ -154,7 +158,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Update With... Methods -- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate; +- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction; @end diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index 62b51e848..1c8cf546d 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -3,6 +3,7 @@ // #import "TSThread.h" +#import "Cryptography.h" #import "NSDate+OWS.h" #import "NSString+SSK.h" #import "OWSDisappearingMessagesConfiguration.h" @@ -21,9 +22,10 @@ NS_ASSUME_NONNULL_BEGIN @interface TSThread () @property (nonatomic) NSDate *creationDate; -@property (nonatomic, copy) NSDate *archivalDate; -@property (nonatomic) NSDate *lastMessageDate; -@property (nonatomic, copy) NSString *messageDraft; +@property (nonatomic, copy, nullable) NSDate *archivalDate; +@property (nonatomic, nullable) NSString *conversationColorName; +@property (nonatomic, nullable) NSDate *lastMessageDate; +@property (nonatomic, copy, nullable) NSString *messageDraft; @property (atomic, nullable) NSDate *mutedUntilDate; @end @@ -45,11 +47,26 @@ NS_ASSUME_NONNULL_BEGIN _lastMessageDate = nil; _creationDate = [NSDate date]; _messageDraft = nil; + _conversationColorName = [self.class randomConversationColorName]; } return self; } +- (nullable instancetype)initWithCoder:(NSCoder *)coder +{ + self = [super initWithCoder:coder]; + if (!self) { + return self; + } + + if (!_conversationColorName) { + _conversationColorName = [self.class stableConversationColorNameForString:self.uniqueId]; + } + + return self; +} + - (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { [self removeAllThreadInteractionsWithTransaction:transaction]; @@ -395,16 +412,58 @@ NS_ASSUME_NONNULL_BEGIN [mutedUntilDate timeIntervalSinceDate:now] > 0); } -#pragma mark - Update With... Methods +- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSThread *thread) { + [thread setMutedUntilDate:mutedUntilDate]; + }]; +} -- (void)updateWithMutedUntilDate:(NSDate *)mutedUntilDate +#pragma mark - Conversation Color + ++ (NSString *)randomConversationColorName { - [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [self applyChangeToSelfAndLatestCopy:transaction - changeBlock:^(TSThread *thread) { - [thread setMutedUntilDate:mutedUntilDate]; - }]; - }]; + NSUInteger count = self.conversationColorNames.count; + NSUInteger index = arc4random_uniform((uint32_t)count); + return [self.conversationColorNames objectAtIndex:index]; +} + ++ (NSString *)stableConversationColorNameForString:(NSString *)colorSeed +{ + NSData *contactData = [colorSeed dataUsingEncoding:NSUTF8StringEncoding]; + + NSUInteger hashingLength = sizeof(unsigned long long); + unsigned long long choose; + NSData *hashData = [Cryptography computeSHA256Digest:contactData truncatedToBytes:hashingLength]; + [hashData getBytes:&choose length:hashingLength]; + + NSUInteger index = (choose % [self.conversationColorNames count]); + return [self.conversationColorNames objectAtIndex:index]; +} + ++ (NSArray *)conversationColorNames +{ + return @[ + @"red", + @"pink", + @"purple", + @"indigo", + @"blue", + @"cyan", + @"teal", + @"green", + @"deep_orange", + @"grey" + ]; +} + +- (void)updateConversationColorName:(NSString *)colorName transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSThread *thread) { + thread.conversationColorName = colorName; + }]; } @end