From 5310810e1d0c919b8e1bd55b588f63bd84bfcad8 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 22 May 2019 14:33:54 -0400 Subject: [PATCH 01/26] Remove test stickers. --- .../Messages/Stickers/DefaultStickers.swift | 18 +----------------- SignalServiceKit/src/Util/FeatureFlags.swift | 3 --- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift b/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift index 3687d159c9..705e91f257 100644 --- a/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift +++ b/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift @@ -19,23 +19,7 @@ class DefaultStickerPack { } private class func parseAll() -> [StickerPackInfo: DefaultStickerPack] { - guard FeatureFlags.testBuiltInStickerPacks else { - return [:] - } - - // TODO: Replace with production values. - let packs = [ - DefaultStickerPack(packIdHex: "0123456789abcdef0123456789abcdef", - packKeyHex: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - shouldAutoInstall: true), - DefaultStickerPack(packIdHex: "aaaaaaaabbbbbbbbcccccccc00000000", - packKeyHex: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - shouldAutoInstall: false), - DefaultStickerPack(packIdHex: "aaaaaaaabbbbbbbbcccccccc11111111", - packKeyHex: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", - shouldAutoInstall: true) - ].compactMap { $0 } - + let packs = [DefaultStickerPack]() var result = [StickerPackInfo: DefaultStickerPack]() for pack in packs { result[pack.info] = pack diff --git a/SignalServiceKit/src/Util/FeatureFlags.swift b/SignalServiceKit/src/Util/FeatureFlags.swift index 4f49207dae..df191052de 100644 --- a/SignalServiceKit/src/Util/FeatureFlags.swift +++ b/SignalServiceKit/src/Util/FeatureFlags.swift @@ -65,9 +65,6 @@ public class FeatureFlags: NSObject { @objc public static let stickerPackOrdering = false - @objc - public static let testBuiltInStickerPacks = false - // Don't enable this flag until the Desktop changes have been in production for a while. @objc public static let strictSyncTranscriptTimestamps = false From 042ba57e741393254fd8aa9e2e5fc4dc95ac9b20 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 22 May 2019 15:10:03 -0400 Subject: [PATCH 02/26] Rework input toolbar layout. --- .../ConversationInputToolbar.m | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index ce843ed46a..ddbdc73309 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -89,6 +89,7 @@ const CGFloat kMaxTextViewHeight = 98; @property (nonatomic, readonly) StickerHorizontalListView *suggestedStickerView; @property (nonatomic) NSArray *suggestedStickerInfos; @property (nonatomic, readonly) UIStackView *outerStack; +@property (nonatomic, readonly) UIStackView *mediaStack; @property (nonatomic) CGFloat textViewHeight; @property (nonatomic, readonly) NSLayoutConstraint *textViewHeightConstraint; @@ -277,9 +278,16 @@ const CGFloat kMaxTextViewHeight = 98; [vStackWrapper setContentHuggingHorizontalLow]; [vStackWrapper setCompressionResistanceHorizontalLow]; + // Media Stack + _mediaStack = [[UIStackView alloc] initWithArrangedSubviews:@[ self.cameraButton, self.voiceMemoButton ]]; + self.mediaStack.axis = UILayoutConstraintAxisHorizontal; + self.mediaStack.alignment = UIStackViewAlignmentCenter; + [self.mediaStack setContentHuggingHorizontalHigh]; + [self.mediaStack setCompressionResistanceHorizontalHigh]; + // H Stack UIStackView *hStack = [[UIStackView alloc] - initWithArrangedSubviews:@[ self.cameraButton, vStackWrapper, self.attachmentButton, self.sendButton ]]; + initWithArrangedSubviews:@[ self.attachmentButton, vStackWrapper, self.mediaStack, self.sendButton ]]; hStack.axis = UILayoutConstraintAxisHorizontal; hStack.layoutMarginsRelativeArrangement = YES; hStack.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6); @@ -323,12 +331,9 @@ const CGFloat kMaxTextViewHeight = 98; self.preservesSuperviewLayoutMargins = NO; // Input buttons - [self addSubview:self.voiceMemoButton]; - [self.voiceMemoButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.inputTextView]; - [self.voiceMemoButton autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:vStackWrapper withOffset:-4]; [self addSubview:self.stickerButton]; - [self.stickerButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.voiceMemoButton]; - [self.voiceMemoButton autoPinLeadingToTrailingEdgeOfView:self.stickerButton offset:0]; + [self.stickerButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.inputTextView]; + [self.stickerButton autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:vStackWrapper withOffset:-4]; // Border // @@ -570,39 +575,27 @@ const CGFloat kMaxTextViewHeight = 98; - (void)ensureButtonVisibilityWithIsAnimated:(BOOL)isAnimated doLayout:(BOOL)doLayout { + void (^ensureViewHiddenState)(UIView *, BOOL) = ^(UIView *subview, BOOL hidden) { + if (subview.isHidden != hidden) { + subview.hidden = hidden; + } + }; + void (^updateBlock)(void) = ^{ + ensureViewHiddenState(self.attachmentButton, NO); + BOOL hasTextInput = self.inputTextView.trimmedText.length > 0; if (hasTextInput) { - if (!self.attachmentButton.isHidden) { - self.attachmentButton.hidden = YES; - } - if (!self.voiceMemoButton.isHidden) { - self.voiceMemoButton.hidden = YES; - } - if (self.sendButton.isHidden) { - self.sendButton.hidden = NO; - } + ensureViewHiddenState(self.mediaStack, YES); + ensureViewHiddenState(self.sendButton, NO); } else { - if (self.attachmentButton.isHidden) { - self.attachmentButton.hidden = NO; - } - if (self.voiceMemoButton.isHidden) { - self.voiceMemoButton.hidden = NO; - } - if (!self.sendButton.isHidden) { - self.sendButton.hidden = YES; - } + ensureViewHiddenState(self.mediaStack, NO); + ensureViewHiddenState(self.sendButton, YES); } BOOL hideStickerButton = hasTextInput || self.quotedReply != nil || !StickerManager.shared.isStickerSendEnabled; - if (hideStickerButton) { - if (!self.stickerButton.isHidden) { - self.stickerButton.hidden = YES; - } - } else { - if (self.stickerButton.isHidden) { - self.stickerButton.hidden = NO; - } + ensureViewHiddenState(self.stickerButton, hideStickerButton); + if (!hideStickerButton) { self.stickerButton.imageView.tintColor = (self.isStickerKeyboardActive ? Theme.primaryColor : Theme.navbarIconColor); } From ac1bc1e8df39dcd585b9f76c7ba36ba1ace133bb Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 16:32:16 -0700 Subject: [PATCH 03/26] Add VolumeButtons class --- Signal.xcodeproj/project.pbxproj | 4 + Signal/src/util/VolumeButtons.swift | 247 ++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 Signal/src/util/VolumeButtons.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ede3fcd703..671f88d9f4 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -535,6 +535,7 @@ 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; 76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; }; + 8811CF842295D8DA00FF6549 /* VolumeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */; }; 954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954AEE681DF33D32002E5410 /* ContactsPickerTest.swift */; }; A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; @@ -1315,6 +1316,7 @@ 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; 76EB03C218170B33006006FC /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 76EB03C318170B33006006FC /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtons.swift; sourceTree = ""; }; 8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = ""; }; 8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = ""; }; 948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = ""; }; @@ -2500,6 +2502,7 @@ 34E5DC8020D8050D00C08145 /* RegistrationUtils.h */, 34E5DC8120D8050D00C08145 /* RegistrationUtils.m */, 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */, + 8811CF832295D8DA00FF6549 /* VolumeButtons.swift */, FCFA64B11A24F29E0007FB87 /* UI Categories */, ); path = util; @@ -3689,6 +3692,7 @@ 450D19131F85236600970622 /* RemoteVideoView.m in Sources */, 34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */, 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */, + 8811CF842295D8DA00FF6549 /* VolumeButtons.swift in Sources */, 45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */, 451166C01FD86B98000739BA /* AccountManager.swift in Sources */, 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */, diff --git a/Signal/src/util/VolumeButtons.swift b/Signal/src/util/VolumeButtons.swift new file mode 100644 index 0000000000..cf9beddcca --- /dev/null +++ b/Signal/src/util/VolumeButtons.swift @@ -0,0 +1,247 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +protocol VolumeButtonObserver: class { + func didPressVolumeButton(with identifier: VolumeButtons.Identifier) + func didReleaseVolumeButton(with identifier: VolumeButtons.Identifier) + + func didTapVolumeButton(with identifier: VolumeButtons.Identifier) + + func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) + func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) + func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) +} + +class VolumeButtons { + static let shared = VolumeButtons() + + enum Identifier { + case up, down + } + + private init?() { + // If for some reason the API we’re using goes away (for example, in + // a future iOS version) this class will never instantiate. + guard VolumeButtons.supportsListeningToEvents else { return nil } + } + + deinit { + stopObservation() + } + + // MARK: Observer Management + + private var observers: [Weak] = [] + func addObserver(observer: VolumeButtonObserver) { + AssertIsOnMainThread() + + if observers.firstIndex(where: { $0.value === observer }) == nil { + observers.append(Weak(value: observer)) + } + + guard !observers.isEmpty else { return } + startObservation() + } + + func removeObserver(_ observer: VolumeButtonObserver) { + AssertIsOnMainThread() + + observers = observers.filter { $0.value !== observer } + + guard observers.isEmpty else { return } + stopObservation() + } + + func removeAllObservers() { + AssertIsOnMainThread() + observers = [] + stopObservation() + } + + private func startObservation() { + guard !VolumeButtons.isRegisteredForEvents else { return } + VolumeButtons.isRegisteredForEvents = true + registerForNotifications() + } + + private func stopObservation() { + VolumeButtons.isRegisteredForEvents = false + unregisterForNotifications() + + defer { resetLongPress() } + guard let longPressingButton = longPressingButton else { return } + notifyObserversOfCancelLongPress(with: longPressingButton) + } + + private func notifyObserversOfTap(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didTapVolumeButton(with: identifier) + } + } + + private func notifyObserversOfBeginLongPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didBeginLongPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfCompleteLongPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didCompleteLongPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfCancelLongPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didCancelLongPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfPress(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didPressVolumeButton(with: identifier) + } + } + + private func notifyObserversOfRelease(with identifier: Identifier) { + observers.forEach { observer in + observer.value?.didReleaseVolumeButton(with: identifier) + } + } + + // MARK: Tap / long press handling + + private var longPressTimer: Timer? + private var longPressingButton: Identifier? + + // It's not possible for up and down to be pressed simulataneously + // (if you press the second button, the OS will end the press on + // the first), so it allows for simplified handling here. + private func beginButtonPress(with identifier: Identifier) { + longPressingButton = nil + + longPressTimer?.invalidate() + longPressTimer = WeakTimer.scheduledTimer( + timeInterval: longPressDuration, + target: self, + userInfo: nil, + repeats: false + ) { [weak self] _ in + self?.longPressingButton = identifier + self?.notifyObserversOfBeginLongPress(with: identifier) + self?.longPressTimer?.invalidate() + self?.longPressTimer = nil + } + + notifyObserversOfPress(with: identifier) + } + + private func endButtonPress(with identifier: Identifier) { + if longPressingButton == identifier { + notifyObserversOfCompleteLongPress(with: identifier) + } else { + notifyObserversOfTap(with: identifier) + } + + resetLongPress() + + notifyObserversOfRelease(with: identifier) + } + + private func resetLongPress() { + longPressTimer?.invalidate() + longPressTimer = nil + longPressingButton = nil + } + + // MARK: Volume Event Registration + + // let encodedSelectorString = "setWantsVolumeButtonEvents:".encodedForSelector + private static let volumeEventsSelector = Selector("BXYGaHIABgVnAX0HfnZTBwYGAQBWCHYABgVL".decodedForSelector!) + + private(set) static var isRegisteredForEvents = false { + didSet { + setEventRegistration(isRegisteredForEvents) + } + } + + private static func setEventRegistration(_ active: Bool) { + typealias Type = @convention(c) (AnyObject, Selector, Bool) -> Void + let implementation = class_getMethodImplementation(UIApplication.self, volumeEventsSelector) + let setRegistration = unsafeBitCast(implementation, to: Type.self) + setRegistration(UIApplication.shared, volumeEventsSelector, active) + } + + private static var supportsListeningToEvents: Bool { + return UIApplication.shared.responds(to: volumeEventsSelector) + } + + // MARK: Notification Handling + + // let encodedDownDownNotificationName = "_UIApplicationVolumeDownButtonDownNotification".encodedForSelector + private let downDownNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAVQEJAF8BBnp3enRyBnoBAA==".decodedForSelector!) + + // let encodedDownUpNotificationName = "_UIApplicationVolumeDownButtonUpNotification".encodedForSelector + private let downUpNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAZgJfAQZ6d3p0cgZ6AQA=".decodedForSelector!) + + // let encodedUpDownNotificationName = "_UIApplicationVolumeUpButtonDownNotification".encodedForSelector + private let upDownNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAFUBCQBfAQZ6d3p0cgZ6AQA=".decodedForSelector!) + + // let encodedUpUpNotificationName = "_UIApplicationVolumeUpButtonUpNotification".encodedForSelector + private let upUpNotificationName = Notification.Name("cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAGYCXwEGend6dHIGegEA".decodedForSelector!) + + private let longPressDuration: TimeInterval = 0.5 + + private func registerForNotifications() { + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeUpButtonDown(_:)), + name: upDownNotificationName, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeUpButtonUp(_:)), + name: upUpNotificationName, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeDownButtonDown(_:)), + name: downDownNotificationName, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(didDetectVolumeDownButtonUp(_:)), + name: downUpNotificationName, + object: nil + ) + } + + private func unregisterForNotifications() { + NotificationCenter.default.removeObserver(self) + } + + @objc func didDetectVolumeUpButtonDown(_ notification: Notification) { + beginButtonPress(with: .up) + } + + @objc func didDetectVolumeUpButtonUp(_ notification: Notification) { + endButtonPress(with: .up) + } + + @objc func didDetectVolumeDownButtonDown(_ notification: Notification) { + beginButtonPress(with: .down) + } + + @objc func didDetectVolumeDownButtonUp(_ notification: Notification) { + endButtonPress(with: .down) + } +} From 6fcd9e2e37d57e309d74e05973ee7a1cf1c578b4 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 16:33:30 -0700 Subject: [PATCH 04/26] Cleanup animations on camera record button to better indicate state to the user --- .../Photos/PhotoCaptureViewController.swift | 89 +++++++++++++------ 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 097c395adc..3df608a8ea 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -70,7 +70,6 @@ class PhotoCaptureViewController: OWSViewController { view.addGestureRecognizer(doubleTapToSwitchCameraGesture) tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture) - doubleTapToSwitchCameraGesture.require(toFail: captureButton.tapGesture) } override func viewWillAppear(_ animated: Bool) { @@ -440,8 +439,6 @@ class CaptureButton: UIView { let innerButton = CircleView() - var tapGesture: UITapGestureRecognizer! - var longPressGesture: UILongPressGestureRecognizer! let longPressDuration = 0.5 @@ -457,11 +454,10 @@ class CaptureButton: UIView { override init(frame: CGRect) { super.init(frame: frame) - tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) - innerButton.addGestureRecognizer(tapGesture) - + // The long press handles both the tap and the hold interaction, as well as the animation + // the presents as the user begins to hold (and the button begins to grow prior to recording) longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress)) - longPressGesture.minimumPressDuration = longPressDuration + longPressGesture.minimumPressDuration = 0 innerButton.addGestureRecognizer(longPressGesture) addSubview(innerButton) @@ -485,14 +481,38 @@ class CaptureButton: UIView { fatalError("init(coder:) has not been implemented") } - // MARK: - Gestures + func beginRecordingAnimation(_ duration: TimeInterval, delay: TimeInterval = 0) { + UIView.beginAnimations("recordingAnimation", context: nil) + UIView.setAnimationDelay(delay) + UIView.setAnimationBeginsFromCurrentState(true) + UIView.setAnimationDuration(duration) + UIView.setAnimationCurve(.linear) - @objc - func didTap(_ gesture: UITapGestureRecognizer) { - delegate?.didTapCaptureButton(self) + innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + superview?.layoutIfNeeded() + + UIView.commitAnimations() } + func endRecordingAnimation(_ duration: TimeInterval) { + UIView.beginAnimations("recordingAnimation", context: nil) + UIView.setAnimationBeginsFromCurrentState(true) + UIView.setAnimationDuration(duration) + UIView.setAnimationCurve(.easeIn) + + innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } + zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } + superview?.layoutIfNeeded() + + UIView.commitAnimations() + } + + // MARK: - Gestures + var initialTouchLocation: CGPoint? + var touchTimer: Timer? + var isLongPressing = false @objc func didLongPress(_ gesture: UILongPressGestureRecognizer) { @@ -507,13 +527,24 @@ class CaptureButton: UIView { case .possible: break case .began: initialTouchLocation = gesture.location(in: gesture.view) - delegate?.didBeginLongPressCaptureButton(self) - UIView.animate(withDuration: 0.2) { - self.innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - self.zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - self.superview?.layoutIfNeeded() + beginRecordingAnimation(0.4, delay: 0.1) + + isLongPressing = false + + touchTimer?.invalidate() + touchTimer = WeakTimer.scheduledTimer( + timeInterval: longPressDuration, + target: self, + userInfo: nil, + repeats: false + ) { [weak self] _ in + guard let `self` = self else { return } + self.isLongPressing = true + self.delegate?.didBeginLongPressCaptureButton(self) } case .changed: + guard isLongPressing else { break } + guard let referenceHeight = delegate?.zoomScaleReferenceHeight else { owsFailDebug("referenceHeight was unexpectedly nil") return @@ -545,22 +576,25 @@ class CaptureButton: UIView { delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha) case .ended: - UIView.animate(withDuration: 0.2) { - self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } - self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } + endRecordingAnimation(0.2) - self.superview?.layoutIfNeeded() + if isLongPressing { + delegate?.didCompleteLongPressCaptureButton(self) + } else { + delegate?.didTapCaptureButton(self) } - delegate?.didCompleteLongPressCaptureButton(self) + + touchTimer?.invalidate() + touchTimer = nil case .cancelled, .failed: + endRecordingAnimation(0.2) - UIView.animate(withDuration: 0.2) { - self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } - self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } - - self.superview?.layoutIfNeeded() + if isLongPressing { + delegate?.didCancelLongPressCaptureButton(self) } - delegate?.didCancelLongPressCaptureButton(self) + + touchTimer?.invalidate() + touchTimer = nil } } } @@ -672,6 +706,7 @@ class RecordingTimerView: UIView { UIView.animate(withDuration: 0.4) { self.icon.alpha = 0 } + label.text = nil } // MARK: - From ad92923f52bbd88b7fc14e59cacd0963cf70a619 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 17:10:41 -0700 Subject: [PATCH 05/26] Hook into volume button handling for camera controls --- .../ViewControllers/Photos/PhotoCapture.swift | 60 ++++++++++++++++--- .../Photos/PhotoCaptureViewController.swift | 16 +++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index 8ce42fe565..83eb1adadd 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -25,6 +25,9 @@ protocol PhotoCaptureDelegate: AnyObject { func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture) var zoomScaleReferenceHeight: CGFloat? { get } var captureOrientation: AVCaptureVideoOrientation { get } + + func beginCaptureButtonAnimation(_ duration: TimeInterval) + func endCaptureButtonAnimation(_ duration: TimeInterval) } class PhotoCapture: NSObject { @@ -332,15 +335,10 @@ class PhotoCapture: NSObject { private func clampZoom(_ factor: CGFloat, device: AVCaptureDevice) -> CGFloat { return min(factor.clamp(minimumZoom, maximumZoom), device.activeFormat.videoMaxZoomFactor) } -} - -extension PhotoCapture: CaptureButtonDelegate { // MARK: - Photo - - func didTapCaptureButton(_ captureButton: CaptureButton) { + private func handleTap() { Logger.verbose("") - guard let delegate = delegate else { return } guard delegate.photoCaptureCanCaptureMoreItems(self) else { delegate.photoCaptureDidTryToCaptureTooMany(self) @@ -355,7 +353,7 @@ extension PhotoCapture: CaptureButtonDelegate { // MARK: - Video - func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) { + private func handleLongPressBegin() { AssertIsOnMainThread() Logger.verbose("") @@ -375,7 +373,7 @@ extension PhotoCapture: CaptureButtonDelegate { }.retainUntilComplete() } - func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { + private func handleLongPressComplete() { Logger.verbose("") sessionQueue.async { self.captureOutput.completeVideo(delegate: self) @@ -386,7 +384,7 @@ extension PhotoCapture: CaptureButtonDelegate { delegate?.photoCaptureDidCompleteVideo(self) } - func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) { + private func handleLongPressCancel() { Logger.verbose("") AssertIsOnMainThread() sessionQueue.async { @@ -394,6 +392,50 @@ extension PhotoCapture: CaptureButtonDelegate { } delegate?.photoCaptureDidCancelVideo(self) } +} + +extension PhotoCapture: VolumeButtonObserver { + func didPressVolumeButton(with identifier: VolumeButtons.Identifier) { + delegate?.beginCaptureButtonAnimation(0.5) + } + + func didReleaseVolumeButton(with identifier: VolumeButtons.Identifier) { + delegate?.endCaptureButtonAnimation(0.2) + } + + func didTapVolumeButton(with identifier: VolumeButtons.Identifier) { + handleTap() + } + + func didBeginLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { + handleLongPressBegin() + } + + func didCompleteLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { + handleLongPressComplete() + } + + func didCancelLongPressVolumeButton(with identifier: VolumeButtons.Identifier) { + handleLongPressCancel() + } +} + +extension PhotoCapture: CaptureButtonDelegate { + func didTapCaptureButton(_ captureButton: CaptureButton) { + handleTap() + } + + func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) { + handleLongPressBegin() + } + + func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { + handleLongPressComplete() + } + + func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) { + handleLongPressCancel() + } var zoomScaleReferenceHeight: CGFloat? { return delegate?.zoomScaleReferenceHeight diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 3df608a8ea..7691c12539 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -81,8 +81,14 @@ class PhotoCaptureViewController: OWSViewController { super.viewDidAppear(animated) if hasCaptureStarted { BenchEventComplete(eventId: "Show-Camera") + VolumeButtons.shared?.addObserver(observer: photoCapture) } } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillAppear(animated) + VolumeButtons.shared?.removeObserver(photoCapture) + } override var prefersStatusBarHidden: Bool { guard !OWSWindowManager.shared().hasCall() else { @@ -332,6 +338,8 @@ class PhotoCaptureViewController: OWSViewController { view.addSubview(captureButton) captureButton.autoHCenterInSuperview() captureButton.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true + + VolumeButtons.shared?.addObserver(observer: photoCapture) } private func showFailureUI(error: Error) { @@ -418,6 +426,14 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { var captureOrientation: AVCaptureVideoOrientation { return lastKnownCaptureOrientation } + + func beginCaptureButtonAnimation(_ duration: TimeInterval) { + captureButton.beginRecordingAnimation(duration) + } + + func endCaptureButtonAnimation(_ duration: TimeInterval) { + captureButton.endRecordingAnimation(duration) + } } // MARK: - Views From 1fe31d9b0c46730f9b73de5b87df976ba9935cc8 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 18:43:26 -0700 Subject: [PATCH 06/26] Fix a bug with capture feedback when using volume buttons --- .../Photos/PhotoCaptureViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 7691c12539..03f574279e 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -375,6 +375,13 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { captureFeedbackView.backgroundColor = .black view.insertSubview(captureFeedbackView, aboveSubview: previewView) captureFeedbackView.autoPinEdgesToSuperviewEdges() + + // Ensure the capture feedback is laid out before we remove it, + // depending on where we're coming from a layout pass might not + // trigger in 0.05 seconds otherwise. + view.setNeedsLayout() + view.layoutIfNeeded() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { captureFeedbackView.removeFromSuperview() } From 0fdb886b4fed929769900c9b8a52eb6e5e1cc56d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 10:17:12 -0400 Subject: [PATCH 07/26] Respond to CR. --- .../ConversationInputToolbar.m | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index ddbdc73309..6c744449fa 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -89,7 +89,6 @@ const CGFloat kMaxTextViewHeight = 98; @property (nonatomic, readonly) StickerHorizontalListView *suggestedStickerView; @property (nonatomic) NSArray *suggestedStickerInfos; @property (nonatomic, readonly) UIStackView *outerStack; -@property (nonatomic, readonly) UIStackView *mediaStack; @property (nonatomic) CGFloat textViewHeight; @property (nonatomic, readonly) NSLayoutConstraint *textViewHeightConstraint; @@ -279,15 +278,22 @@ const CGFloat kMaxTextViewHeight = 98; [vStackWrapper setCompressionResistanceHorizontalLow]; // Media Stack - _mediaStack = [[UIStackView alloc] initWithArrangedSubviews:@[ self.cameraButton, self.voiceMemoButton ]]; - self.mediaStack.axis = UILayoutConstraintAxisHorizontal; - self.mediaStack.alignment = UIStackViewAlignmentCenter; - [self.mediaStack setContentHuggingHorizontalHigh]; - [self.mediaStack setCompressionResistanceHorizontalHigh]; + UIStackView *mediaStack = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.sendButton, + self.cameraButton, + self.voiceMemoButton, + ]]; + mediaStack.axis = UILayoutConstraintAxisHorizontal; + mediaStack.alignment = UIStackViewAlignmentCenter; + [mediaStack setContentHuggingHorizontalHigh]; + [mediaStack setCompressionResistanceHorizontalHigh]; // H Stack - UIStackView *hStack = [[UIStackView alloc] - initWithArrangedSubviews:@[ self.attachmentButton, vStackWrapper, self.mediaStack, self.sendButton ]]; + UIStackView *hStack = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.attachmentButton, + vStackWrapper, + mediaStack, + ]]; hStack.axis = UILayoutConstraintAxisHorizontal; hStack.layoutMarginsRelativeArrangement = YES; hStack.layoutMargins = UIEdgeInsetsMake(6, 6, 6, 6); @@ -586,10 +592,12 @@ const CGFloat kMaxTextViewHeight = 98; BOOL hasTextInput = self.inputTextView.trimmedText.length > 0; if (hasTextInput) { - ensureViewHiddenState(self.mediaStack, YES); + ensureViewHiddenState(self.cameraButton, YES); + ensureViewHiddenState(self.voiceMemoButton, YES); ensureViewHiddenState(self.sendButton, NO); } else { - ensureViewHiddenState(self.mediaStack, NO); + ensureViewHiddenState(self.cameraButton, NO); + ensureViewHiddenState(self.voiceMemoButton, NO); ensureViewHiddenState(self.sendButton, YES); } From 600dd2eb93cf4e3e482e306ecbac4e5866b451d8 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 22 May 2019 15:26:45 -0400 Subject: [PATCH 08/26] Sticker design tweaks. --- .../ViewControllers/Stickers/StickerKeyboard.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift index e658e7fe86..5f475e6430 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift @@ -81,14 +81,17 @@ public class StickerKeyboard: UIStackView { autoresizingMask = .flexibleHeight alignment = .fill - addBackgroundView(withBackgroundColor: Theme.offBackgroundColor) + let backgroundColor = (Theme.isDarkThemeEnabled + ? Theme.offBackgroundColor + : UIColor.ows_gray02) + addBackgroundView(withBackgroundColor: backgroundColor) addArrangedSubview(headerView) headerView.setContentHuggingVerticalHigh() headerView.setCompressionResistanceVerticalHigh() stickerCollectionView.stickerDelegate = self - stickerCollectionView.backgroundColor = Theme.offBackgroundColor + stickerCollectionView.backgroundColor = backgroundColor addArrangedSubview(stickerCollectionView) stickerCollectionView.setContentHuggingVerticalLow() stickerCollectionView.setCompressionResistanceVerticalLow() From b07e004815ec48859e9d72393b6646c3e6ed2d27 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 22 May 2019 15:34:03 -0400 Subject: [PATCH 09/26] Sticker design tweaks. --- .../Stickers/StickerKeyboard.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift index 5f475e6430..300d3b1b73 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift @@ -81,17 +81,14 @@ public class StickerKeyboard: UIStackView { autoresizingMask = .flexibleHeight alignment = .fill - let backgroundColor = (Theme.isDarkThemeEnabled - ? Theme.offBackgroundColor - : UIColor.ows_gray02) - addBackgroundView(withBackgroundColor: backgroundColor) + addBackgroundView(withBackgroundColor: keyboardBackgroundColor) addArrangedSubview(headerView) headerView.setContentHuggingVerticalHigh() headerView.setCompressionResistanceVerticalHigh() stickerCollectionView.stickerDelegate = self - stickerCollectionView.backgroundColor = backgroundColor + stickerCollectionView.backgroundColor = keyboardBackgroundColor addArrangedSubview(stickerCollectionView) stickerCollectionView.setContentHuggingVerticalLow() stickerCollectionView.setCompressionResistanceVerticalLow() @@ -99,6 +96,12 @@ public class StickerKeyboard: UIStackView { populateHeaderView() } + private var keyboardBackgroundColor: UIColor { + return (Theme.isDarkThemeEnabled + ? Theme.offBackgroundColor + : UIColor.ows_gray02) + } + @objc public func wasPresented() { // If there are no recents, default to showing the first sticker pack. @@ -138,12 +141,10 @@ public class StickerKeyboard: UIStackView { private let packsCollectionView = StickerHorizontalListView(cellSize: StickerKeyboard.packCoverSize, spacing: StickerKeyboard.packCoverSpacing) private func populateHeaderView() { - backgroundColor = Theme.offBackgroundColor - headerView.spacing = StickerKeyboard.packCoverSpacing headerView.axis = .horizontal headerView.alignment = .center - headerView.backgroundColor = Theme.offBackgroundColor + headerView.backgroundColor = keyboardBackgroundColor headerView.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) headerView.isLayoutMarginsRelativeArrangement = true @@ -159,7 +160,7 @@ public class StickerKeyboard: UIStackView { } headerView.addArrangedSubview(recentsButton) - packsCollectionView.backgroundColor = Theme.offBackgroundColor + packsCollectionView.backgroundColor = keyboardBackgroundColor headerView.addArrangedSubview(packsCollectionView) let manageButton = buildHeaderButton("plus-24") { [weak self] in From 69ca57608a4e73b0c751cca1ff09b4694454c558 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 22 May 2019 15:48:30 -0400 Subject: [PATCH 10/26] Move "recent stickers" button into sticker pack list. --- .../ConversationInputToolbar.m | 12 +++--- .../Stickers/StickerHorizontalListView.swift | 42 ++++++++++++++++--- .../Stickers/StickerKeyboard.swift | 15 ++++--- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 6c744449fa..5edf95880c 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -1366,13 +1366,13 @@ const CGFloat kMaxTextViewHeight = 98; } __weak __typeof(self) weakSelf = self; BOOL shouldReset = self.suggestedStickerView.isHidden; - NSMutableArray *items = [NSMutableArray new]; + NSMutableArray> *items = [NSMutableArray new]; for (StickerInfo *stickerInfo in self.suggestedStickerInfos) { - [items addObject:[[StickerHorizontalListViewItem alloc] initWithStickerInfo:stickerInfo - selectedBlock:^{ - [weakSelf - didSelectSuggestedSticker:stickerInfo]; - }]]; + [items addObject:[[StickerHorizontalListViewItemSticker alloc] + initWithStickerInfo:stickerInfo + selectedBlock:^{ + [weakSelf didSelectSuggestedSticker:stickerInfo]; + }]]; } self.suggestedStickerView.items = items; self.suggestedStickerView.hidden = NO; diff --git a/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift b/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift index 5015f86581..cf8c91205d 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift @@ -5,15 +5,45 @@ import Foundation @objc -public class StickerHorizontalListViewItem: NSObject { - let stickerInfo: StickerInfo - let selectedBlock: () -> Void +public protocol StickerHorizontalListViewItem { + var view: UIView { get } + var selectedBlock: () -> Void { get } +} + +// MARK: - + +@objc +public class StickerHorizontalListViewItemSticker: NSObject, StickerHorizontalListViewItem { + private let stickerInfo: StickerInfo + public let selectedBlock: () -> Void @objc public init(stickerInfo: StickerInfo, selectedBlock: @escaping () -> Void) { self.stickerInfo = stickerInfo self.selectedBlock = selectedBlock } + + public var view: UIView { + return StickerView(stickerInfo: stickerInfo) + } +} + +// MARK: - + +@objc +public class StickerHorizontalListViewItemRecents: NSObject, StickerHorizontalListViewItem { + public let selectedBlock: () -> Void + + @objc + public init(selectedBlock: @escaping () -> Void) { + self.selectedBlock = selectedBlock + } + + public var view: UIView { + let imageView = UIImageView() + imageView.setTemplateImageName("recent-outline-24", tintColor: Theme.secondaryColor) + return imageView + } } // MARK: - @@ -122,9 +152,9 @@ extension StickerHorizontalListView: UICollectionViewDataSource { return cell } - let stickerView = StickerView(stickerInfo: item.stickerInfo) - cell.contentView.addSubview(stickerView) - stickerView.autoPinEdgesToSuperviewEdges() + let itemView = item.view + cell.contentView.addSubview(itemView) + itemView.autoPinEdgesToSuperviewEdges() return cell } diff --git a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift index 300d3b1b73..a13f6ac35c 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift @@ -123,12 +123,16 @@ public class StickerKeyboard: UIStackView { } } - let packItems = stickerPacks.map { (stickerPack) in - StickerHorizontalListView.Item(stickerInfo: stickerPack.coverInfo) { [weak self] in + var items = [StickerHorizontalListViewItem]() + items.append(StickerHorizontalListViewItemRecents { [weak self] in + self?.recentsButtonWasTapped() + }) + items += stickerPacks.map { (stickerPack) in + StickerHorizontalListViewItemSticker(stickerInfo: stickerPack.coverInfo) { [weak self] in self?.stickerPack = stickerPack } } - packsCollectionView.items = packItems + packsCollectionView.items = items guard stickerPacks.count > 0 else { stickerPack = nil @@ -155,11 +159,6 @@ public class StickerKeyboard: UIStackView { headerView.addArrangedSubview(searchButton) } - let recentsButton = buildHeaderButton("recent-outline-24") { [weak self] in - self?.recentsButtonWasTapped() - } - headerView.addArrangedSubview(recentsButton) - packsCollectionView.backgroundColor = keyboardBackgroundColor headerView.addArrangedSubview(packsCollectionView) From 829cdd1ad6a7f0cb1f294146cc7e976e58a380d0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 22 May 2019 17:20:38 -0400 Subject: [PATCH 11/26] Add selected state to sticker keyboard. --- .../ConversationInputToolbar.m | 11 ++-- .../Stickers/StickerHorizontalListView.swift | 62 +++++++++++++++---- .../Stickers/StickerKeyboard.swift | 26 +++++--- 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 5edf95880c..2c8e9c85e8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -303,8 +303,9 @@ const CGFloat kMaxTextViewHeight = 98; // Suggested Stickers const CGFloat suggestedStickerSize = 48; const CGFloat suggestedStickerSpacing = 12; - _suggestedStickerView = - [[StickerHorizontalListView alloc] initWithCellSize:suggestedStickerSize spacing:suggestedStickerSpacing]; + _suggestedStickerView = [[StickerHorizontalListView alloc] initWithCellSize:suggestedStickerSize + cellInset:0 + spacing:suggestedStickerSpacing]; self.suggestedStickerView.backgroundColor = UIColor.clearColor; self.suggestedStickerView.contentInset = UIEdgeInsetsMake( suggestedStickerSpacing, suggestedStickerSpacing, suggestedStickerSpacing, suggestedStickerSpacing); @@ -1370,9 +1371,9 @@ const CGFloat kMaxTextViewHeight = 98; for (StickerInfo *stickerInfo in self.suggestedStickerInfos) { [items addObject:[[StickerHorizontalListViewItemSticker alloc] initWithStickerInfo:stickerInfo - selectedBlock:^{ - [weakSelf didSelectSuggestedSticker:stickerInfo]; - }]]; + didSelectBlock:^{ + [weakSelf didSelectSuggestedSticker:stickerInfo]; + }]]; } self.suggestedStickerView.items = items; self.suggestedStickerView.hidden = NO; diff --git a/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift b/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift index cf8c91205d..5c1e97fa12 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift @@ -7,7 +7,8 @@ import Foundation @objc public protocol StickerHorizontalListViewItem { var view: UIView { get } - var selectedBlock: () -> Void { get } + var didSelectBlock: () -> Void { get } + var isSelected: Bool { get } } // MARK: - @@ -15,28 +16,46 @@ public protocol StickerHorizontalListViewItem { @objc public class StickerHorizontalListViewItemSticker: NSObject, StickerHorizontalListViewItem { private let stickerInfo: StickerInfo - public let selectedBlock: () -> Void + public let didSelectBlock: () -> Void + public let isSelectedBlock: () -> Bool + + // This initializer can be used for cells which are never selected. + @objc + public init(stickerInfo: StickerInfo, didSelectBlock: @escaping () -> Void) { + self.stickerInfo = stickerInfo + self.didSelectBlock = didSelectBlock + self.isSelectedBlock = { + false + } + } @objc - public init(stickerInfo: StickerInfo, selectedBlock: @escaping () -> Void) { + public init(stickerInfo: StickerInfo, didSelectBlock: @escaping () -> Void, isSelectedBlock: @escaping () -> Bool) { self.stickerInfo = stickerInfo - self.selectedBlock = selectedBlock + self.didSelectBlock = didSelectBlock + self.isSelectedBlock = isSelectedBlock } public var view: UIView { return StickerView(stickerInfo: stickerInfo) } + + public var isSelected: Bool { + return isSelectedBlock() + } } // MARK: - @objc public class StickerHorizontalListViewItemRecents: NSObject, StickerHorizontalListViewItem { - public let selectedBlock: () -> Void + public let didSelectBlock: () -> Void + public let isSelectedBlock: () -> Bool @objc - public init(selectedBlock: @escaping () -> Void) { - self.selectedBlock = selectedBlock + public init(didSelectBlock: @escaping () -> Void, isSelectedBlock: @escaping () -> Bool) { + self.didSelectBlock = didSelectBlock + self.isSelectedBlock = isSelectedBlock } public var view: UIView { @@ -44,6 +63,10 @@ public class StickerHorizontalListViewItemRecents: NSObject, StickerHorizontalLi imageView.setTemplateImageName("recent-outline-24", tintColor: Theme.secondaryColor) return imageView } + + public var isSelected: Bool { + return isSelectedBlock() + } } // MARK: - @@ -52,6 +75,7 @@ public class StickerHorizontalListViewItemRecents: NSObject, StickerHorizontalLi public class StickerHorizontalListView: UICollectionView { private let cellSize: CGFloat + private let cellInset: CGFloat public typealias Item = StickerHorizontalListViewItem @@ -77,8 +101,9 @@ public class StickerHorizontalListView: UICollectionView { private var heightConstraint: NSLayoutConstraint? @objc - public required init(cellSize: CGFloat, spacing: CGFloat = 0) { + public required init(cellSize: CGFloat, cellInset: CGFloat, spacing: CGFloat = 0) { self.cellSize = cellSize + self.cellInset = cellInset let layout = LinearHorizontalLayout(itemSize: CGSize(width: cellSize, height: cellSize), spacing: spacing) super.init(frame: .zero, collectionViewLayout: layout) @@ -122,7 +147,10 @@ extension StickerHorizontalListView: UICollectionViewDelegate { return } - item.selectedBlock() + item.didSelectBlock() + + // Selection has changed; update cells to reflect that. + self.reloadData() } } @@ -152,10 +180,22 @@ extension StickerHorizontalListView: UICollectionViewDataSource { return cell } + if item.isSelected { + let selectionView = UIView() + selectionView.backgroundColor = (Theme.isDarkThemeEnabled + ? UIColor(rgbHex: 0x1e1d1c) + : UIColor(rgbHex: 0xe1e2e3)) + selectionView.layer.cornerRadius = 8 + cell.contentView.addSubview(selectionView) + selectionView.autoPinEdgesToSuperviewEdges() + } + let itemView = item.view cell.contentView.addSubview(itemView) - itemView.autoPinEdgesToSuperviewEdges() - + itemView.autoPinEdge(toSuperviewEdge: .top, withInset: cellInset) + itemView.autoPinEdge(toSuperviewEdge: .bottom, withInset: cellInset) + itemView.autoPinEdge(toSuperviewEdge: .leading, withInset: cellInset) + itemView.autoPinEdge(toSuperviewEdge: .trailing, withInset: cellInset) return cell } } diff --git a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift index a13f6ac35c..7e4c723ddf 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift @@ -124,13 +124,18 @@ public class StickerKeyboard: UIStackView { } var items = [StickerHorizontalListViewItem]() - items.append(StickerHorizontalListViewItemRecents { [weak self] in + items.append(StickerHorizontalListViewItemRecents(didSelectBlock: { [weak self] in self?.recentsButtonWasTapped() - }) + }, isSelectedBlock: { [weak self] in + self?.stickerPack == nil + })) items += stickerPacks.map { (stickerPack) in - StickerHorizontalListViewItemSticker(stickerInfo: stickerPack.coverInfo) { [weak self] in - self?.stickerPack = stickerPack - } + StickerHorizontalListViewItemSticker(stickerInfo: stickerPack.coverInfo, + didSelectBlock: { [weak self] in + self?.stickerPack = stickerPack + }, isSelectedBlock: { [weak self] in + self?.stickerPack?.info == stickerPack.info + }) } packsCollectionView.items = items @@ -140,16 +145,19 @@ public class StickerKeyboard: UIStackView { } } - private static let packCoverSize: CGFloat = 24 - private static let packCoverSpacing: CGFloat = 12 - private let packsCollectionView = StickerHorizontalListView(cellSize: StickerKeyboard.packCoverSize, spacing: StickerKeyboard.packCoverSpacing) + private static let packCoverSize: CGFloat = 32 + private static let packCoverInset: CGFloat = 4 + private static let packCoverSpacing: CGFloat = 4 + private let packsCollectionView = StickerHorizontalListView(cellSize: StickerKeyboard.packCoverSize, + cellInset: StickerKeyboard.packCoverInset, + spacing: StickerKeyboard.packCoverSpacing) private func populateHeaderView() { headerView.spacing = StickerKeyboard.packCoverSpacing headerView.axis = .horizontal headerView.alignment = .center headerView.backgroundColor = keyboardBackgroundColor - headerView.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) + headerView.layoutMargins = UIEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6) headerView.isLayoutMarginsRelativeArrangement = true if FeatureFlags.stickerSearch { From 0313a0ce2a31c1ad076a0edd85b97d01600ebe4e Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 10:21:09 -0400 Subject: [PATCH 12/26] Respond to CR. --- .../ViewControllers/Stickers/StickerHorizontalListView.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift b/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift index 5c1e97fa12..640e75a85c 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerHorizontalListView.swift @@ -192,10 +192,7 @@ extension StickerHorizontalListView: UICollectionViewDataSource { let itemView = item.view cell.contentView.addSubview(itemView) - itemView.autoPinEdge(toSuperviewEdge: .top, withInset: cellInset) - itemView.autoPinEdge(toSuperviewEdge: .bottom, withInset: cellInset) - itemView.autoPinEdge(toSuperviewEdge: .leading, withInset: cellInset) - itemView.autoPinEdge(toSuperviewEdge: .trailing, withInset: cellInset) + itemView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: cellInset, leading: cellInset, bottom: cellInset, trailing: cellInset)) return cell } } From 4bd83073cf3874ac6c391de6e8b4f54c886e9e98 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Thu, 23 May 2019 08:46:38 -0700 Subject: [PATCH 13/26] PR Feedback --- .../Photos/PhotoCaptureViewController.swift | 57 ++++++++++--------- Signal/src/util/VolumeButtons.swift | 28 ++++----- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 03f574279e..13619d93dd 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -435,11 +435,11 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { } func beginCaptureButtonAnimation(_ duration: TimeInterval) { - captureButton.beginRecordingAnimation(duration) + captureButton.beginRecordingAnimation(duration: duration) } func endCaptureButtonAnimation(_ duration: TimeInterval) { - captureButton.endRecordingAnimation(duration) + captureButton.endRecordingAnimation(duration: duration) } } @@ -504,31 +504,32 @@ class CaptureButton: UIView { fatalError("init(coder:) has not been implemented") } - func beginRecordingAnimation(_ duration: TimeInterval, delay: TimeInterval = 0) { - UIView.beginAnimations("recordingAnimation", context: nil) - UIView.setAnimationDelay(delay) - UIView.setAnimationBeginsFromCurrentState(true) - UIView.setAnimationDuration(duration) - UIView.setAnimationCurve(.linear) - - innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } - superview?.layoutIfNeeded() - - UIView.commitAnimations() + func beginRecordingAnimation(duration: TimeInterval, delay: TimeInterval = 0) { + UIView.animate( + withDuration: duration, + delay: delay, + options: [.beginFromCurrentState, .curveLinear], + animations: { + self.innerButtonSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + self.zoomIndicatorSizeConstraints.forEach { $0.constant = type(of: self).recordingDiameter } + self.superview?.layoutIfNeeded() + }, + completion: nil + ) } - func endRecordingAnimation(_ duration: TimeInterval) { - UIView.beginAnimations("recordingAnimation", context: nil) - UIView.setAnimationBeginsFromCurrentState(true) - UIView.setAnimationDuration(duration) - UIView.setAnimationCurve(.easeIn) - - innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } - zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } - superview?.layoutIfNeeded() - - UIView.commitAnimations() + func endRecordingAnimation(duration: TimeInterval, delay: TimeInterval = 0) { + UIView.animate( + withDuration: duration, + delay: delay, + options: [.beginFromCurrentState, .curveEaseIn], + animations: { + self.innerButtonSizeConstraints.forEach { $0.constant = self.defaultDiameter } + self.zoomIndicatorSizeConstraints.forEach { $0.constant = self.defaultDiameter } + self.superview?.layoutIfNeeded() + }, + completion: nil + ) } // MARK: - Gestures @@ -550,7 +551,7 @@ class CaptureButton: UIView { case .possible: break case .began: initialTouchLocation = gesture.location(in: gesture.view) - beginRecordingAnimation(0.4, delay: 0.1) + beginRecordingAnimation(duration: 0.4, delay: 0.1) isLongPressing = false @@ -599,7 +600,7 @@ class CaptureButton: UIView { delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: alpha) case .ended: - endRecordingAnimation(0.2) + endRecordingAnimation(duration: 0.2) if isLongPressing { delegate?.didCompleteLongPressCaptureButton(self) @@ -610,7 +611,7 @@ class CaptureButton: UIView { touchTimer?.invalidate() touchTimer = nil case .cancelled, .failed: - endRecordingAnimation(0.2) + endRecordingAnimation(duration: 0.2) if isLongPressing { delegate?.didCancelLongPressCaptureButton(self) diff --git a/Signal/src/util/VolumeButtons.swift b/Signal/src/util/VolumeButtons.swift index cf9beddcca..7d2cef5b23 100644 --- a/Signal/src/util/VolumeButtons.swift +++ b/Signal/src/util/VolumeButtons.swift @@ -120,7 +120,7 @@ class VolumeButtons { // It's not possible for up and down to be pressed simulataneously // (if you press the second button, the OS will end the press on // the first), so it allows for simplified handling here. - private func beginButtonPress(with identifier: Identifier) { + private func didPressButton(with identifier: Identifier) { longPressingButton = nil longPressTimer?.invalidate() @@ -139,7 +139,7 @@ class VolumeButtons { notifyObserversOfPress(with: identifier) } - private func endButtonPress(with identifier: Identifier) { + private func didReleaseButton(with identifier: Identifier) { if longPressingButton == identifier { notifyObserversOfCompleteLongPress(with: identifier) } else { @@ -198,28 +198,28 @@ class VolumeButtons { private func registerForNotifications() { NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeUpButtonDown(_:)), + selector: #selector(didPressVolumeUp), name: upDownNotificationName, object: nil ) NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeUpButtonUp(_:)), + selector: #selector(didReleaseVolumeUp), name: upUpNotificationName, object: nil ) NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeDownButtonDown(_:)), + selector: #selector(didPressVolumeDown), name: downDownNotificationName, object: nil ) NotificationCenter.default.addObserver( self, - selector: #selector(didDetectVolumeDownButtonUp(_:)), + selector: #selector(didReleaseVolumeDown), name: downUpNotificationName, object: nil ) @@ -229,19 +229,19 @@ class VolumeButtons { NotificationCenter.default.removeObserver(self) } - @objc func didDetectVolumeUpButtonDown(_ notification: Notification) { - beginButtonPress(with: .up) + @objc func didPressVolumeUp(_ notification: Notification) { + didPressButton(with: .up) } - @objc func didDetectVolumeUpButtonUp(_ notification: Notification) { - endButtonPress(with: .up) + @objc func didReleaseVolumeUp(_ notification: Notification) { + didReleaseButton(with: .up) } - @objc func didDetectVolumeDownButtonDown(_ notification: Notification) { - beginButtonPress(with: .down) + @objc func didPressVolumeDown(_ notification: Notification) { + didPressButton(with: .down) } - @objc func didDetectVolumeDownButtonUp(_ notification: Notification) { - endButtonPress(with: .down) + @objc func didReleaseVolumeDown(_ notification: Notification) { + didReleaseButton(with: .down) } } From ed8267dde441d759193293c2d899112d55dc851d Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 23 May 2019 10:10:11 -0400 Subject: [PATCH 14/26] Proceed when capture succeeds --- .../ViewControllers/Photos/PhotoCapture.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index 8ce42fe565..f99ecdc2dd 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -449,8 +449,11 @@ extension PhotoCapture: CaptureOutputDelegate { AssertIsOnMainThread() if let error = error { - delegate?.photoCapture(self, processingDidError: error) - return + guard didSucceedDespiteError(error) else { + delegate?.photoCapture(self, processingDidError: error) + return + } + Logger.info("Ignoring error, since capture succeeded.") } guard let dataSource = DataSourcePath.dataSource(with: outputFileURL, shouldDeleteOnDeallocation: true) else { @@ -467,6 +470,19 @@ extension PhotoCapture: CaptureOutputDelegate { self.delegate?.photoCapture(self, didFinishProcessingAttachment: attachment) }.retainUntilComplete() } + + /// The AVCaptureFileOutput can return an error even though recording succeeds. + /// I can't find useful documentation on this, but Apple's example AVCam app silently + /// discards these errors, so we do the same. + /// These spurious errors can be reproduced 1/3 of the time when making a series of short videos. + private func didSucceedDespiteError(_ error: Error) -> Bool { + let nsError = error as NSError + guard let successfullyFinished = nsError.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as? Bool else { + return false + } + + return successfullyFinished + } } // MARK: - Capture Adapter From 1a371a184def1ef561102f3e28535fcc15a0c00f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 10:55:37 -0400 Subject: [PATCH 15/26] More sticker design tweaks. --- .../ConversationView/ConversationInputToolbar.m | 2 +- .../ViewControllers/Stickers/StickerKeyboard.swift | 2 +- .../Stickers/StickerPackViewController.swift | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 2c8e9c85e8..75e8e6eed5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -606,7 +606,7 @@ const CGFloat kMaxTextViewHeight = 98; ensureViewHiddenState(self.stickerButton, hideStickerButton); if (!hideStickerButton) { self.stickerButton.imageView.tintColor - = (self.isStickerKeyboardActive ? Theme.primaryColor : Theme.navbarIconColor); + = (self.isStickerKeyboardActive ? UIColor.ows_signalBlueColor : Theme.navbarIconColor); } [self updateSuggestedStickers]; diff --git a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift index 7e4c723ddf..13661d9a52 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift @@ -98,7 +98,7 @@ public class StickerKeyboard: UIStackView { private var keyboardBackgroundColor: UIColor { return (Theme.isDarkThemeEnabled - ? Theme.offBackgroundColor + ? UIColor.ows_gray90 : UIColor.ows_gray02) } diff --git a/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift b/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift index 5f98696c47..5ccf48d1e3 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift @@ -37,6 +37,8 @@ public class StickerPackViewController: OWSViewController { super.init(nibName: nil, bundle: nil) + self.modalPresentationStyle = .overFullScreen + stickerCollectionView.stickerDelegate = self stickerCollectionView.show(dataSource: dataSource) dataSource.add(delegate: self) @@ -58,8 +60,9 @@ public class StickerPackViewController: OWSViewController { view.backgroundColor = Theme.darkThemeBackgroundColor } else { view.backgroundColor = .clear + view.isOpaque = false - let blurEffect = Theme.darkThemeBarBlurEffect + let blurEffect = UIBlurEffect(style: Theme.isDarkThemeEnabled ? .light : .dark) let blurEffectView = UIVisualEffectView(effect: blurEffect) view.addSubview(blurEffectView) blurEffectView.autoPinEdgesToSuperviewEdges() @@ -250,6 +253,10 @@ public class StickerPackViewController: OWSViewController { return true } + override public var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + // - MARK: Events @objc From d8f4e9f8dba91eb4587dd752bce73404b647c5ab Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 11:04:46 -0400 Subject: [PATCH 16/26] More sticker design tweaks. --- Signal/src/views/LinkPreviewView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/src/views/LinkPreviewView.swift b/Signal/src/views/LinkPreviewView.swift index fcccd97e85..5d0c6a0771 100644 --- a/Signal/src/views/LinkPreviewView.swift +++ b/Signal/src/views/LinkPreviewView.swift @@ -843,7 +843,7 @@ public class LinkPreviewView: UIStackView { } if let sentBodyView = self.sentBodyView { let borderView = OWSBubbleShapeView(draw: ()) - let borderColor = UIColor(rgbHex: Theme.isDarkThemeEnabled ? 0x0F1012 : 0xD5D6D6) + let borderColor = UIColor(rgbHex: Theme.isDarkThemeEnabled ? 0x6b6d70 : 0xD5D6D6) borderView.strokeColor = borderColor borderView.strokeThickness = CGHairlineWidth() sentBodyView.addSubview(borderView) From 4cd7ab3da70a10c6657e30339c8dacbf7c5d6054 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 12:28:59 -0400 Subject: [PATCH 17/26] Respond to CR. --- .../ViewControllers/Stickers/StickerPackViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift b/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift index 5ccf48d1e3..4ada6e142d 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerPackViewController.swift @@ -62,7 +62,7 @@ public class StickerPackViewController: OWSViewController { view.backgroundColor = .clear view.isOpaque = false - let blurEffect = UIBlurEffect(style: Theme.isDarkThemeEnabled ? .light : .dark) + let blurEffect = Theme.barBlurEffect let blurEffectView = UIVisualEffectView(effect: blurEffect) view.addSubview(blurEffectView) blurEffectView.autoPinEdgesToSuperviewEdges() From 5a495a968eceebdcce7a77d0ddc20f521668012f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 11:16:18 -0400 Subject: [PATCH 18/26] Use PNG instead of JPEG for sticker pack link preview thumbnails, to preserve transparency. --- Signal/src/views/LinkPreviewView.swift | 8 +-- .../Interactions/OWSLinkPreview.swift | 51 ++++++++++++------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/Signal/src/views/LinkPreviewView.swift b/Signal/src/views/LinkPreviewView.swift index 5d0c6a0771..4b4fc6a64b 100644 --- a/Signal/src/views/LinkPreviewView.swift +++ b/Signal/src/views/LinkPreviewView.swift @@ -103,7 +103,7 @@ public class LinkPreviewDraft: NSObject, LinkPreviewState { } public func imageState() -> LinkPreviewImageState { - if linkPreviewDraft.jpegImageData != nil { + if linkPreviewDraft.imageData != nil { return .loaded } else { return .none @@ -113,11 +113,11 @@ public class LinkPreviewDraft: NSObject, LinkPreviewState { public func image() -> UIImage? { assert(imageState() == .loaded) - guard let jpegImageData = linkPreviewDraft.jpegImageData else { + guard let imageData = linkPreviewDraft.imageData else { return nil } - guard let image = UIImage(data: jpegImageData) else { - owsFailDebug("Could not load image: \(jpegImageData.count)") + guard let image = UIImage(data: imageData) else { + owsFailDebug("Could not load image: \(imageData.count)") return nil } return image diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 40d393c24e..604af19cc4 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -41,12 +41,16 @@ public class OWSLinkPreviewDraft: NSObject { public var title: String? @objc - public var jpegImageData: Data? + public var imageData: Data? - public init(urlString: String, title: String?, jpegImageData: Data? = nil) { + @objc + public var imageMimeType: String? + + public init(urlString: String, title: String?, imageData: Data? = nil, imageMimeType: String? = nil) { self.urlString = urlString self.title = title - self.jpegImageData = jpegImageData + self.imageData = imageData + self.imageMimeType = imageMimeType super.init() } @@ -56,7 +60,7 @@ public class OWSLinkPreviewDraft: NSObject { if let titleValue = title { hasTitle = titleValue.count > 0 } - let hasImage = jpegImageData != nil + let hasImage = imageData != nil && imageMimeType != nil return hasTitle || hasImage } @@ -181,8 +185,9 @@ public class OWSLinkPreview: MTLModel { guard SSKPreferences.areLinkPreviewsEnabled(transaction: transaction.asAnyRead) else { throw LinkPreviewError.noPreview } - let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(jpegImageData: info.jpegImageData, - transaction: transaction) + let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(imageData: info.imageData, + imageMimeType: info.imageMimeType, + transaction: transaction) let linkPreview = OWSLinkPreview(urlString: info.urlString, title: info.title, imageAttachmentId: imageAttachmentId) @@ -194,22 +199,28 @@ public class OWSLinkPreview: MTLModel { return linkPreview } - private class func saveAttachmentIfPossible(jpegImageData: Data?, + private class func saveAttachmentIfPossible(imageData: Data?, + imageMimeType: String?, transaction: YapDatabaseReadWriteTransaction) -> String? { - guard let jpegImageData = jpegImageData else { + guard let imageData = imageData else { return nil } - let fileSize = jpegImageData.count + guard let imageMimeType = imageMimeType else { + return nil + } + guard let fileExtension = MIMETypeUtil.fileExtension(forMIMEType: imageMimeType) else { + return nil + } + let fileSize = imageData.count guard fileSize > 0 else { owsFailDebug("Invalid file size for image data.") return nil } - let fileExtension = "jpg" - let contentType = OWSMimeTypeImageJpeg + let contentType = imageMimeType let filePath = OWSFileSystem.temporaryFilePath(withFileExtension: fileExtension) do { - try jpegImageData.write(to: NSURL.fileURL(withPath: filePath)) + try imageData.write(to: NSURL.fileURL(withPath: filePath)) } catch let error as NSError { owsFailDebug("file write failed: \(filePath), \(error)") return nil @@ -807,7 +818,10 @@ public class OWSLinkPreviewManager: NSObject { return downloadImage(url: imageUrlString, imageMimeType: imageMimeType) .map(on: DispatchQueue.global()) { (imageData: Data) -> OWSLinkPreviewDraft in // We always recompress images to Jpeg. - let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, jpegImageData: imageData) + let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, + title: title, + imageData: imageData, + imageMimeType: OWSMimeTypeImageJpeg) return linkPreviewDraft } .recover(on: DispatchQueue.global()) { (_) -> Promise in @@ -919,7 +933,7 @@ public class OWSLinkPreviewManager: NSObject { // tryToDownloadSticker will use locally saved data if possible. return StickerManager.tryToDownloadSticker(stickerPack: stickerPack, stickerInfo: coverInfo).map(on: DispatchQueue.global()) { (coverData) -> OWSLinkPreviewDraft in // Try to build thumbnail from cover webp. - var jpegImageData: Data? + var pngImageData: Data? if let stillImage = (coverData as NSData).stillForWebpData() { var stillThumbnail = stillImage let maxImageSize: CGFloat = 1024 @@ -933,8 +947,8 @@ public class OWSLinkPreviewManager: NSObject { } } - if let stillData = stillThumbnail.jpegData(compressionQuality: 0.85) { - jpegImageData = stillData + if let stillData = stillThumbnail.pngData() { + pngImageData = stillData } else { owsFailDebug("Could not encode as JPEG.") } @@ -942,7 +956,10 @@ public class OWSLinkPreviewManager: NSObject { owsFailDebug("Could not extract still.") } - return OWSLinkPreviewDraft(urlString: url.absoluteString, title: stickerPack.title, jpegImageData: jpegImageData) + return OWSLinkPreviewDraft(urlString: url.absoluteString, + title: stickerPack.title, + imageData: pngImageData, + imageMimeType: OWSMimeTypeImagePng) } } } From 679ed1a2f2fb7810fdae21825b90f0e6df6f6a15 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 11:21:10 -0400 Subject: [PATCH 19/26] Use PNG instead of JPEG for sticker pack link preview thumbnails, to preserve transparency. --- .../Messages/Attachments/OWSThumbnailService.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift index 9e481ca19e..65c2a85d53 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift +++ b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift @@ -166,8 +166,17 @@ private struct OWSThumbnailRequest { } else { throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.") } - guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { - throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") + let thumbnailData: Data + if attachment.contentType == OWSMimeTypeImageWebp { + guard let pngThumbnailData = thumbnailImage.pngData() else { + throw OWSThumbnailError.failure(description: "Could not convert thumbnail to PNG.") + } + thumbnailData = pngThumbnailData + } else { + guard let jpegThumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { + throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") + } + thumbnailData = jpegThumbnailData } do { try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomic) From 55d8c4622d02050d243f3b39ebe7059e4d2d4898 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 12:35:57 -0400 Subject: [PATCH 20/26] Respond to CR. --- .../src/Messages/Attachments/OWSThumbnailService.swift | 7 ++++++- .../src/Messages/Interactions/OWSLinkPreview.swift | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift index 65c2a85d53..925e5a468b 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift +++ b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift @@ -138,7 +138,7 @@ private struct OWSThumbnailRequest { guard canThumbnailAttachment(attachment: attachment) else { throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.") } - let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints) + var thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints) if FileManager.default.fileExists(atPath: thumbnailPath) { guard let image = UIImage(contentsOfFile: thumbnailPath) else { throw OWSThumbnailError.failure(description: "Could not load thumbnail.") @@ -172,6 +172,11 @@ private struct OWSThumbnailRequest { throw OWSThumbnailError.failure(description: "Could not convert thumbnail to PNG.") } thumbnailData = pngThumbnailData + + // The thumbnail path will end up like this ".jpg.png". + // This isn't ideal, but it will work. Since this path will never + // be user-facing, it's better to do the simplest correct thing. + thumbnailPath += ".png" } else { guard let jpegThumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 604af19cc4..9343252a25 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -959,7 +959,7 @@ public class OWSLinkPreviewManager: NSObject { return OWSLinkPreviewDraft(urlString: url.absoluteString, title: stickerPack.title, imageData: pngImageData, - imageMimeType: OWSMimeTypeImagePng) + imageMimeType: OWSMimeTypeImagePng) } } } From 514160a03f2c926cc36f58c16cc00a82700dc0bc Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 23 May 2019 12:29:14 -0400 Subject: [PATCH 21/26] less verbose logging since we now call this very rapidly --- .../ConversationView/ConversationViewController.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 41569b261a..26f432fadd 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3617,15 +3617,12 @@ typedef enum : NSUInteger { - (void)markVisibleMessagesAsRead { if (self.presentedViewController) { - OWSLogInfo(@"Not marking messages as read; another view is presented."); return; } if (OWSWindowManager.sharedManager.shouldShowCallView) { - OWSLogInfo(@"Not marking messages as read; call view is presented."); return; } if (self.navigationController.topViewController != self) { - OWSLogInfo(@"Not marking messages as read; another view is pushed."); return; } From 9acba0e23b40f23eea9e37fef1476ca6f33a6d63 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 11:55:03 -0400 Subject: [PATCH 22/26] Update color palette. --- Signal/src/views/LinkPreviewView.swift | 2 +- SignalMessaging/categories/UIColor+OWS.h | 5 ++++- SignalMessaging/categories/UIColor+OWS.m | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Signal/src/views/LinkPreviewView.swift b/Signal/src/views/LinkPreviewView.swift index 4b4fc6a64b..c431852948 100644 --- a/Signal/src/views/LinkPreviewView.swift +++ b/Signal/src/views/LinkPreviewView.swift @@ -843,7 +843,7 @@ public class LinkPreviewView: UIStackView { } if let sentBodyView = self.sentBodyView { let borderView = OWSBubbleShapeView(draw: ()) - let borderColor = UIColor(rgbHex: Theme.isDarkThemeEnabled ? 0x6b6d70 : 0xD5D6D6) + let borderColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray60 : UIColor.ows_gray15) borderView.strokeColor = borderColor borderView.strokeThickness = CGHairlineWidth() sentBodyView.addSubview(borderView) diff --git a/SignalMessaging/categories/UIColor+OWS.h b/SignalMessaging/categories/UIColor+OWS.h index 7aece5c18b..82a8f5116f 100644 --- a/SignalMessaging/categories/UIColor+OWS.h +++ b/SignalMessaging/categories/UIColor+OWS.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import @@ -40,10 +40,13 @@ NS_ASSUME_NONNULL_BEGIN @property (class, readonly, nonatomic) UIColor *ows_whiteColor; @property (class, readonly, nonatomic) UIColor *ows_gray02Color; @property (class, readonly, nonatomic) UIColor *ows_gray05Color; +@property (class, readonly, nonatomic) UIColor *ows_gray10Color; +@property (class, readonly, nonatomic) UIColor *ows_gray15Color; @property (class, readonly, nonatomic) UIColor *ows_gray25Color; @property (class, readonly, nonatomic) UIColor *ows_gray45Color; @property (class, readonly, nonatomic) UIColor *ows_gray60Color; @property (class, readonly, nonatomic) UIColor *ows_gray75Color; +@property (class, readonly, nonatomic) UIColor *ows_gray85Color; @property (class, readonly, nonatomic) UIColor *ows_gray90Color; @property (class, readonly, nonatomic) UIColor *ows_gray95Color; @property (class, readonly, nonatomic) UIColor *ows_blackColor; diff --git a/SignalMessaging/categories/UIColor+OWS.m b/SignalMessaging/categories/UIColor+OWS.m index 8dadbc7955..c9cb8caa1d 100644 --- a/SignalMessaging/categories/UIColor+OWS.m +++ b/SignalMessaging/categories/UIColor+OWS.m @@ -160,6 +160,16 @@ NS_ASSUME_NONNULL_BEGIN return [UIColor colorWithRGBHex:0xEEEFEF]; } ++ (UIColor *)ows_gray10Color +{ + return [UIColor colorWithRGBHex:0xE1E2E3]; +} + ++ (UIColor *)ows_gray15Color +{ + return [UIColor colorWithRGBHex:0xD5D6D6]; +} + + (UIColor *)ows_gray25Color { return [UIColor colorWithRGBHex:0xBBBDBE]; @@ -172,7 +182,7 @@ NS_ASSUME_NONNULL_BEGIN + (UIColor *)ows_gray60Color { - return [UIColor colorWithRGBHex:0x636467]; + return [UIColor colorWithRGBHex:0x6B6D70]; } + (UIColor *)ows_gray75Color @@ -180,6 +190,11 @@ NS_ASSUME_NONNULL_BEGIN return [UIColor colorWithRGBHex:0x3D3E44]; } ++ (UIColor *)ows_gray85Color +{ + return [UIColor colorWithRGBHex:0x23252A]; +} + + (UIColor *)ows_gray90Color { return [UIColor colorWithRGBHex:0x17191D]; From 06a291944ca8a1aaf452d8da4ba7423fc4fb8d86 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 12:45:35 -0400 Subject: [PATCH 23/26] "Bump build to 2.40.0.12." --- Signal/Signal-Info.plist | 2 +- SignalShareExtension/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index a21a55beb6..88ce1a9052 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -47,7 +47,7 @@ CFBundleVersion - 2.40.0.11 + 2.40.0.12 ITSAppUsesNonExemptEncryption LOGS_EMAIL diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index 2b4a70d6e6..7d1cc627bd 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.40.0 CFBundleVersion - 2.40.0.11 + 2.40.0.12 ITSAppUsesNonExemptEncryption NSAppTransportSecurity From 3d68bab1a519e3aa1564c162a5467adca08effb3 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Wed, 22 May 2019 18:24:39 -0700 Subject: [PATCH 24/26] Scroll to default position when opening a conversation in compose mode. --- .../ConversationView/ConversationViewController.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 26f432fadd..ab0b549713 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -1270,6 +1270,11 @@ typedef enum : NSUInteger { break; case ConversationViewActionCompose: [self popKeyBoard]; + // When we programmatically pop the keyboard here, + // the scroll position gets into a weird state and + // content is hidden behind the keyboard so we restore + // it to the default position. + [self scrollToDefaultPosition:YES]; break; case ConversationViewActionAudioCall: [self startAudioCall]; From d7ca55ce010689125429bb94dfd4d036fc3efab7 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 13:51:04 -0400 Subject: [PATCH 25/26] Ensure webp thumbnails use cache. --- .../Messages/Attachments/OWSThumbnailService.swift | 14 ++++++-------- .../src/Messages/Attachments/TSAttachmentStream.m | 5 ++++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift index 925e5a468b..ebc187a7e2 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift +++ b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift @@ -138,7 +138,10 @@ private struct OWSThumbnailRequest { guard canThumbnailAttachment(attachment: attachment) else { throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.") } - var thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints) + + let isWebp = attachment.contentType == OWSMimeTypeImageWebp + + let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints) if FileManager.default.fileExists(atPath: thumbnailPath) { guard let image = UIImage(contentsOfFile: thumbnailPath) else { throw OWSThumbnailError.failure(description: "Could not load thumbnail.") @@ -157,7 +160,7 @@ private struct OWSThumbnailRequest { } let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints) let thumbnailImage: UIImage - if attachment.contentType == OWSMimeTypeImageWebp { + if isWebp { thumbnailImage = try OWSMediaUtils.thumbnail(forWebpAtPath: originalFilePath, maxDimension: maxDimension) } else if attachment.isImage || attachment.isAnimated { thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension) @@ -167,16 +170,11 @@ private struct OWSThumbnailRequest { throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.") } let thumbnailData: Data - if attachment.contentType == OWSMimeTypeImageWebp { + if isWebp { guard let pngThumbnailData = thumbnailImage.pngData() else { throw OWSThumbnailError.failure(description: "Could not convert thumbnail to PNG.") } thumbnailData = pngThumbnailData - - // The thumbnail path will end up like this ".jpg.png". - // This isn't ideal, but it will work. Since this path will never - // be user-facing, it's better to do the simplest correct thing. - thumbnailPath += ".png" } else { guard let jpegThumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else { throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.") diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 33e584f002..3cec8e8bcf 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -345,7 +345,10 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); - (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints { - NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.jpg", (unsigned long)thumbnailDimensionPoints]; + BOOL isWebp = [self.contentType isEqualToString:OWSMimeTypeImageWebp]; + NSString *fileExtension = isWebp ? @"png" : @"jpg"; + NSString *filename = + [NSString stringWithFormat:@"thumbnail-%lu.%@", (unsigned long)thumbnailDimensionPoints, fileExtension]; return [self.thumbnailsDirPath stringByAppendingPathComponent:filename]; } From 0a234ae131b1d021a1ae6dbd7afa32794d289990 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 23 May 2019 13:57:37 -0400 Subject: [PATCH 26/26] Ensure webp thumbnails use cache. --- .../src/Messages/Attachments/OWSThumbnailService.swift | 6 ++++++ .../src/Messages/Attachments/TSAttachmentStream.m | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift index ebc187a7e2..ce2e022ebc 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift +++ b/SignalServiceKit/src/Messages/Attachments/OWSThumbnailService.swift @@ -189,4 +189,10 @@ private struct OWSThumbnailRequest { OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath) return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData) } + + @objc + public class func thumbnailFileExtension(forContentType contentType: String) -> String { + let isWebp = contentType == OWSMimeTypeImageWebp + return isWebp ? "png" : "jpg" + } } diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 3cec8e8bcf..23cd9201fc 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -345,8 +345,7 @@ typedef void (^OWSLoadedThumbnailSuccess)(OWSLoadedThumbnail *loadedThumbnail); - (NSString *)pathForThumbnailDimensionPoints:(NSUInteger)thumbnailDimensionPoints { - BOOL isWebp = [self.contentType isEqualToString:OWSMimeTypeImageWebp]; - NSString *fileExtension = isWebp ? @"png" : @"jpg"; + NSString *fileExtension = [OWSThumbnailService thumbnailFileExtensionForContentType:self.contentType]; NSString *filename = [NSString stringWithFormat:@"thumbnail-%lu.%@", (unsigned long)thumbnailDimensionPoints, fileExtension]; return [self.thumbnailsDirPath stringByAppendingPathComponent:filename];