From de6a43d6a22f604cf3fa8267389a53dcdfd56771 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Thu, 10 Feb 2022 11:49:35 -0800 Subject: [PATCH 01/32] Use decimal number format for number of media items in a photo album. --- .../Photos/PhotoCollectionPickerController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCollectionPickerController.swift b/Signal/src/ViewControllers/Photos/PhotoCollectionPickerController.swift index 5e9e998928..8b0729d466 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCollectionPickerController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCollectionPickerController.swift @@ -62,7 +62,11 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg self.contents = contents } - private let numberFormatter: NumberFormatter = NumberFormatter() + private lazy var numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter + }() private func buildTableCell(collection: PhotoCollection) -> UITableViewCell { let cell = OWSTableItem.newCell() From 6dfd13530fbb57b6953d07993548d0c5b11df23e Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Thu, 10 Feb 2022 11:50:59 -0800 Subject: [PATCH 02/32] Add CircleBlurView. Round view that has blur (configurable) background. --- SignalUI/Views/CircleView.swift | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/SignalUI/Views/CircleView.swift b/SignalUI/Views/CircleView.swift index 91f837d722..e4c2978ea5 100644 --- a/SignalUI/Views/CircleView.swift +++ b/SignalUI/Views/CircleView.swift @@ -43,6 +43,48 @@ public class CircleView: UIView { } } +@objc (OWSCircleBlurView) +public class CircleBlurView: UIVisualEffectView { + + @available(*, unavailable, message: "use other constructor instead.") + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + @objc + public override init(effect: UIVisualEffect?) { + super.init(effect: effect) + frame = .zero + } + + @objc + public required init(effect: UIVisualEffect, diameter: CGFloat) { + super.init(effect: effect) + frame = .zero + + autoSetDimensions(to: CGSize(square: diameter)) + } + + @objc + override public var frame: CGRect { + didSet { + updateRadius() + } + } + + @objc + override public var bounds: CGRect { + didSet { + updateRadius() + } + } + + private func updateRadius() { + layer.cornerRadius = bounds.size.height / 2 + clipsToBounds = true + } +} + @objc (OWSPillView) public class PillView: UIView { From 1c6fb5e8221a23b0cd41d5aaa0386b76f4567ee3 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Thu, 10 Feb 2022 11:51:50 -0800 Subject: [PATCH 03/32] Make SignalUI.PillView public. Will need to subclass it for camera UI. --- SignalUI/Views/CircleView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SignalUI/Views/CircleView.swift b/SignalUI/Views/CircleView.swift index e4c2978ea5..cd3f84f060 100644 --- a/SignalUI/Views/CircleView.swift +++ b/SignalUI/Views/CircleView.swift @@ -86,11 +86,13 @@ public class CircleBlurView: UIVisualEffectView { } @objc (OWSPillView) -public class PillView: UIView { +open class PillView: UIView { public override init(frame: CGRect) { super.init(frame: frame) + layer.masksToBounds = true + // Constrain to be a pill that is at least a circle, and maybe wider. autoPin(toAspectRatio: 1.0, relation: .greaterThanOrEqual) @@ -101,7 +103,7 @@ public class PillView: UIView { } } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } From 168de08f9e0a59bfbebf572f5a4e7dedbde444c6 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Thu, 10 Feb 2022 11:58:51 -0800 Subject: [PATCH 04/32] Use default background for photo library item picker (grid). That'll be white while in Light appearance and pure black in Dark. Also mark as "private" as many methods/vars as possble. --- .../Photos/ImagePickerController.swift | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index f3becd21ee..8e1d2c3af9 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -27,8 +27,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat private var photoCollectionContents: PhotoCollectionContents private let photoMediaSize = PhotoMediaSize() - var collectionViewFlowLayout: UICollectionViewFlowLayout - var titleView: TitleView! + private var collectionViewFlowLayout: UICollectionViewFlowLayout + private var titleView: TitleView! init() { collectionViewFlowLayout = type(of: self).buildLayout() @@ -89,26 +89,23 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let titleView = TitleView() titleView.delegate = self titleView.text = photoCollection.localizedTitle() - navigationItem.titleView = titleView self.titleView = titleView - collectionView.backgroundColor = .ows_gray95 - let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection)) selectionPanGesture.delegate = self self.selectionPanGesture = selectionPanGesture collectionView.addGestureRecognizer(selectionPanGesture) } - var selectionPanGesture: UIPanGestureRecognizer? - enum BatchSelectionGestureMode { + private var selectionPanGesture: UIPanGestureRecognizer? + private enum BatchSelectionGestureMode { case select, deselect } - var selectionPanGestureMode: BatchSelectionGestureMode = .select + private var selectionPanGestureMode: BatchSelectionGestureMode = .select @objc - func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) { + private func didPanSelection(_ selectionPanGesture: UIPanGestureRecognizer) { guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return @@ -126,6 +123,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat switch selectionPanGesture.state { case .possible: break + case .began: collectionView.isUserInteractionEnabled = false collectionView.isScrollEnabled = false @@ -140,6 +138,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } else { selectionPanGestureMode = .select } + case .changed: let velocity = selectionPanGesture.velocity(in: view) @@ -160,15 +159,17 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } tryToToggleBatchSelect(at: indexPath) + case .cancelled, .ended, .failed: collectionView.isUserInteractionEnabled = true collectionView.isScrollEnabled = true + @unknown default: owsFailDebug("unexpected selectionPanGesture.state: \(selectionPanGesture.state)") } } - func tryToToggleBatchSelect(at indexPath: IndexPath) { + private func tryToToggleBatchSelect(at indexPath: IndexPath) { guard let collectionView = collectionView else { owsFailDebug("collectionView was unexpectedly nil") return @@ -214,7 +215,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat updateLayout() } - var hasEverAppeared: Bool = false + private var hasEverAppeared: Bool = false override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -259,12 +260,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - var lastPageYOffset: CGFloat { + private var lastPageYOffset: CGFloat { let yOffset = collectionView.contentSize.height - collectionView.frame.height + collectionView.contentInset.bottom + view.safeAreaInsets.bottom return yOffset } - func scrollToBottom(animated: Bool) { + private func scrollToBottom(animated: Bool) { self.view.layoutIfNeeded() guard let collectionView = collectionView else { @@ -296,7 +297,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - public func reloadData() { + private func reloadData() { guard let collectionView = collectionView else { owsFailDebug("Missing collectionView.") return @@ -309,13 +310,13 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - Actions @objc - func didPressCancel() { + private func didPressCancel() { self.delegate?.imagePickerDidCancel(self) } // MARK: - Layout - static let kInterItemSpacing: CGFloat = 2 + private static let kInterItemSpacing: CGFloat = 2 private class func buildLayout() -> UICollectionViewFlowLayout { let layout = UICollectionViewFlowLayout() @@ -327,7 +328,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return layout } - func updateLayout() { + private func updateLayout() { let containerWidth = self.view.safeAreaLayoutGuide.layoutFrame.size.width let minItemWidth: CGFloat = 100 @@ -351,7 +352,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - Batch Selection - func isSelected(indexPath: IndexPath) -> Bool { + private func isSelected(indexPath: IndexPath) -> Bool { guard let selectedIndexPaths = collectionView.indexPathsForSelectedItems else { return false } @@ -386,14 +387,14 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - PhotoCollectionPicker Presentation - var isShowingCollectionPickerController: Bool = false + private var isShowingCollectionPickerController: Bool = false - lazy var collectionPickerController: PhotoCollectionPickerController = { + private lazy var collectionPickerController: PhotoCollectionPickerController = { return PhotoCollectionPickerController(library: library, collectionDelegate: self) }() - func showCollectionPicker() { + private func showCollectionPicker() { Logger.debug("") guard let collectionPickerView = collectionPickerController.view else { @@ -419,7 +420,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - func hideCollectionPicker() { + private func hideCollectionPicker() { Logger.debug("") assert(isShowingCollectionPickerController) @@ -466,10 +467,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat if delegate.imagePickerCanSelectMoreItems(self) { return true - } else { - delegate.imagePickerDidTryToSelectTooMany(self) - return false } + + delegate.imagePickerDidTryToSelectTooMany(self) + return false } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { @@ -489,8 +490,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - Logger.debug("") + override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { guard let delegate = delegate else { owsFailDebug("delegate was unexpectedly nil") return @@ -532,7 +532,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat photoGridViewCell.allowsMultipleSelection = collectionView.allowsMultipleSelection } - func updateVisibleCells() { + private func updateVisibleCells() { guard let delegate = delegate else { return } for cell in collectionView.visibleCells { guard let photoGridViewCell = cell as? PhotoGridViewCell else { @@ -561,11 +561,11 @@ extension ImagePickerGridController: UIGestureRecognizerDelegate { } } -protocol TitleViewDelegate: AnyObject { +private protocol TitleViewDelegate: AnyObject { func titleViewWasTapped(_ titleView: TitleView) } -class TitleView: UIView { +private class TitleView: UIView { // MARK: - Private @@ -606,7 +606,7 @@ class TitleView: UIView { weak var delegate: TitleViewDelegate? - public var text: String? { + var text: String? { get { return label.text } @@ -615,11 +615,11 @@ class TitleView: UIView { } } - public enum TitleViewRotationDirection { + enum TitleViewRotationDirection { case up, down } - public func rotateIcon(_ direction: TitleViewRotationDirection) { + func rotateIcon(_ direction: TitleViewRotationDirection) { switch direction { case .up: // *slightly* more than `pi` to ensure the chevron animates counter-clockwise @@ -639,7 +639,7 @@ class TitleView: UIView { } extension ImagePickerGridController: TitleViewDelegate { - func titleViewWasTapped(_ titleView: TitleView) { + fileprivate func titleViewWasTapped(_ titleView: TitleView) { if isShowingCollectionPickerController { hideCollectionPicker() } else { From 3d385c441996f77d226e97dc1e77f59738919d47 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Sun, 13 Feb 2022 08:31:39 -0800 Subject: [PATCH 05/32] Initial commit for redesigned in-app camera. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Work in progress: • iPad support needs work • navigation between screens isn't finished yet. --- Signal.xcodeproj/project.pbxproj | 10 +- .../ic_flash_mode_auto.imageset/Contents.json | 23 - .../flash-auto-28@1x.png | Bin 524 -> 0 bytes .../flash-auto-28@2x.png | Bin 995 -> 0 bytes .../flash-auto-28@3x.png | Bin 1629 -> 0 bytes .../ic_flash_mode_off.imageset/Contents.json | 23 - .../flash-outline-28@1x.png | Bin 493 -> 0 bytes .../flash-outline-28@2x.png | Bin 860 -> 0 bytes .../flash-outline-28@3x.png | Bin 1387 -> 0 bytes .../ic_flash_mode_on.imageset/Contents.json | 23 - .../flash-filled-28@1x.png | Bin 399 -> 0 bytes .../flash-filled-28@2x.png | Bin 750 -> 0 bytes .../flash-filled-28@3x.png | Bin 1065 -> 0 bytes .../ic_switch_camera.imageset/Contents.json | 23 - .../switch-camera-28@1x.png | Bin 425 -> 0 bytes .../switch-camera-28@2x.png | Bin 808 -> 0 bytes .../switch-camera-28@3x.png | Bin 1275 -> 0 bytes .../ic_x_with_shadow.imageset/Contents.json | 23 - .../ic_x_with_shadow.imageset/x-24@1x.png | Bin 243 -> 0 bytes .../ic_x_with_shadow.imageset/x-24@2x.png | Bin 398 -> 0 bytes .../ic_x_with_shadow.imageset/x-24@3x.png | Bin 573 -> 0 bytes .../Contents.json | 16 + .../media_composer_close.pdf | 229 ++++ .../Contents.json | 16 + .../create-album-outline-24.pdf | 121 ++ .../Contents.json | 16 + .../create-album-solid-24.pdf | Bin 0 -> 47980 bytes .../Contents.json | 16 + .../media-composer-flash-auto-24.pdf | 125 ++ .../Contents.json | 16 + .../media-composer-flash-filled-24.pdf | 93 ++ .../Contents.json | 16 + .../media-composer-flash-outline-24.pdf | 110 ++ .../Contents.json | 16 + .../media-composer-lock-outline-24.pdf | 121 ++ .../Contents.json | 16 + .../media-composer-navbar-chevron.pdf | 73 + .../Contents.json | 16 + .../switch-camera-24.pdf | 108 ++ .../Contents.json | 23 - .../navbar_disclosure_down_small@1x.png | Bin 1279 -> 0 bytes .../navbar_disclosure_down_small@2x.png | Bin 1433 -> 0 bytes .../navbar_disclosure_down_small@3x.png | Bin 1580 -> 0 bytes .../Photos/CameraCaptureControl.swift | 424 ++++++ .../Photos/ImagePickerController.swift | 67 +- .../Photos/MediaDoneButton.swift | 106 ++ .../ViewControllers/Photos/PhotoCapture.swift | 83 +- .../Photos/PhotoCaptureViewController.swift | 1169 +++++++---------- .../SendMediaNavigationController.swift | 421 +----- SignalUI/Views/CircleView.swift | 2 +- 50 files changed, 2266 insertions(+), 1278 deletions(-) delete mode 100644 Signal/Images.xcassets/ic_flash_mode_auto.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@1x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@2x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@3x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_off.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/ic_flash_mode_off.imageset/flash-outline-28@1x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_off.imageset/flash-outline-28@2x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_off.imageset/flash-outline-28@3x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_on.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@1x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@2x.png delete mode 100644 Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@3x.png delete mode 100644 Signal/Images.xcassets/ic_switch_camera.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@1x.png delete mode 100644 Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@2x.png delete mode 100644 Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@3x.png delete mode 100644 Signal/Images.xcassets/ic_x_with_shadow.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@1x.png delete mode 100644 Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@2x.png delete mode 100644 Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@3x.png create mode 100644 Signal/Images.xcassets/media-composer-close.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-close.imageset/media_composer_close.pdf create mode 100644 Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/create-album-outline-24.pdf create mode 100644 Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/create-album-solid-24.pdf create mode 100644 Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-flash-auto-24.imageset/media-composer-flash-auto-24.pdf create mode 100644 Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-flash-filled-24.imageset/media-composer-flash-filled-24.pdf create mode 100644 Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-flash-outline-24.imageset/media-composer-flash-outline-24.pdf create mode 100644 Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-lock-outline-24.imageset/media-composer-lock-outline-24.pdf create mode 100644 Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-navbar-chevron.imageset/media-composer-navbar-chevron.pdf create mode 100644 Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-switch-camera-24.imageset/switch-camera-24.pdf delete mode 100644 Signal/Images.xcassets/navbar_disclosure_down.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@1x.png delete mode 100644 Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@2x.png delete mode 100644 Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@3x.png create mode 100644 Signal/src/ViewControllers/Photos/CameraCaptureControl.swift create mode 100644 Signal/src/ViewControllers/Photos/MediaDoneButton.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 31d839843b..83f57363b7 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -791,8 +791,10 @@ 641CECC436F5F3EE2AC07EE9 /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6657FDE7B91C2845BB3BEAB5 /* Pods_SignalShareExtension.framework */; }; 760D93AB27A0E28600F351AC /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 760D93AA27A0E28600F351AC /* CoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; + 76B90A0327B5B9220013D510 /* MediaDoneButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B90A0227B5B9220013D510 /* MediaDoneButton.swift */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; 76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; }; + 76FCCDBC27AB8FBE00BAA7F0 /* CameraCaptureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCCDBB27AB8FBE00BAA7F0 /* CameraCaptureControl.swift */; }; 8806EF19248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */; }; 8806EF1B248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806EF1A248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift */; }; 8809CE8722F8FE6D00D38867 /* AttachmentKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8809CE8622F8FE6D00D38867 /* AttachmentKeyboard.swift */; }; @@ -1986,9 +1988,11 @@ 748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 760D93AA27A0E28600F351AC /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 768A1A2A17FC9CD300E00ED8 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; + 76B90A0227B5B9220013D510 /* MediaDoneButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDoneButton.swift; sourceTree = ""; }; 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 = ""; }; + 76FCCDBB27AB8FBE00BAA7F0 /* CameraCaptureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureControl.swift; sourceTree = ""; }; 7856A9F703AAD99E22B75A9B /* Pods-SignalShareExtension.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.profiling.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.profiling.xcconfig"; sourceTree = ""; }; 7BB1CB6F2D7841356BE367EA /* Pods-Signal.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.testable release.xcconfig"; sourceTree = ""; }; 7C5EABE2C09180BC71C4E097 /* Pods-SignalShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.app store release.xcconfig"; sourceTree = ""; }; @@ -2977,14 +2981,16 @@ isa = PBXGroup; children = ( 32C584A725B81C6600256804 /* AvatarViewController.swift */, + 76FCCDBB27AB8FBE00BAA7F0 /* CameraCaptureControl.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */, 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */, + E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */, 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */, 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */, 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */, 4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */, 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, - E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */, + 76B90A0227B5B9220013D510 /* MediaDoneButton.swift */, ); path = Photos; sourceTree = ""; @@ -6317,6 +6323,7 @@ 342FFE6B271EF502000AC89F /* OWSWindowManager.m in Sources */, 347C382E252CE69400F3D941 /* CVComponentState.swift in Sources */, 342FFE6A271EF502000AC89F /* ConversationSearch.swift in Sources */, + 76B90A0327B5B9220013D510 /* MediaDoneButton.swift in Sources */, 340D900024FEE6A9007B5504 /* GroupInviteLinksUI.swift in Sources */, 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */, 887B381325F0681400685845 /* AdvancedPrivacySettingsViewController.swift in Sources */, @@ -6359,6 +6366,7 @@ 34ACA7DE2733159600E47AD4 /* OnboardingPhoneNumberDiscoverabilityViewController.swift in Sources */, 88A9729222FA5D4B004B4FBF /* AttachmentFormatPickerView.swift in Sources */, 346C19E125ACE9AE00061D3A /* MediaDownloadSettingsViewController.swift in Sources */, + 76FCCDBC27AB8FBE00BAA7F0 /* CameraCaptureControl.swift in Sources */, 8827004E23208A1900F01C46 /* AppearanceSettingsTableViewController.swift in Sources */, 344A761324B36C8C009D69A5 /* TestingViewController.swift in Sources */, 3495FF0D25F934C500959D6E /* PaymentsRestoreWalletCompleteViewController.swift in Sources */, diff --git a/Signal/Images.xcassets/ic_flash_mode_auto.imageset/Contents.json b/Signal/Images.xcassets/ic_flash_mode_auto.imageset/Contents.json deleted file mode 100644 index cb322d88de..0000000000 --- a/Signal/Images.xcassets/ic_flash_mode_auto.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "flash-auto-28@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "flash-auto-28@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "flash-auto-28@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@1x.png b/Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@1x.png deleted file mode 100644 index 40e81ed9855cc71c7b86cce1f7eb273daacacd54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmV+n0`vWeP)T0eGXbbIrgkCX@=?L~c~p)DBEG zp$0x{@ZQNg4~qp37L^;ClY;w`nTMI~5Ln-nbmmRJ%R}S9J=wk$V6}BP?E-xu0JaX? zZ7gVtbsHx2-^c8N>xqaiinp&7IFmgZov*b1U3{52ev3pZ92W+`bLad5_{2CWT+upP zl;JXfPl}U@99tzR_y9QSqJ+2Ep;?j4syWT79h?ojMGW4*<^ncs$@4ifZLrio{v>S zGdsA0gV_z#sK?{&h^$Sx%YobJ{q1s(*}{Z&niSvk#>G$ln&3|MGI=Bu(Iv^cz z9i)Rm2T*}BzIYi$lFcqi-VB=2%pu$Bd#jJ9^)3<-JhHr-5*s4UmC;!y1Bi%Q0QilF z3pMB{lK@1-4FG)3Klf^aqcjJIh#dfYWu{+hlA|;Qm~(C?%#Q;wgL@LT#{ig%%Ci~> z2H1oL5pe|o-_-yB5D_Y)ql9}f)4&Teo!Q^7YJsza0z|}_$h)%$$xw=b1|Cd#_ksOR z{KttsH2)wHU?%fV%=DT8@hTb)FeSOW0>Cm|n&|-mQn;>}?G&-MRCf<%GT#dGl88YC@M+55K#|?=U;e%c@xX6w`%qr56x|VtsJ6!tZNaX%5Vp`l>m^uHe=(R z7@t|!`7d)YlR58H&MfbG;rVwIV_|YPK$dr0Rz!3Vwx;~oi-!kyg3P}Wkpv7=RM0oE z;&w#dZA4z}3-jhv6qvh;Oo_|~&h^ri$8&4XyNb*eGPfk^VCgAW@1l$bIDpJ8=?t5f z)EfrNtu?KYd3ZkQy*Jjpn{)6QnFoWD1#RbAWO5$y4X!4*Be=l@O|7+zYSQ06@*p-@ zT5|)y&GXIVH+EsowbV2ko9xHNz7x1vP6NL8%9>l!;sD!3#{QH=W%S<4AU2Rs0G?`g zJ#24P(5!ja^coLN!e6DQ6E$&&)b4bQK~si6fV^0l zCu&APtLEtteear8b*UaWA>vZJH$3vZ%MHHOSf`tyu6L1!9z2x7u8`{sF-LlLP|Kza zLEwrfYD39fEu6nSAOo*1+O_)i5Lan#rgzd=1Peu7Z$xGc9iho?GG8%yS*!Y&Kf zwHpV^7+7FVM!Bz&x$&UQ3lygwRe7lq(aZc>v){)7O*CJh@^pcC`EKFF4xKyWq_2qw zWjGEh?`*xKk&>E^Q_2tku=Ed^A!V$WPZl**6cZYvl`jc@5Rnciy?!pN)Oh^+!ECw7{Uo(@nJMTxnYgWEvY_YW#Pb| za}#lq&~Mcc@x{+t3FwD{UERl<>B>AQb4z)1xF{s7-V8wOn& RY777X002ovPDHLkV1oRZzt;c& diff --git a/Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@3x.png b/Signal/Images.xcassets/ic_flash_mode_auto.imageset/flash-auto-28@3x.png deleted file mode 100644 index 38b6ffb80f5cc683bb5e7dc0203adf9f69f9e228..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1629 zcmV-j2BP_iP)}4G|5FV_6hdHCSqQVR&^iba(ONZ}@m19ngnL3If+N3q z!A{i_#2hSi(}?H@Zu}>CCzP2#5z!y2;To<8bsLoA6!b+kT%!shxDoZ_)j~zQZ&iba zE9)f3TOI`=6wO@)B=SgTLcAmLP+d{DB2>itpQ?fAg@~4_*5Q_S6O=@}@4!|+5>efD zh}w=J1VnOCxUkR`@%~<{Gj4Ndo#bfCZw>MOR|ugd4keQBgd&J|e+QagLkO4i`%1Ms z3`kVkhca`$o&Q%2MBi(x3?ZDP-yybOPZ_2ridm?L_l;^Gs_G1UgUkhR2jA5wCn!Q_ zOAvYzzj*=L?k@c&6|~T`A>RKDA;d?{*#smiL1;p}e+?l#tJ;}a=%hhO#QQhZaQ2(a zi{FgK%qAewlos0Xn+T5vnM)9mP?E$Arrh#v;vHdD+nuH&w4-=Om>r?DKN_{jSv-JJ z6SGMD-*Os?$F5LcFJ;uABtCEt^kyY~Gihzie2T|<4oEcSmd_OL3bGhovi80()>e^Y zCEDt1)E|t00%NBxyEeC^=8Y-`Ak!jahx(%;rz!LqQZ}b znL*~l*e-Q>kH$D6(~@O3xhPeUIjNh(dqFZy7lryyLg*0Uy%3qJgo|3DF{2YIlR{*k zW)Ord7xjq7)JMF_-BUE?s-Uq}@&0;uymgPr2PVWj zwTO3gnfDcZKI#YImQdqMSiZ~>?;l$HU+kkRc{UaU%>}}}z{s z856aW9ZdX@f*MI?~$rA4d;{W4UO3=vOs|7&t zN+_EAPpgs4yY*TQiqHzi%Mh_u#yM9sCxtm#=wAFLdW><*yxE7yiLEjw_^pv!xWZfr zt>g<@+c`J$-i6R!+uq}Ej-Z!iw<5UF&AvAAjt~=b1Pg*bv(-X9W}A=|)KU9yRohLx zU%~L72SXNBk&-}tu3;VW3noO+wMdjC>2%1+hfB<%uQw{K3Y1 zB(#fwz9sXXB;?ld-hQ_Uy_*yxN4&?Mk4|8@NG`aypp=|!j(9hKtn1at30x4L${y;4 zvD}R}LeUq#AXmI2pat6+N6nvL<}I-5M@tiX^P%VqUogbA8MI&~nTdAvIcK2IUr;hx z(xvyE_m`gp{<$st`I-j z>(%fO0-76yCm$MLl^o@|;n?s*=%(>->?mZ8dTnM5xVkJRerc7GG2U_ii(dod{ujT7 b1QGoJTuvej@h3Ab00000NkvXXu0mjfb+7?w diff --git a/Signal/Images.xcassets/ic_flash_mode_off.imageset/Contents.json b/Signal/Images.xcassets/ic_flash_mode_off.imageset/Contents.json deleted file mode 100644 index 140ab9904a..0000000000 --- a/Signal/Images.xcassets/ic_flash_mode_off.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "flash-outline-28@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "flash-outline-28@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "flash-outline-28@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/ic_flash_mode_off.imageset/flash-outline-28@1x.png b/Signal/Images.xcassets/ic_flash_mode_off.imageset/flash-outline-28@1x.png deleted file mode 100644 index a132c89e203094397e07139faeb64e9358c921e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 493 zcmVd0MGh97;7YQhBW|ikg$a6cu!dZfKC%#^?51zmaw{|GLd?2sipPp1n_7y zj?9MSn0ZtuES5g$`+r5Z7HV>NX+8f4p-~s#>*7N2o$AId1GAV=D##|XRo!muz=R2P z;ZYagXZkLu#RANxqMb$*&&b_) zil!0syWZ9J(Xf|Z218vZprmR=<@>L8^DM0bR;5vNm4>-Z9b769vxk-uvh3`KP=biKQFV7wDH$peP$7bS-Mth4 z_xdM=J+%HHk|2xu2VpufA=yR43HC+qJ^*0bKU$140QBklN!XqveCy#JK^F6aGH;3K z830;gi;tdniW*I&$44R>$jJH&Z!j;g!a8?w=*{$x2!%u9$SIK27!5h>;+ m)R2j$c3S@DKbqjG0DwQ3q|I<6u&@pQ00005MYT{B?@y+|B9wJf zV3$Wp2xaq735g;S>WFtl9;&N~P=u;@|70479*JmfY6H4_n53lQeGOcFM?_uQHfh_5 z2qL*ELRjb-@qVk;nYM+qP737mPZ{z4RYW@C&_?o=Py`k4A3)Q;B63{6FHCE|ghaJt zC^L8N{M|GV{qC$%ME2Tmn_6(B9i~%MvrrZ9E7L%9=yu>w$ee+2@Y{?Ef+~cbNkVrT zH;+K>9x{GXMGIYK#QQf9Ntc|>BqXXqs3YEgiO9XFZOlR^O-d@>|1%BGadUo*o5`4F z5)w^mp(k+@;U+`o93&*PNfHKAy8Jouj?k35(^Q1^6z>Sl5!%J0QH!p{1(c4MnfHI| zYA7AMKz*Z>QInF!zzxuw2aTJqwK4NP9UD0z(U>kjP`n%HkI`q>J{HDu67WU!FHnZBfkCGN zMG3t`ygNWLs|MUypW>~lc^xhh?*{tAJer2_(EZR4APN2u5wyjaM=lmz5I4ICq4>5! z{i75OEgLZJ?a+i&EGfN4yw@z<+|)YLdG9Y>K$waP$efp+)Kn$25);+Qm+^rr2aq|- zxJy@V(U?GF=49EcLX@c(IH^~O_ljg@Llo*i385v#dnGa#PKcVLF{2ZzlR{+f8%RRA z5Ossb3@w9&y(8Ycqus?o@jf+jhl7tIQsm;q;O#qRUV~N4K|{cl%f;o8ACH;41Hivc z!?SoFvLyCVC~i(((EM`62~ih`cdra}`6$;1d+&>Ch(hjsP-#~*L=7c0N_-r$VUgS9 zz8|u`&>Nol0+*jTtBWa1ua(SL;@vB5V-Tr>e!#POaZK^!J7;H@Vj6lLsFc;X!`NQ$CGLbDdC;yrh>@4?iL zfY33t`#=k@wm*>DIyd`vQ!a_46PivAzD?4&nZ9E>;;&KkW(HwhKJ!hc95FfZ{&`9= zDR>O+6+fAI2{vm;Z}ts4u`Gbr2z`;^^;ymr)9ZOF5nV4I9nD&(CLv^jqilNgMbr-v z%s_VEC2yCiiFXfCEfiVjsk>Jd?;fHOn$7SL#Jh)+3IqM7|-aSMk6vfXY!iaYdEm>&DB$Oi5 z5ePW)IO20Oy_rK83(cItQ$+FZA(G1@LPLso50MB>Pcy@ccMp*WT`7rl%;-_YyN75= tX{MJFLVhT)53Yalt1zyA@v9_==zm5S(y-*@Y3Kj|002ovPDHLkV1j;>ch>*_ diff --git a/Signal/Images.xcassets/ic_flash_mode_on.imageset/Contents.json b/Signal/Images.xcassets/ic_flash_mode_on.imageset/Contents.json deleted file mode 100644 index 54632ce264..0000000000 --- a/Signal/Images.xcassets/ic_flash_mode_on.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "flash-filled-28@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "flash-filled-28@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "flash-filled-28@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@1x.png b/Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@1x.png deleted file mode 100644 index 55911ca8f1bbaea062b29b734be26a3275aa2aab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 399 zcmV;A0dW3_P)baI0qvmIJQMoiDVV1v7z_d+Ka}T$r7~8nYH?!Mu-F{3>8!GdNlC zse2A!>xBOTK2)orX8>=0DLLmB6=|pkm|mKE-mWStCu^Qdu36kA7dwrDiLV%HWB!k# tdF1aqY=O;c1^N^I4y!GaRNG$lfgcoIw6cCu(Z&D(002ovPDHLkV1myMr^WyP diff --git a/Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@2x.png b/Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@2x.png deleted file mode 100644 index cf134711fc8059df621832817550891916609836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 750 zcmVfKj}nQ_g$m{S`OZAH?V*#r{N5x{pM zdJuz-%p#D8_5j}IKX1eb9BD2vFUyae)V=tSBTWSo(Hg+9m%gecn#8^A<*q{@5giKq zEmU9=A0(n3fX`w805g+FIuc$%BH94B6$5_=Z7_%yNJOi`zI*0QWSohvVA^-DMfNYD z`wx)&}cv~=Y zjV1!!_Mm0v1zs`pFMY@l7lGN#e~5uHCuUw3M;)2GL_7Pgh9(ULPJ8e;VCEYChrn#+ zZ^XbbO&ZLZvla9;L3vYazi7+Y zYV&ioiK#KcW(LjNik1&wE?HvVn0YU<6|@w%5gD7)(24XImMZYvR6Oq4G!dAuKx(e~ zKQOb)Re!4r*4WJ#W_G*QY$b5)^8A^qC$bY*QU%@iU6&Ee^^`p#Gjog3E}CCgzR6~8`C;4~f(5SfU1<9W gP$1X+Zv_B;13Zs)#9=2nEC2ui07*qoM6N<$f)D3RasU7T diff --git a/Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@3x.png b/Signal/Images.xcassets/ic_flash_mode_on.imageset/flash-filled-28@3x.png deleted file mode 100644 index 48056f90a733488daadbe72e2e7e774448683276..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1065 zcmV+^1lIeBP)L%ZdV_j{q!ZK|)Y%{#G~FN@)SaO2 z1W6`{vw^)qJpp=5>8oPUuqct@qwMdFe@NejdIthel&G+X(1mooyJ(q~Y6WHH2{T`- zfx=WNs26fWL=ViocH*iZ6{0#pnR!Y?Dpv=7V z5qZd3^%SBS+r-RnS^rQ2L?j}tItx)ngfjDti0;+EKb}-iLEr`T6+DzT_dKeeg47V9 zE^mHRz5jv9pM)+bGv5)>7d7w}C_!P1k~i-UYTzw95rHxFgc_j@^L`4b=p^7JZxx{? zPz8w)1vSij2slBZa;RnAkMKc6U~W4JNlm zWd5Xr5qckC-a}jj-Q&&sS2Yj^te}GyC2!uJ)j%Yu1&M}?&_>?u3)CZzLxx=PUCet3 z*yT<`3EIQFhd6IRy?Otp2D$?+Xx5_S&HK8C-&7zMB+40~3Cw#4=z{c$i_jd+-&RWH z&27L;V)V%pp^3~p0#~tb_9Yk8oA;Q<80gGm=9g5^gytQIxP8}99_ZTqAFn-cHrpyz z3)Rs>OlRIhld83CVhd6-L5XNSGBMqw=~?JnvkOW@Q$K+*3AzYfYj#21lJIvzp-I)+ zmZ57MO3*Qw_hws%u5%DUz2OZ_s@CSx)--gjF|E_)NUEK8Fg}Y&?`Qqd#(t#c{d)>A zU+J*a&)&RS%^VDfIkvpn$-G-FcFDuNKG=huyCF&aerc?rP%pRkNBQ7+ypqYL7E&z7+_Xa)LX*>9J8Gi9kO|mpYQ>ImM(3&U&U4 zG@W@jI2S>Gi^!eo%>cTfDZYBM9rBwb&_yXhL2^atI*Z>t0&IjP`X*Bz^KO8=IVoWJx+-%a+`Mx)ClbwozA=$eVTwOXqO8A zG?aO_7)yk1{o52L6+PqsJMrcg_EMnd*5Cv_!+>4oai&`VJ zsCl=jHA0J;cMB*%{Xwx)=bDO}cMG%#Jr$i0=G_8KP%D4Jn0JesA{4R+9fJ1lyJM5N zLGzo(fQeA!89bbMw?JC*AweO{y9JVfKVFHHq00000NkvXXu0mjfHU#TV diff --git a/Signal/Images.xcassets/ic_switch_camera.imageset/Contents.json b/Signal/Images.xcassets/ic_switch_camera.imageset/Contents.json deleted file mode 100644 index c35436bc5b..0000000000 --- a/Signal/Images.xcassets/ic_switch_camera.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "switch-camera-28@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "switch-camera-28@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "switch-camera-28@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@1x.png b/Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@1x.png deleted file mode 100644 index a22cad61e2c1dc44d722a06da1a413638f193bb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmV;a0apHrP)KR?%_7FbsX_b%1t)G6ExH1V+dR8G#Wp0&Ku28KF#o zH&9Ph4#Jib5<>b@-H9CXvz}#HmWi3Mro64n_7{ZjnWZ2^^Z{T8V3Q!e0i2jQ_%joc z91f`x90WR|g4qoDy882s{UFd75{aY*P$k;xSj07+uTer1RTy((78i6*?O}?9CNgob z^)Q|aRwDMsiiDc8$z%^NDl^=+a&#N#=-o}DNB;ug%*^TWUqsTvuk?F*);ST?M8rhY zX&}AS06G9--0OB$);>8;zjJNf_I{v$qA?P-2E6l}mei1iMEyU6mlXg%t)}pd TNZj5O00000NkvXXu0mjf$`ZJt diff --git a/Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@2x.png b/Signal/Images.xcassets/ic_switch_camera.imageset/switch-camera-28@2x.png deleted file mode 100644 index 0fc0d562fb9d698681a5c3d0309f6da59ff3d473..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 808 zcmV+@1K0eCP)7@;H34R{+| zH+UPQO3$aJjtznQ93U)Ng5V_nVkfZ^&=>=Irk7o#I{@y04uD&y-2?pNl_VfUJOMxh z05t&E=QpL*PXHJIpa+0=W6aHyh$XuZreXkSNnx{)ZQVAhPEz6QM()6F5x_X#QUQ&M zWXKge0Ql>DyweA{pS8??@9W)#>xueplSc3L zQbq(O$uAf<$=w&V?G-Ep*Uhn5phd5|4@ovDKpNd7BRg|27lE4vkoGe^=dyuw`mLq% z902>$;F1iGROku$^KP-rl;)s)5E1RF_DZidygD0WqeYN?C6Gi#V5ly9J z8!mhL0Qs^g!~rUuWZ(3ael_Ms5-Se^z($hGZ9zppo@wy+A0wq`q%gRFyvZ%ox$2=kUMWgA`+DJ&gyU79B ziUn!z4errNNbEDM-31X9?BVk3eS@fw=UKtd;FYpyP+w mtvRCzI_(=E0DJ)%Rz4?&J=l`~0000RC&pvp=>mGydT1v17g0bl_DD;9XIJpdGlcwiv|YVfHi0bCQnkL-bZdL+OP zLjxWJs4*r3fSlYr_CPm11HheihI%W^#T9~`W_z1Ny^CC@9wfe+FL~2u@Fs3loe~`gHT(l&O)$7ppg|##fudR(Q^v&V-t| z%X#f`CCDPtRzfwCJWw!}&ctB`?E>n#gOO;u>D-U;6zm{nJYkq3pEA&eW54Apk`HL0 zV7X+>h+#@Qr9nRb)5Xhvi!+iBsHb34gjvvr9_uS;HYmlpZpo!gd{B1|pqXcLZfY>G zbgWVW>f>{1$(77}k(xAxDZ$uWk8G0i594puapo#~@9DRj&SXW9X zxHu9x8DjMn5$~AqMAdZxD7a!zt1By7ZMk)r(rM^MTRXKDm@QOe%!=moXW0S&tt~D~ z+dGYbre9=~WQ;iq*L>bLxt^1*CX#5|qYCq>>0PG;^r8*(Ih`kMq}1e3wbS+wzgPgk z+otEVQVl&xI#+Pt=Fr>hff{2rjxn}$ek-V11Eq1N1nzs=i?_4=hI(dzFErI`b;fnh zHFPJygsumMX9<*Uf_z~C06)9V{-QPc*w5NBD96fE zE2F@S0u<8^2)L(`VT&4u;Q+tLc!=3a_VE*!jpNpxB;QY53VKP<;U_M6^j!vta)jF2 z_y3xzuD78Z^SCjvz5fU+{i;n_>1Tq<8r||;`rT#Hm1e&MqOAl}SqZ4J5>RC&pvp=> lm6d=hD*;tj0;(wh_zM_7Ce=uu`eXnA002ovPDHLkV1hf-OJx86 diff --git a/Signal/Images.xcassets/ic_x_with_shadow.imageset/Contents.json b/Signal/Images.xcassets/ic_x_with_shadow.imageset/Contents.json deleted file mode 100644 index 1f25af162c..0000000000 --- a/Signal/Images.xcassets/ic_x_with_shadow.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "x-24@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "x-24@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "x-24@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@1x.png b/Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@1x.png deleted file mode 100644 index ee6d0b887443c3733f4bf3dc008a1adce365639a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GGLLkg|>2BR0px{zZ z7sn8f&bOhtTn!35wXa#^ZYq~4CkHIZJIgEcH&fxt=UR_fi>^r-z4j>muhP(=B`A6@ zMkG5zJ7N#}d4>Zlbry_M>dr}>*=zbIi2c0g0`*-^VY4WGsShchAuJR{iW<~ m>yER_Cs;lH?|)y9K|JM*R^O4Q$E1M{X7F_Nb6Mw<&;$VBSYMt1 diff --git a/Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@2x.png b/Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@2x.png deleted file mode 100644 index ea502285caf8531820a4cffebcb866af3b2bc408..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 398 zcmV;90df9`P)bfi!V8w;*bPFyWC? z5LS4a@X95KOM1<-BQz4(0Pjyx5J(e{|41VdCQu`pAP^>?!>~qrny{|9Xltai361lc z#Wm8|1hkG41kePe2n@fZ6psG(6RI>m#!qZC{{F*g=!qfnTLRL&#bj$Gc`3=U%j$ibvB^93G-~AMAJcc sIWaMN0f1J`aZ=wK!$5Bg16@HxZh($ diff --git a/Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@3x.png b/Signal/Images.xcassets/ic_x_with_shadow.imageset/x-24@3x.png deleted file mode 100644 index cbb9bb263d56a3b51b69b48ace2bb48ad7eb0b3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 573 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ<{->y95KI&fr0V8 zr;B4q#hkZu8aodg2(X05J6in|3!3Hasj2z!^2zcA?g#mgso%Y+c4=;c__PV4ZqF49 zRlNO9SvF_AUVYPPud72F#|Z&ONhJkmmXwYL9>EEUOhzS~0bZG&ns;5C1ExlKpPjx+ zJGj@F?Sn=;;}6CH>*_xl)1|IQot}OC<>$4ny7>>DNGiO2@I-rp%LSu`)a!v69=sO+ zDr#cjvmIQp>6ZL{q^VZy^hrgSzU7eubkbjY5&$a7ew{4&Q(T=h@ zmC8JoaiyBL#fK=182>N(ejdpvYGyt^^=k8kn1>DC=eONqkln*Kb=&giM%%d;Ocu`! z_c+WbSrFHoJxPAatp^>|mP>V6MuwibE!$Oj*vBw`r`9vy>Z38=0?)KMAC0LB zJ9C!1Yv*5AL-PR5XJR%-?;Ke<<936nd)sC1NgPr082$J?lrm=WZ9_y@ diff --git a/Signal/Images.xcassets/media-composer-close.imageset/Contents.json b/Signal/Images.xcassets/media-composer-close.imageset/Contents.json new file mode 100644 index 0000000000..4c94f4f150 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-close.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media_composer_close.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-close.imageset/media_composer_close.pdf b/Signal/Images.xcassets/media-composer-close.imageset/media_composer_close.pdf new file mode 100644 index 0000000000..122e40ab70 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-close.imageset/media_composer_close.pdf @@ -0,0 +1,229 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.469971 4.469727 cm +0.000000 0.000000 0.000000 scn +15.060001 14.000058 m +14.000000 15.060059 l +7.530000 8.591059 l +1.060000 15.060059 l +0.000000 14.000058 l +6.469000 7.530058 l +0.000000 1.060059 l +1.060000 0.000057 l +7.530000 6.469059 l +14.000000 0.000057 l +15.060001 1.060059 l +8.591000 7.530058 l +15.060001 14.000058 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 4.469971 3.409180 cm +0.000000 0.000000 0.000000 scn +15.060001 15.060605 m +15.325187 14.795461 l +15.590311 15.060626 l +15.325167 15.325770 l +15.060001 15.060605 l +h +14.000000 16.120605 m +14.265165 16.385771 l +14.000020 16.650915 l +13.734856 16.385792 l +14.000000 16.120605 l +h +7.530000 9.651606 m +7.264856 9.386419 l +7.530000 9.121316 l +7.795145 9.386419 l +7.530000 9.651606 l +h +1.060000 16.120605 m +1.325145 16.385792 l +1.059980 16.650915 l +0.794835 16.385771 l +1.060000 16.120605 l +h +0.000000 15.060605 m +-0.265165 15.325770 l +-0.530310 15.060625 l +-0.265186 14.795461 l +0.000000 15.060605 l +h +6.469000 8.590605 m +6.734186 8.325460 l +6.999290 8.590605 l +6.734186 8.855749 l +6.469000 8.590605 l +h +0.000000 2.120605 m +-0.265186 2.385750 l +-0.530309 2.120585 l +-0.265165 1.855440 l +0.000000 2.120605 l +h +1.060000 1.060604 m +0.794835 0.795439 l +1.059980 0.530294 l +1.325145 0.795419 l +1.060000 1.060604 l +h +7.530000 7.529606 m +7.795145 7.794791 l +7.530000 8.059896 l +7.264855 7.794791 l +7.530000 7.529606 l +h +14.000000 1.060604 m +13.734856 0.795419 l +14.000020 0.530294 l +14.265165 0.795439 l +14.000000 1.060604 l +h +15.060001 2.120605 m +15.325167 1.855440 l +15.590311 2.120585 l +15.325187 2.385750 l +15.060001 2.120605 l +h +8.591000 8.590605 m +8.325814 8.855750 l +8.060710 8.590605 l +8.325814 8.325460 l +8.591000 8.590605 l +h +15.325167 15.325770 m +14.265165 16.385771 l +13.734835 15.855440 l +14.794836 14.795440 l +15.325167 15.325770 l +h +13.734856 16.385792 m +7.264856 9.916790 l +7.795145 9.386419 l +14.265144 15.855420 l +13.734856 16.385792 l +h +7.795145 9.916790 m +1.325145 16.385792 l +0.794856 15.855420 l +7.264856 9.386419 l +7.795145 9.916790 l +h +0.794835 16.385771 m +-0.265165 15.325770 l +0.265165 14.795440 l +1.325166 15.855440 l +0.794835 16.385771 l +h +-0.265186 14.795461 m +6.203815 8.325460 l +6.734186 8.855749 l +0.265186 15.325749 l +-0.265186 14.795461 l +h +6.203815 8.855749 m +-0.265186 2.385750 l +0.265186 1.855461 l +6.734186 8.325460 l +6.203815 8.855749 l +h +-0.265165 1.855440 m +0.794835 0.795439 l +1.325166 1.325769 l +0.265165 2.385771 l +-0.265165 1.855440 l +h +1.325145 0.795419 m +7.795145 7.264421 l +7.264855 7.794791 l +0.794856 1.325789 l +1.325145 0.795419 l +h +7.264855 7.264421 m +13.734856 0.795419 l +14.265144 1.325789 l +7.795145 7.794791 l +7.264855 7.264421 l +h +14.265165 0.795439 m +15.325167 1.855440 l +14.794836 2.385771 l +13.734835 1.325769 l +14.265165 0.795439 l +h +15.325187 2.385750 m +8.856185 8.855750 l +8.325814 8.325460 l +14.794816 1.855461 l +15.325187 2.385750 l +h +8.856185 8.325460 m +15.325187 14.795461 l +14.794816 15.325749 l +8.325814 8.855750 l +8.856185 8.325460 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 3034 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003124 00000 n +0000003147 00000 n +0000003320 00000 n +0000003394 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3453 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json new file mode 100644 index 0000000000..8899a572d9 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "create-album-outline-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/create-album-outline-24.pdf b/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/create-album-outline-24.pdf new file mode 100644 index 0000000000..146295452b --- /dev/null +++ b/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/create-album-outline-24.pdf @@ -0,0 +1,121 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.000000 2.000000 cm +0.000000 0.000000 0.000000 scn +16.584000 1.500000 m +16.322899 1.045122 15.946626 0.667013 15.493025 0.403700 c +15.039424 0.140387 14.524487 0.001152 14.000000 0.000000 c +4.500000 0.000000 l +3.306526 0.000000 2.161933 0.474106 1.318019 1.318020 c +0.474106 2.161934 0.000000 3.306526 0.000000 4.500000 c +0.000000 14.000000 l +0.001153 14.524487 0.140387 15.039425 0.403700 15.493025 c +0.667013 15.946627 1.045122 16.322899 1.500000 16.584000 c +1.500000 4.500000 l +1.500000 3.704351 1.816071 2.941288 2.378680 2.378679 c +2.941289 1.816071 3.704350 1.500000 4.500000 1.500000 c +16.584000 1.500000 l +h +17.000000 18.500000 m +6.000000 18.500000 l +5.602175 18.500000 5.220644 18.341965 4.939340 18.060659 c +4.658035 17.779356 4.500000 17.397825 4.500000 17.000000 c +4.500000 6.000000 l +4.500000 5.602176 4.658035 5.220645 4.939340 4.939341 c +5.220644 4.658036 5.602175 4.500000 6.000000 4.500000 c +17.000000 4.500000 l +17.397825 4.500000 17.779354 4.658036 18.060659 4.939341 c +18.341965 5.220645 18.500000 5.602176 18.500000 6.000000 c +18.500000 17.000000 l +18.500000 17.397825 18.341965 17.779356 18.060659 18.060659 c +17.779354 18.341965 17.397825 18.500000 17.000000 18.500000 c +h +17.000000 20.000000 m +17.795649 20.000000 18.558712 19.683929 19.121321 19.121321 c +19.683929 18.558712 20.000000 17.795650 20.000000 17.000000 c +20.000000 6.000000 l +20.000000 5.204350 19.683929 4.441288 19.121321 3.878679 c +18.558712 3.316071 17.795649 3.000000 17.000000 3.000000 c +6.000000 3.000000 l +5.204350 3.000000 4.441289 3.316071 3.878680 3.878679 c +3.316071 4.441288 3.000000 5.204350 3.000000 6.000000 c +3.000000 17.000000 l +3.000000 17.795650 3.316071 18.558712 3.878680 19.121321 c +4.441289 19.683929 5.204350 20.000000 6.000000 20.000000 c +17.000000 20.000000 l +h +15.500000 12.250000 m +12.250000 12.250000 l +12.250000 15.500000 l +10.750000 15.500000 l +10.750000 12.250000 l +7.500000 12.250000 l +7.500000 10.750000 l +10.750000 10.750000 l +10.750000 7.500000 l +12.250000 7.500000 l +12.250000 10.750000 l +15.500000 10.750000 l +15.500000 12.250000 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2134 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002224 00000 n +0000002247 00000 n +0000002420 00000 n +0000002494 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2553 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json new file mode 100644 index 0000000000..4ecc875445 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "create-album-solid-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/create-album-solid-24.pdf b/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/create-album-solid-24.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8484574055721954af8018ae82091197fef4073e GIT binary patch literal 47980 zcmeIb2b|p0)i-Pi9SjKsNb*n?gTZE4tBo|$vR*R1_Zdww1WO}L)kq_arY5w|LJOTh zu&Jgc*oM#y4j6+efrM(h0Ry3#1k)kZ_e!%fySTw_-uL;&`7%E|%u4qh-FyD$oO{l> zSJL#5%`u3~!lzDu^|eQzoVtT?03MLo;?&_`$X5_~Mk%y;ZUBLN14a`?!ccI4fb0Xf zk)WUeI2;3qO(X%i2IdS92r+=*$PptWQ+biom<(+2GqtJkS?x*)F?9!c^aE)1Ujt(A zn29j0cZ@{WJBAKXT-7fX$g4!VyO$atJY_XdxRuhgJ9i+)Q!)6-=XzX?h zCygUVKp};%Yp9zD0CZJl6jjF)o;rQHJ?PjN<_I;WSdN0A@(Sb-7?rp28HwW|_W)^x ze7u-b^8+RfiVk4p$VgRHc&0pchx|+I+_|gyxpUL^Oxeof+3DJ2fApKwPspEbb=HdJ z^7M{#sq8i<&0iDRZJ#Z+`pJ|X7i{-) zO)p28j%uhTrcrhG^D4tJDl>rU@(M9IzB+*DzhkWm4_U!ELCTG3H$b6FYgSZ?)SQFP zf(P_}-LE4ci@@`ob`A#Tw122^^y>)7Dv2sDs?`CY(*Pl>B2_lh($8}RW+Uf9i~^X9 z_B6s*rJ9oAt3V&Dk{~J33X|crKcesesa0~#hU*kHvOI7vywja;8ovBN?c8SzPrUY%?e@Fz-WA7Tf4IN? z)>*g8udJ2las0qaqV;I&(chl?$)nPdOR^#W`(c$$E@G1Oz(Mi^M^B;1qV#M`6%~?ZwwqfegDtTnv*%;4}vH> z2+zIaL_ap`YeRL9Ome}pnb7T0<=opZK49C-*)Lv}IQzC4^6ax?yF7U1n@afk1&i$J zezS}#cbs|NA8&-uJZ1Yu1*3Q2WsA~>&sHjD|M*O2BRnO4oW1u};qBbpZLzDx_3YMW+txp}+fMky z`RLPS)e*GMpS38naObCwTJyvYK8Dx4Mc%x}Gryg`C+VCY$X)&9+I`o4N`2Z`yY|JG z@BDQBhf^*)=&C>5@)W$|lrx^b>`~9@_wBL!kJ$y=Px-+P16%%R3ffvYYw*zhwq1SB z{oWhWtG560v`c=m?OBC8r}-Yb8-DMbQwE=Td+L;*|CHQz#(B3W^A434{lWB>?~|X- zYh88zfhUD`d*}VF|41Q->b@ z`Og1y-qdZLyy}3b?r_|hj*EA%dVQY@XBcCrPoMeDAJ5z7c-VU2;`83#{lW$5E6*+b z&lVRh`*0q4_491=?Bng1FJEpsX5YsTJ@NC!7cUE4{n8^(p8MC^FEM|)qf|cZ&BW{- zj&1I-_-~)zcIy*^Q%+yJ)!iSwG3VlkznB8Qf7jl3efG#vpIzOWY5IxE+;!K-AAdYj zylT}0vBylGU%KOxZXz=`Vwng8=Uw-AKFIPRf{pX)XUm1DwN_ye% zPdk@;eXerlUEujcmG z|GKpLz$Lr=V%uGoU-{n1XYjAK9NFc_;}3e};4&3XH^ z7wrDFW1F+X++oU2)M5T*RnCC@P0s!!na>9kS zLk8#{UVO@(D;BQ!!HU;bcvmc4k=peX&i=tP`&m!8(Q}SlX@B1Gyz}$Z)r*Y&cHru> z_kQr}zi*}Nm-4Z z2n%$@|GfRqTl+q7=99~=IE;aRBA%Xi?04_o$p@bO8+qSnzkh$#>7_R^2jbBSZ@K7< z3->0L3ZGo)X6`^U7kV%9|JD4)es9>{*nf``U%&sH8@TJ~r?-1&#yi`TF8CXN+Ouy9 zQ^65^E-#h=leKYcl ztrwsC>719glJ1|E*(ZNV?xndS?ANFK>wtYC(gH@e&Q4C z8TTE1+QWN1dPw<@D+kZN;Kd8JJ!Em}uAA?<{?&yCFC>y@%z5!f$HLhQv4!s}ye)O< zjYo6$-n9K~XTPAnu-DDE7j}H1{e1R0;)SE<{&IQ!=|^6g^>`q6+kNkz_u~G)yz8i& zk2>+IW52rQlU?>&vh&$H7f;^efcVu99 z{)^?mC@x<3i{Jfu`efWs|@o#c;$-)pR1pp{>AVowf6@dm!pL!a*Z{<5Fly`5{j{EpbB}zBT{xPuXGiVD^*yhJX92TjyIS%sJ9obaul>ts)&mi2|LOrBLa8gRzu@|t zo?G>gSN}2lxnPy8ey)hG-|^-|q$aL=FbnSbIRtFddhn|S7p4v|jKnN6{sKK0 z{owdRjvx-c7^vV~iJ|>}_n_(FeGk53_aB_P?L*t~%Rcx;%9c9jxufDI6t7%kZpHpJ z_h0+pVw-*a*SAi)j{W6*H$C*xL%SG`x&EQWLznGW@)(`B>{Y(%6ZMv(YSxdZ&;Q*b zuPL<~ebaSW$E8=ioZ9iJS=Y^GSHE&tbkEb@z5o3Cue;;HJI=o2&5v6jpZv3Dr#^qu zj%QqQM(Jm3@#)5|uKsEH0@I~}EEJ@R(tF$_I1MY)&$s{dx7!!qRBpFk{osgyuKeVo zPY$bnR{N;&t7X%Ux$nln(nnsO-$)#|Xw?h+(^sv%Ex%sUb0Ad(RO6ziz`~2^p zy4Z5@;(2?`J7wOd^SSv?T=Mfv7G1jcrKer``DKO6p1s_8`K?zRc*O-*ZgXY*$~P9! z3m&+Nyz08Ee|Gh0SATVlc+Ki-BiF9D&UD>%f7t5}XZ~@^Ki2;E_VtHf|JXv?!X=9a z7hS%1kHx3mFy)5Y4QpX8z#K{CU4WUwr58cb<9I4tJe!*Eh@B%Raula`!)$=a#>A&yn}M zaBuS7l`Fz49=XqZ--Gu%?!Wf|^8?Esoc-XR|1$KK+aDqyy6s`&;amTT|Mix?;eWg3 z5#o_sA2mI?0bHpQ-P-*e>(p3v(Fs<%<5;kXWx2G zc<%k@>(77j!m+EiUUlk=KYa0mm-c$;ikD};ym&RS`p#Euul)7Z=&LWhmU-=+zgPeM z)t}&`t#e4w;x+GXU*&HRNndGA1D3uNB^An?u>VDUiy<`WClbk9eS2CdOBBOqI$Tm}(vr!#i0pfaq$kN&Bp#09?5=|D&+fV8gLGFDIpeulGF;P~oH zh%QtMJ+g5f&{EFP4vdD)@L0UO0SRfjSWiOwFkJIkQEO?w#1U$Sci3v5_LR%te)Y#L?4!?L-=DwFdfYw8XAeI1=z@`#uX~eR{mk26ePv$z zr5$?b>1wMY?RnU#7yROukN$Ppp{MPBBG09ktlIAFGlrJ$yY&8_+&_HXyEgyy1^eCZ zJN&^C!o73CKKG!xGa|38cyZUmf4pY!-2<}QPhBirv~&+o<)t5ec|_#MGauf2W#F2f zs`G}Izj)t3R5@k2@7kA-o>4k%pNG=#lvYmv)!m2RcGf`a+S}Kj^uV2a%vl)vTV(bE z;s<*@FOZJi${)>Nr0%!;ltWKFVR!V*;p3kZtwAp#dQ;q1! zxqa_WN~Md-LG_5Y631>arM2rB7i>U*#@87R|f;fBH+NH~;4HUT9 z(-OEJ_E)TYzTj#re6$^iW!j|-#Z5bGWQXDAP_tYqgW}gfvn+_!q2}yq45;4nL*Tb| z7@9VqH&IKorQA zMY~ntX(Ky;pTi259g5f-qn87J&7PK5)yfbAH5!ds4Q!UABIz2+MI!l+$tc+BKnoI={!5brTy34su6Xu{oIkz!#zkk!j&0RkhcFBuA0?9*pdDAjS={DhNUilwBEBQZ~$RT!T$XG#2^} zfvQkt!}6@EVEW$hwYw;Gqyn1pVijl;P_zx`5YPqZrrB)x`jv4?{w-006JW&H2B8G! zfGuta0PReNOt+|xq7QSKp{y1msUu9KA`~)=W)%=Ue%(LaXbJg-8{{L z`t76L6GA~V+aZAw0Tj(aj?XeRL7g^|0hM}{A7li!RvxTMLV+7Zjl+;;l0xCztU5=( zeFeRT>vm=sNPEbtF_x5=BncxO@8cfRrU9#U0L4H(eTJXOfT&BV=4TE=>ynKda_d|Qc>MinoA(vN25E!V$I>$CeHmc2@kR4Pjx^$xlH$_({Ys(4{RfLRY zb2wT`dm=fr#hkO7qh^cSY_>tBu(=v{=Ifl3YBNr%;kNl}ezVyaE(rm%0XFL&iy5@n zK6dkg`!~&pn!_3~_=L?C$Y3U=a&d`O_DE;QW_QFK_D8_b z>}EzEz00hPE({v=K4FKs;dTPCxUA+97#=v6bKBN!*I^hoTfu07TrzHJSn`3UR&W8E zITyBqV?ZK+k?03t`6EF74tK=e4AXX}9kmxSDSO;sapo<}L^E6nS#*YR)s^=*(*@ER zY31s6A>59{O0u_=!a~+SJS>)oU^|fK;%QgR2G`tfkL34Q^SN~114JiBVOkB)UW?K) zLLs}(5DCWMbTR=JClg4MOA1sXjpi#%S*@}Oyq>}$2q$tzK3OqkU5Fuz1?~A1<0`nz z-h#0eC`txv36Ii3g-?~!IzyQiJt!~Y-eSd@C|8I=MV4z-g(2!v#B6qh;k#{?Fi@9p z%5DV;!D_P2+ie!BJ?f9!9q~vY@6M&0xwO5dGn80Y!{K*DjM0GAL3;|By1nhOMQph| z6OFeLg_Ox>wP2xWfcMyQO)g$)h93$mmyV1&3bcV1Y)XsaNCDk8` zoC>l@`v(>Jtn2j_GP$?OO+GZ%A#M0T218P_^Izui>s)SFrlP$(3 z)r!GVsDi?FB}`Uus}rxan`%O$8jRiRNJOEA&07j^5Lrv(3PePWK(l4BAV>KO)B?YC zhHV>J=950i>__=zy=_H`abG(ud3**orlvSw8L7E_T+9=2wsM{@=?K^;a|N*~NU_D? z`6Aik>41xDM4E;|O=l>h83#sq0=%VMvq+NA(4BW$Z~h?i?% zdiE;`gXBoDj1>>VjE`vgOR-wTX-v7BMp0*2O?<1u zK$TeBXCMouno^6&Q8zBZmXt{}GG;D@HR)ibZpftSEnB|mFJXetu$eM8!d4ST86>$W zS>l*0Q!aa}saQnyL0%VW#4j%Uy^YD&7xc`98_1sk!V z&aj3R>oG~DYGjR11^i@_v85POC6gCmD4vSfd|b;LXl7`sVe;1_c{J#)6bnj~j*4v- zFIDTlkWi#Ueg|PiRGndkQ$3khM5(q7<%&C!?wSv}G7k3f3~7 zW`JoH1#hZiX?m-2L3NltCTG1-l_{32@M)i~9l%p?y=_UG6_%;65zL=q*{Ue(487Jk zi$!EPD->ilC7T?Ha+;1;n^swp+pd(^R?E=5)4+s+yabhPI6+nniL5tU_7ZTch0AK5 zBb1OV4-sr1kNM15^!0D%57Yz#_PVkFNiR_ zp%9KH?P|@e-e^D5i+QnNY8lDi_6EiLx16 z#U|;n*vxSZr2xc8yOmbrXw49#tVkhzcg3-a}^{r5R%`O6Ni;N0p8h9m$lO zry_{81!FOj7-tP>mTy&!V$COWlo0iLqNW-KWLxvrOEDn-Rsaem+X7Tzb%s$>vL(t? zyOPMqm^1@oP?fbeVzF#ef)X^Ia5x?Ibflq#Q!=Tl*>(yF5+>Co<7(Dlt|c96N~#K` zkRuQz!a75#ZE-NPAloe|*uvHevMB=O0R^yFAf_rLkS}2}C@l-BvU0j2c^noj8gY3n zHXGX{y)H+x>cZnZ9}4g)q%#z?h#AHB;>zOempxoCYb@aj=efCTfZoQdS`- zHlP&EH?yS*gnL7+JcPECBvgX^bXw8Omk(!2b1>loKzMvKaHz#l8loaz0Aj!ld99@0 zL(&jUX3K37l|son70vOsXx5iY3wcVI~20H6Y zmJM;H=#mU&#!xF5QUL>JVxmkk)TC0*y52(*>dQCzic$;K(K5}q!MqU+U{I-4_hOLK z%0jY7Xwn3&kD1MqxZ7SNOZ9-qB4xt`CzfN&ZkHoLgL#S98Hz*_cfh7N7pb{T(WV** zs5o23g_O78u6j$=X3$#?LV$TNk`M_6FBepfh6_eG*=j18d;z$N6kBz;S;W|JunjJW zo2{WTMqyMFcy)rqMG|IgcoI$+k$O^T`dl$KP75Mm!GaB%jyuZfV$~6+gI+0_P-8G~ zX6~9q6f20{!={Tv16W0E`q-pDW+`#bd@3)vn{+{h%(fyQ&96qiXfOG=6og8a4&5S2$~C{*ceAc;G(WJNV-D&e-)B%mH#82GIiaGsu)Ee8d2 zNoQV_9c?Mb*r9R@aS6b^bJ;{(^pp*ivL;%iRc9Ei%cM1ytQ3?`C;%t%ViPJAam*#Q z*eI@oNQiSHY@``<=PiUQ0wrlDP7+i=PRVT_S%Dz4l@?1Fq~-~>V|wO5IKuTVjr+LF9Zd-VYp{tCDY*q{o}4s6=3pjQ_a+qJv|44P9_3txKvRy`15Fmyd)R`Q zQlRcHDN?=Aj3kU;mbN((A+!z!X$+6~*rttE3M8z=8o0(dK2WTxdaltZia=Fo~BO4G=<Lx6v3k$6(%EadsT6#SC$(%g~PD zTma~UIe`hqc)$wR562jn1U5D%Ly?OwVQc~j5P@v z=#36a6&92AR$8T!BAjrtaS$viYN{YcO>i@sEWrC`OOo}EZ0V)U4a;@$|c)CZ)^7J1N)hwhQ(b2d>NY4Vzx}!>UCvYG6-Qna0s=F0ianDQw(LB3A$ZTFrNdL zxDe1R-d^JstJ^NuTZtUdtWFjnLSeSUv=Y6HVHYVlp0F33r&jt8>k z0ABZVq9KKeDUfv&K{{RG>>!MfwxdR`t0cGDg(8T3P{!4?SU{L+vVah#GwU&lYM{b2 z6&sr%d=85>C&}&{$j5V>+3dAB=w#1yb^G1qmoCGOoo}+;_imyy9Anj#O(0?*Ra4N& zqcteN7ZXU^&ihOLnv{@(6<1nd!uFJ0$&yM6^0py4(-D`D295)Gi@45^gtJN2hRBFP zB?59lkvZAc^iyKM6>=pTctpq-MS(NSy^bhll!8RAsiT%zirkP)zW z&2Xm5ORZqh%jfBQ9E?*4iAk~$@PR@Z9iU0zmjW3Ods7_jMFhm@jVLNE={-!x)wo}2 zMw>OO)8?+2O*DZxQExq&b&F+htnI;la43nVnzn!}6j>||N1=2gn(+GxB~fqWf%%}` zib=DIP9~KEJq$)cjpObko85(?(`d2_NHXoank3ueo*g0i59D8azvBLS*4($>m~ZJ8IdB^7iR2q;Qb{8f+#W)OSU zibr`6h$OJQo3Kc@Imu#1Lx84Zq>yb>WP^|T7@X`(Qb`BiguPTW!Ul1n7-$-bY&q;I zW;~9#!wAZ1(FkM$xvR}9LjrIhp<+8C@g+m1?#Y%508lc_WEF=f)2gN8ZLBdXPI<|i zJ8e%yoe9;I6G|!C#njz#%tB|AF5H_nM2n=BP_kY%YlwxLG?*H-S;hjjg*=371qcH$ zu5PZURUr)CP!JNcZ3hmM&1A67P?<6h?9UF?5iqNMa1g`F(_MbQDshWH7rReP4JjI4t!c2Oa!{xbY=r> zA~5CK0JT8E3!-~42ZHKI1jP0j2p7$P1`hr%D~`AXbVR?NkHA7@TiXn-$7X$wT9}J> ziWo5JjiPmycTqA&hZ>GJ#1IC^PN%%}RKSTa4Wt-leY6@XQA`uDT7$Tnj;O7o5D!)= zC>jEOHQk*rJEqr}QoF}SXV`*5s(~m>P%swvi7|!nNItYJ@K!vRO_xeVke-S-@M`%g z&6+&{OD$GVs7~Ynf*QC+!3tT0yaq7|r}K3m8}re=f}^gwA+Tx_sgsphHJb(19=%3u z&5C*4142T@0J40-QV~M1;IC$ovfjg@-K(?`q}(d_OJLWk-J27c3JC5USO^D2Dj$Q{ zg?b22B59A*ZgR17(t;UM(Yz}cDS&LLrCV(9>svQRA zWuW8bu!O5($to(%l33zwP*q3)F;Onff7Am&14K7U8$s2CMErSU35A)Gp^e#@Y6w)k z>9VV{xZp@hA&;RTONpS<7c<4ptc9ji4zNm^lAE!XXy7qCtCg_>COiSROerj5aaR>N zY+-qWKTDB58r(!mqzxpi=rWA6M!8Lx>b_`8O|vypEUHSv0#cw%3<#QWOLBrk-E`Q3 z01NhoKm{WPsuT>JtAhDj#KLY+?oq-4$_PwUXP5^=X@f#N30C5GI*}m@J|+^*rhSD7 z?Ser!DbRBKM7yq6ZPIzDERs^F)mdC1ktB}Tl64=V@Ssj-!)PbC6Ei0T*=E4mhMf(Z zZqlBtA0d5koVAlvq&jMqCI> zqj5y z3u=KX0&ui$P$+5GI>C}&Eir{?6;!6J?P}KR5@d#rWm{nD7pj`FpbC-?J3Kx#;udVE zqZ|!Ysfr6SB9e>d3kEjSN|7Ri<3X^Rr1ww|%>ieyXb#~#Vr{#ed{azvX*XuE1zIV( zBx6mEA!R>^s7cWftcOCP5(Dl{$+qK&*A)$f;FddSCj-?)zB2{1?X6JM0}GN&@?e)h z0S<%_hs{QzvdG~wTF;iCG=>Sbwl5F>MeHmgfQ8dK4$Aj6D=7$WGG_5vNw*8-;SNJE zcBdLHC_#`gicU~0^MmqH0+f$pSt{ZOOB~T|(y*R~TknKE@T3vb9h?4zd@$hgL9)8B#%! zNXxELHmM}zVEv)cVj4B4P%N9hS&OqRqfl6G<#T+~k)cx{U-299NXTV~iUOT<*~@4t zWkY#mNoSajQEpJUjq2L9o>Vp>kPWnxtw?7rAY!tC_@mCb5oZWdY;m{82kM*z4Q3$; zv7rH570kyWu=WfbXxN4G2``^i9jY2239yPB0po~afd(6-@}nz0Xeu2{+tb##qz37b zK)Pw*dz~Ql0-Hua>Xjg>S%a|+q8Km@)k%;*fMs68_@i=+pup8Y`%bCKkm3XnQ0g)Y z7HPp^NewkJN{CHb>OK(ghfpci%2UB&4rc1kddmQE*;K#|^9?u|G5Ml_V3Be*V1qS? zs;Q2*0YzrP>H|4Im<3CP>~|>}WDY^;v0XTwEjH_J!kNg1FR-6yXoFjqv|U4f|4;4OTW-HD7~lQK1JgQwdzQUkm2hX~wGU=&P{ zgw=r{V8aPvM{Oi&vf9m7ixD>IV+7aV0R4K`7{?oQY8D6BXE_c-5Q-hlm{54o$eE}? z2HYw?$Wj!;u_V|en_;?_7)QPyYqtn)trs)E=XAP_n!qCnFoYb;uqZ!BAx0MPMHbS<>ozTRm?d6N|^0$w6Pk`N;+WGIj z$xq+&JH%u(vl{_bPM*`?1N(rx$Huxmw#l#4U<2wwWI2SvNNSK`Da{mcYLJE5%piDR z2m>A^!f^zScKMGZ-}p5ed0y1^Y?6ZrJcuDN1Z>#Eh7i2FXS352z17BSebq}mdvSEMOALxBQN46gL>kd&H%s5;&vEz1J ztBo`HHU!^uL-7A9dL2x^ZIWI4Uf+a=$8CKd0?*`7)LI2Rg(C|xiID`)4nvzbKPDy3 zLx9ckLxIlCB#3^!aR=6K=>o;y&b??cg%PX3L-1mzHR8~2wd+xDq|v6h1?#+0(Qbg- zD66p+>+|t5O>G4FgKWN^;{v(`w(JizMzqJO=pW7II)}#ica%^&3brn<+c5ua-#N;$ zCKl9@P@%~S)h(kZ1w_1 z;2g?nm(j#MV(Z+SFwnXDfPQ_w!{Zx`!PybMrYU25w2kp2otpthiS#2ICDLymWx&lu ze5UItu9f2VFm1CAfkoHn+IBB zD*yjuyt?z?e==laK>vprnN#!hn~Rxsebq#a>^}~%PVBmQ7}@_KU}HG<-%Mr!VbOoG zjdfw3kWcJqzV3}>~W;=bKn1N1DA~D0LQKAVM zTu&-}<{)RIm!AvX z0O(9zt?YZV!zW{P2-%$@CuDU!sq`Ciy3HB2IEC13;*?2QoN-c7N>3<#i5slVbG;?t zzBjl@;nsa>9@yKUt$2U;TCk2(PbmF{8)nodIo(qt$YxUn!@5p=A_2-IUFDcq`VBkX zR}xzJ66wty|CvVDb9)mqx-qHr8*+LP3^8f(35s?ng1(1tQdKZ8%E^S5V`Awy>~yOG z?_UE}M|Pv;zK3p7QytRHdM0LdJ*o5?av-d9&Nk})vKM{zJ#xLCPGqVxc4Oy>A=kTB zzaaq({GkV9atdI?@i0HEtFC-%M3fnHCd ziqae3#IWm;^&NI#t!H$795Ht1g8s*@w{|lTk2$#o@-eaW9dj3bZ9r{>3`@Z zwJD{y{y(vqt|yj$!w$UP2v{9r($;zqeDj&=sBWA;5r+weIhl-nOfG$g9(xFdW_vxk^c{K#4$uPzXqioRGp7E>u2(Cc2zEVloe*?Asq`Ci zq-J({5tG<#Ru0InbDs!qlPh8Ngwk)gX?q%gzr?jwRs`-&A^mT2y-f-e!43>_QnOu8 zE`5g{p_MTaeNPeDjl26FJfaK3gut8Jo}!*u`VKpNPm#W*X!BXIo7|S7p0Q47uIovq z?~v1X^649BdporI-{^Wl-$aaVa{Fg{Qt3D3D9v4V=DN*hs-q_3EGIe9b%oMzxardz zrmwSZHr13#O>(^*uoLo>NLMKT7H%7Qj!f@)8l!R3#-F3I(dThY<}n=Op7f$WF2$)Z zt!giH>pn+j1N*JU5$X?w=?X@F_{`?@h?oed*mtV--Kuuq&oSMOPGpAZ(o9IK`8Ss?@8o7ehgZz*;{ z==G>3hF*`X@6a=KR;TrytEO(g-v74O>*|Ei>rqV%y&hS=p@)LIqO=`7+Uhj;9(6~3 zkKR~UCxYG>)r8O+BkMc#z=j@xp1!RId~Y`B^}0GC^m2nbt+=Bsds>0Pa^FCucH{HnvetQiKXAL0}ol$ za^}tLiCMjS)hA?ly}K?ZgkAqe4mx(ImU8uWNcX+VoZM4A(QZaLG32_=s86j9)oUH5 zt{?gj#STHc`xz$$9@3LbzoCbaTCD@*gIdJiyRD?}(F5I{)P&b92TTaPF}eI(=>5Nl z*f+#e7mW7)qv(cTJu?j6BRB+Jv&7XhyfXR@u#n9$AQV`IQCb5C4qhfS41KHB*fs!- zx;bQ3cm}+WjpmiA_O>(>nFY`4(5!cEl(f@R8(+;Rg^K=)HSI*_XvF3i*#UHW2t3ZL z%&W{MpYqb%F7faF7#q{S%V`tz|Gmg2gtPvs|B7pUnQY?7f5o*4;jDk^zv5b7CYw0& zUvX_hIP0JKuejEi$tI3$N?bb(AJ(jZm+|rH0C=nH>}ic@hmA}Hzp5&TJIdNGi7igW z2H*kh|HH$O9*=-X#0FkBIKwuiy=W7rKv@^T!Tl)U4XFpg@If;>tt4f}#^0QI_zXXj z0WWZrs`NXYYe4KB!@<*AzJ1JS!trs( zRD}Vr*;F*)>2I7gLT=lfVH0j~*eL`y+flQTz-$)OL19+HXtkOtqY1%AhAnoJ#R^y% r9i+)Q!)6-=XzX?hCygUVfbN60H!G^%c?2P*PM>ZMI;Q>~9-}b{ literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json new file mode 100644 index 0000000000..99ae7bab96 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-flash-auto-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/media-composer-flash-auto-24.pdf b/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/media-composer-flash-auto-24.pdf new file mode 100644 index 0000000000..784656ed6d --- /dev/null +++ b/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/media-composer-flash-auto-24.pdf @@ -0,0 +1,125 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.148315 0.646362 cm +0.000000 0.000000 0.000000 scn +10.394505 22.621460 m +10.186519 22.621460 9.971912 22.518707 9.860236 22.307106 c +0.186246 9.979193 l +0.027322 9.816003 -0.018069 9.583500 0.006074 9.378285 c +0.030281 9.172525 0.128247 8.961831 0.296594 8.849599 c +0.303070 8.845283 l +0.312180 8.840727 l +0.396315 8.798658 0.484727 8.754453 0.563120 8.720855 c +0.602809 8.703845 0.642242 8.688539 0.679090 8.677324 c +0.714395 8.666579 0.754920 8.657176 0.794503 8.657176 c +8.269956 8.657176 l +6.672553 0.838310 l +6.566589 0.407591 6.894166 0.102909 7.183547 0.006449 c +7.202790 0.000034 l +7.308789 0.000034 l +7.515537 0.000034 7.718171 0.100496 7.825749 0.208075 c +7.831357 0.213682 l +17.517363 12.728341 l +17.676031 12.891515 17.721346 13.123846 17.697220 13.328923 c +17.673504 13.530506 17.578995 13.736824 17.416866 13.850653 c +17.307270 13.954901 17.109980 14.050033 16.908789 14.050033 c +9.433671 14.050033 l +11.030709 21.783056 l +11.136769 22.213842 10.809155 22.518576 10.519747 22.615046 c +10.515336 22.601814 l +10.495173 22.615263 10.479262 22.621460 10.479256 22.621460 c +10.394505 22.621460 l +h +8.522223 18.133879 m +8.967959 18.822742 l +8.731995 18.077103 l +7.495820 12.514318 l +15.035454 12.514318 l +15.499903 12.607208 l +15.100375 12.207681 l +9.094956 4.401608 l +8.752861 3.854254 l +8.971349 4.544376 l +9.830936 8.498480 l +10.207474 10.192890 l +2.755587 10.192890 l +2.144394 10.091025 l +2.600486 10.496440 l +8.522223 18.133879 l +h +17.606695 7.792892 m +15.095354 7.792892 l +13.494057 0.942892 l +15.034959 0.942892 l +15.377815 2.314320 l +17.411192 2.314320 l +17.754049 0.942892 l +19.296955 0.942892 l +17.606695 7.792892 l +h +15.693610 3.764320 m +16.394505 6.879395 l +17.095398 3.764320 l +15.693610 3.764320 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1797 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001887 00000 n +0000001910 00000 n +0000002083 00000 n +0000002157 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2216 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json new file mode 100644 index 0000000000..53ade49241 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-flash-filled-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/media-composer-flash-filled-24.pdf b/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/media-composer-flash-filled-24.pdf new file mode 100644 index 0000000000..1175799051 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/media-composer-flash-filled-24.pdf @@ -0,0 +1,93 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.148315 0.646362 cm +0.000000 0.000000 0.000000 scn +10.394504 22.621460 m +10.186520 22.621460 9.971915 22.518707 9.860238 22.307110 c +0.186246 9.979193 l +0.027322 9.816003 -0.018069 9.583500 0.006074 9.378285 c +0.030281 9.172525 0.128247 8.961831 0.296594 8.849599 c +0.303070 8.845283 l +0.312179 8.840727 l +0.396315 8.798658 0.484726 8.754453 0.563120 8.720855 c +0.602809 8.703845 0.642242 8.688539 0.679090 8.677324 c +0.714395 8.666579 0.754920 8.657176 0.794503 8.657176 c +8.269956 8.657176 l +6.672552 0.838310 l +6.566589 0.407591 6.894166 0.102909 7.183546 0.006449 c +7.202790 0.000034 l +7.308788 0.000034 l +7.515537 0.000034 7.718170 0.100496 7.825748 0.208075 c +7.831357 0.213682 l +17.517353 12.728332 l +17.676027 12.891506 17.721346 13.123842 17.697218 13.328923 c +17.673502 13.530502 17.578995 13.736816 17.416872 13.850647 c +17.307278 13.954897 17.109980 14.050033 16.908789 14.050033 c +9.433671 14.050033 l +11.030708 21.783056 l +11.136768 22.213842 10.809155 22.518576 10.519746 22.615046 c +10.515335 22.601814 l +10.495173 22.615263 10.479261 22.621460 10.479255 22.621460 c +10.394504 22.621460 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1182 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001272 00000 n +0000001295 00000 n +0000001468 00000 n +0000001542 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1601 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json new file mode 100644 index 0000000000..973a998219 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-flash-outline-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/media-composer-flash-outline-24.pdf b/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/media-composer-flash-outline-24.pdf new file mode 100644 index 0000000000..38181245b3 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/media-composer-flash-outline-24.pdf @@ -0,0 +1,110 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.148315 0.646362 cm +0.000000 0.000000 0.000000 scn +10.394504 22.621460 m +10.186520 22.621460 9.971915 22.518707 9.860238 22.307110 c +0.186246 9.979193 l +0.027322 9.816003 -0.018069 9.583500 0.006074 9.378285 c +0.030281 9.172525 0.128247 8.961831 0.296594 8.849599 c +0.303070 8.845283 l +0.312179 8.840727 l +0.396315 8.798658 0.484726 8.754453 0.563120 8.720855 c +0.602809 8.703845 0.642242 8.688539 0.679090 8.677324 c +0.714395 8.666579 0.754920 8.657176 0.794503 8.657176 c +8.269956 8.657176 l +6.672552 0.838310 l +6.566589 0.407591 6.894166 0.102909 7.183546 0.006449 c +7.202790 0.000034 l +7.308788 0.000034 l +7.515537 0.000034 7.718170 0.100496 7.825748 0.208075 c +7.831357 0.213682 l +17.517353 12.728332 l +17.676027 12.891506 17.721346 13.123842 17.697218 13.328923 c +17.673502 13.530502 17.578995 13.736816 17.416872 13.850647 c +17.307278 13.954897 17.109980 14.050033 16.908789 14.050033 c +9.433671 14.050033 l +11.030708 21.783056 l +11.136768 22.213842 10.809155 22.518576 10.519746 22.615046 c +10.515335 22.601814 l +10.495173 22.615263 10.479261 22.621460 10.479255 22.621460 c +10.394504 22.621460 l +h +8.522223 18.133879 m +8.967960 18.822746 l +8.731994 18.077103 l +7.495819 12.514318 l +15.035453 12.514318 l +15.499902 12.607208 l +15.100374 12.207681 l +9.094956 4.401608 l +8.752862 3.854258 l +8.971348 4.544376 l +9.830936 8.498480 l +10.207473 10.192890 l +2.755587 10.192890 l +2.144394 10.091025 l +2.600486 10.496440 l +8.522223 18.133879 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1520 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001610 00000 n +0000001633 00000 n +0000001806 00000 n +0000001880 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1939 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json new file mode 100644 index 0000000000..2c04ad3cc9 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-lock-outline-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/media-composer-lock-outline-24.pdf b/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/media-composer-lock-outline-24.pdf new file mode 100644 index 0000000000..ff0cfb7f23 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/media-composer-lock-outline-24.pdf @@ -0,0 +1,121 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 2.000000 cm +0.105882 0.105882 0.105882 scn +13.000000 13.000000 m +13.000000 16.000000 l +13.000000 17.326082 12.473216 18.597853 11.535534 19.535534 c +10.597852 20.473215 9.326082 21.000000 8.000000 21.000000 c +6.673918 21.000000 5.402148 20.473215 4.464466 19.535534 c +3.526784 18.597853 3.000000 17.326082 3.000000 16.000000 c +3.000000 13.000000 l +2.204350 13.000000 1.441289 12.683929 0.878680 12.121321 c +0.316071 11.558712 0.000000 10.795650 0.000000 10.000000 c +0.000000 3.000000 l +0.000000 2.204351 0.316071 1.441288 0.878680 0.878679 c +1.441289 0.316071 2.204350 0.000000 3.000000 0.000000 c +13.000000 0.000000 l +13.795650 0.000000 14.558712 0.316071 15.121321 0.878679 c +15.683929 1.441288 16.000000 2.204351 16.000000 3.000000 c +16.000000 10.000000 l +16.000000 10.795650 15.683929 11.558712 15.121321 12.121321 c +14.558712 12.683929 13.795650 13.000000 13.000000 13.000000 c +h +4.500000 16.000000 m +4.500000 16.928257 4.868749 17.818497 5.525126 18.474874 c +6.181504 19.131250 7.071742 19.500000 8.000000 19.500000 c +8.928258 19.500000 9.818497 19.131250 10.474874 18.474874 c +11.131250 17.818497 11.500000 16.928257 11.500000 16.000000 c +11.500000 13.000000 l +4.500000 13.000000 l +4.500000 16.000000 l +h +14.500000 3.000000 m +14.500000 2.602175 14.341964 2.220646 14.060659 1.939341 c +13.779355 1.658035 13.397824 1.500000 13.000000 1.500000 c +3.000000 1.500000 l +2.602175 1.500000 2.220644 1.658035 1.939340 1.939341 c +1.658035 2.220646 1.500000 2.602175 1.500000 3.000000 c +1.500000 10.000000 l +1.500000 10.397824 1.658035 10.779356 1.939340 11.060660 c +2.220644 11.341965 2.602175 11.500000 3.000000 11.500000 c +13.000000 11.500000 l +13.397824 11.500000 13.779355 11.341965 14.060659 11.060660 c +14.341964 10.779356 14.500000 10.397824 14.500000 10.000000 c +14.500000 3.000000 l +h +8.750000 6.208000 m +8.750000 4.000000 l +7.250000 4.000000 l +7.250000 6.208000 l +6.964037 6.373101 6.740543 6.627947 6.614181 6.933013 c +6.487818 7.238079 6.465649 7.576317 6.551111 7.895267 c +6.636574 8.214216 6.824892 8.496055 7.086858 8.697068 c +7.348824 8.898082 7.669799 9.007038 8.000000 9.007038 c +8.330201 9.007038 8.651175 8.898082 8.913142 8.697068 c +9.175109 8.496055 9.363426 8.214216 9.448889 7.895267 c +9.534351 7.576317 9.512182 7.238079 9.385819 6.933013 c +9.259457 6.627947 9.035963 6.373101 8.750000 6.208000 c +8.750000 6.208000 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2423 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002513 00000 n +0000002536 00000 n +0000002709 00000 n +0000002783 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2842 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json b/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json new file mode 100644 index 0000000000..475ad9106b --- /dev/null +++ b/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-navbar-chevron.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/media-composer-navbar-chevron.pdf b/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/media-composer-navbar-chevron.pdf new file mode 100644 index 0000000000..0baa5fd89a --- /dev/null +++ b/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/media-composer-navbar-chevron.pdf @@ -0,0 +1,73 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +0.000000 -1.000000 1.000000 0.000000 4.995239 8.406494 cm +0.000000 0.000000 0.000000 scn +5.508584 1.675859 m +0.000000 6.963662 l +1.385004 8.406494 l +8.406458 1.666456 l +1.375829 -4.995262 l +0.000217 -3.543472 l +5.508584 1.675859 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 269 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 13.401733 8.406494 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000359 00000 n +0000000381 00000 n +0000000553 00000 n +0000000627 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +686 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json new file mode 100644 index 0000000000..5ae7a11ef1 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "switch-camera-24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/switch-camera-24.pdf b/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/switch-camera-24.pdf new file mode 100644 index 0000000000..b7c885ec3a --- /dev/null +++ b/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/switch-camera-24.pdf @@ -0,0 +1,108 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.209961 2.659668 cm +0.000000 0.000000 0.000000 scn +18.150002 15.700432 m +17.090000 14.640432 l +16.396545 15.340194 15.570852 15.895062 14.660963 16.272751 c +13.751074 16.650442 12.775159 16.843410 11.790000 16.840431 c +9.800877 16.840431 7.893222 16.050255 6.486699 14.643732 c +5.080176 13.237209 4.290000 11.329556 4.290000 9.340431 c +4.290000 8.640431 l +5.000000 9.860432 l +6.000000 10.860432 l +7.070000 9.790431 l +3.540000 6.280431 l +0.000000 9.810431 l +1.070000 10.880432 l +2.070000 9.880431 l +2.790000 8.640431 l +2.790000 9.340431 l +2.791565 11.119312 3.320253 12.857834 4.309292 14.336420 c +5.298331 15.815008 6.703359 16.967339 8.346918 17.647875 c +9.990478 18.328411 11.798848 18.506628 13.543634 18.160015 c +15.288421 17.813402 16.891361 16.957508 18.150002 15.700432 c +18.150002 15.700432 l +h +20.040001 12.400433 m +16.500000 8.870431 l +17.570002 7.800431 l +18.570002 8.800431 l +19.280001 10.020432 l +19.280001 9.340431 l +19.280003 7.353039 18.491199 5.446898 17.086838 4.040663 c +15.682477 2.634427 13.777390 1.843081 11.790000 1.840431 c +10.804841 1.837452 9.828926 2.030422 8.919037 2.408112 c +8.009148 2.785801 7.183455 3.340670 6.490000 4.040432 c +5.430000 2.980431 l +6.688641 1.723354 8.291579 0.867460 10.036366 0.520847 c +11.781152 0.174234 13.589522 0.352451 15.233082 1.032988 c +16.876642 1.713524 18.281670 2.865855 19.270708 4.344442 c +20.259747 5.823030 20.788437 7.561551 20.790001 9.340431 c +20.790001 10.040431 l +21.500000 8.820431 l +22.500000 7.820431 l +23.570002 8.890431 l +20.040001 12.400433 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1599 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001689 00000 n +0000001712 00000 n +0000001885 00000 n +0000001959 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2018 +%%EOF \ No newline at end of file diff --git a/Signal/Images.xcassets/navbar_disclosure_down.imageset/Contents.json b/Signal/Images.xcassets/navbar_disclosure_down.imageset/Contents.json deleted file mode 100644 index 3cfcd92ea0..0000000000 --- a/Signal/Images.xcassets/navbar_disclosure_down.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "navbar_disclosure_down_small@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "navbar_disclosure_down_small@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "navbar_disclosure_down_small@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@1x.png b/Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@1x.png deleted file mode 100644 index fe04a6e29fa20d66a7b79914f45a1544942e5e79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1279 zcmZ`(OH9;27_J5omXMI>0hL3kA%R3X?E(U|+qkqSYgj~Flf{IDZg+rcUukFE;v?}v zVvIyDz7F1u2?t~JgdR8;JsJ=@zy&OOxr$eDi(t{r}(2+?*Ic z){@wlpeU*(GpbDDz8SY&jScwy#afxhUEN$do2IB2cbfmI^?2Uoj81tJ)iFrgj*b{t3wUTkq|&}=Sf1dgn1CVRF@XU?2NZNJ#pJUS zv~0VO?q~a0j*${HO^a@ER+v3Oq0HeKgOJ!C=1# zVw^|>WJPwNiflJ$+lCY*VUj7`%MP%iHp&RaA|MATa^9qm>?I?W` z-wIAfN$1-3UcUXh=?d3c2R1%0=5I`Q&;ROPUVHQ5;OO_Kt(V%SAGTjydGNNd)QQ&C z<#!jBmmb&aS3m7s&B!0S*44Z6em`}j#9iOB_NuXW`PTWanS1GuKZhIiZ!fjq2G?KO i{pift>eu7X-`BHC!;3#Qo>)K0m6RD7SDp>eT>A$k28wv0i>ry1t>MrKP@sk-m|UE>MMTab;dfVufyAu`tKo-FP#GNIXX$YJ0ilN>DdQcxEqi?8ZppQ*kYDFdv zZ6Fz(+ES9?zA7j!$^rW+C0Rc;Cp9-UucR2L&k(D1z_5jDL~&$AVgbZ5a3DhbW&?7$ zRZwbieoiSU2txBROYDqnz#c*uMuZ4Nmveq@K~ZXPF;JVG0aOdJ7`plhgf(_XhGtM5 z$fD>v{EISyUQGl!%+3(19$5@seI!;JkVTPnfCAqtAhRMhC&DEY7^Gl-7yKQE)+Gaw^DWU@b^O=&HfiL?Wy)G(oloNdie@O0rdPX;M~datTs=0b>oE`kfNf z^^t^a^s%b80j32j-~5!!v`Ux6l2kh*149cdLqjV=69q#95W~XMMjuTLk_$jG&PAz- zCHX}m`T03^U|-~dxC+L4=6X=Q$SRx+i>cemg8=p@~BOs|vc(_pPtIb<`3Ud2S0%^ZZ&{A~d04qi#saqs0c# tg&aRd?`SzT_j~+TmSZjJo$5B$Fs`q!K2)ne$q7_gdb;|#taD0e0szAu)1m+X diff --git a/Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@3x.png b/Signal/Images.xcassets/navbar_disclosure_down.imageset/navbar_disclosure_down_small@3x.png deleted file mode 100644 index a8422b949fce08f45264e56ff9adf5b6dcb12fe8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1580 zcmeAS@N?(olHy`uVBq!ia0vp^ia;#N!3HEx@V8$9QY`6?zK#qG8~eHcB(eheoCO|{ z#XxlyAk64G%`gF|MlCZWq9nrC$0|8LS1&OoKPgqOBDVmjnt{Ql!V1XDO)W`OsL0L9 zE4HezRRXK90GK4GQ<#=IWDQi z$wiq3C7Jno3LtY6lk!VTY?YKi7Qq3;oh6xR2%GYXq22;|P#+|tZ>VRWk4;-@MJ5hy zAQ_z6Qj+1mDkv?=0sAQ>SwA%=H8(Y{q!_5r5UX{-u!U6i- zqUbvOi!y;;O$0g2&Jd~|Sqxo$Bvu=cMUixX0^ceivm!Mo!X*w6HQXv@$eNFf;%$%q(p5(bOQh03_pFl$uzQ zUlfv`pJNAhY%YkaV611ZXA04atOBapMjw=uka89z+k!=b`PPmLs1sHO*l`IRXRBji zV4UOW;uzw=dv)qwuOkj3t=Io1ma-mf-|#W?&6}Lu>!qdA@s5IGAsYf*95-y-ut7-P zlY8sNz{lkYYAVxb&N#yRqV@0@+nwLT?3G z4f9dM7cAV_lU{5+Xcw@j>FXSkBLaIGbLLDcDQ-0tE=hS{pndM2tWS`q(BG#EE!y8K zZ8bIfa{7RY!av6H36ooHFvmAO^4<~U_F{6Qa)LA4F-vB@?fx@s>-*QI)?6t+ z(E0kj-h7qlRwo~PnC$IDG^JvN8dH_T}Kko}@{^*WwQsl1YvP1_glx+(Rc zO|UQa+M3R1JSjad%GFAgjFjrk$`A1Otd)AgwrPnX^U?T2GZJFsm>c8lTdx(xAKJy7 sT+4HhO@F~mZwcv~N}e@Q@8X}`-#yQMPU`;2M?vMcr>mdKI;Vst0HUfR8~^|S diff --git a/Signal/src/ViewControllers/Photos/CameraCaptureControl.swift b/Signal/src/ViewControllers/Photos/CameraCaptureControl.swift new file mode 100644 index 0000000000..11e9d0256e --- /dev/null +++ b/Signal/src/ViewControllers/Photos/CameraCaptureControl.swift @@ -0,0 +1,424 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import UIKit +import SignalUI + +protocol CameraCaptureControlDelegate: AnyObject { + // MARK: Photo + func cameraCaptureControlDidRequestCapturePhoto(_ control: CameraCaptureControl) + + // MARK: Video + func cameraCaptureControlDidRequestStartVideoRecording(_ control: CameraCaptureControl) + func cameraCaptureControlDidRequestFinishVideoRecording(_ control: CameraCaptureControl) + func cameraCaptureControlDidRequestCancelVideoRecording(_ control: CameraCaptureControl) + + // MARK: Zoom + var zoomScaleReferenceHeight: CGFloat? { get } + func cameraCaptureControl(_ control: CameraCaptureControl, didUpdate zoomAlpha: CGFloat) +} + +class CameraCaptureControl: UIView { + + private let shutterButtonOuterCircle = CircleBlurView(effect: UIBlurEffect(style: .light)) + private let shutterButtonInnerCircle = CircleView() + + private static let recordingLockControlSize: CGFloat = 36 // Stop button, swipe tracking circle, lock icon + private static let shutterButtonDefaultSize: CGFloat = 72 + private static let shutterButtonRecordingSize: CGFloat = 122 + + private var outerCircleSizeConstraint: NSLayoutConstraint! + private var innerCircleSizeConstraint: NSLayoutConstraint! + private var slidingCirclePositionContstraint: NSLayoutConstraint! + + private lazy var slidingCircleView: CircleView = { + let view = CircleView(diameter: CameraCaptureControl.recordingLockControlSize) + view.backgroundColor = .ows_white + return view + }() + private lazy var lockIconView = LockView(frame: CGRect(origin: .zero, size: .square(CameraCaptureControl.recordingLockControlSize))) + private lazy var stopButton: UIButton = { + let button = OWSButton { [weak self] in + guard let self = self else { return } + self.didTapStopButton() + } + button.backgroundColor = .white + button.dimsWhenHighlighted = true + button.layer.masksToBounds = true + button.layer.cornerRadius = 4 + button.alpha = 0 + return button + }() + + weak var delegate: CameraCaptureControlDelegate? + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + autoSetDimension(.height, toSize: CameraCaptureControl.shutterButtonDefaultSize) + + // Round Shutter Button + addSubview(shutterButtonOuterCircle) + outerCircleSizeConstraint = shutterButtonOuterCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) + shutterButtonOuterCircle.autoPin(toAspectRatio: 1) + shutterButtonOuterCircle.autoVCenterInSuperview() + shutterButtonOuterCircle.autoHCenterInSuperview() + + addSubview(shutterButtonInnerCircle) + innerCircleSizeConstraint = shutterButtonInnerCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) + shutterButtonInnerCircle.autoPin(toAspectRatio: 1) + shutterButtonInnerCircle.isUserInteractionEnabled = false + shutterButtonInnerCircle.backgroundColor = .clear + shutterButtonInnerCircle.layer.borderColor = UIColor.ows_white.cgColor + shutterButtonInnerCircle.layer.borderWidth = 5 + shutterButtonInnerCircle.centerXAnchor.constraint(equalTo: shutterButtonOuterCircle.centerXAnchor).isActive = true + shutterButtonInnerCircle.centerYAnchor.constraint(equalTo: shutterButtonOuterCircle.centerYAnchor).isActive = true + + // 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(handleLongPress)) + longPressGesture.minimumPressDuration = 0 + shutterButtonOuterCircle.addGestureRecognizer(longPressGesture) + } + + // MARK: - UI State + + enum State { + case initial + case recording + case recordingLocked + } + + private var _internalState: State = .initial + var state: State { + set { + setState(newValue) + } + get { + _internalState + } + } + + private var sliderTrackingProgress: CGFloat = 0 { + didSet { + if sliderTrackingProgress == 1 && state == .recording { + setState(.recordingLocked) // this will call updateUIForCurrentState() + } else { + updateUIForCurrentState() + } + } + } + + func setState(_ state: State, animationDuration: TimeInterval = 0) { + guard _internalState != state else { return } + + Logger.debug("New state: \(_internalState) -> \(state)") + + _internalState = state + if animationDuration > 0 { + UIView.animate(withDuration: animationDuration, + delay: 0, + options: [ .beginFromCurrentState ], + animations: { + self.updateUIForCurrentState() + }) + } else { + updateUIForCurrentState() + } + } + + private func updateUIForCurrentState() { + switch state { + case .initial: + // element visibility + if slidingCirclePositionContstraint != nil { + stopButton.alpha = 0 + slidingCircleView.alpha = 0 + lockIconView.alpha = 0 + lockIconView.state = .unlocked + } + shutterButtonInnerCircle.alpha = 1 + shutterButtonInnerCircle.backgroundColor = .clear + // element sizes + outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize + innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize + + case .recording: + prepareRecordingControlsIfNecessary() + // element visibility + stopButton.alpha = sliderTrackingProgress > 0 ? 1 : 0 + slidingCircleView.alpha = sliderTrackingProgress > 0 ? 1 : 0 + lockIconView.alpha = 1 + lockIconView.setState(sliderTrackingProgress > 0.5 ? .locking : .unlocked, animated: true) + shutterButtonInnerCircle.backgroundColor = .ows_white + // element sizes + outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonRecordingSize + // Inner (white) circle gets smaller as user drags the slider and reveals stop button when the slider is halfway to the lock icon. + innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize - 2 * sliderTrackingProgress * (CameraCaptureControl.shutterButtonDefaultSize - CameraCaptureControl.recordingLockControlSize) + + case .recordingLocked: + prepareRecordingControlsIfNecessary() + // element visibility + stopButton.alpha = 1 + slidingCircleView.alpha = 1 + lockIconView.alpha = 1 + lockIconView.setState(.locked, animated: true) + shutterButtonInnerCircle.alpha = 0 + shutterButtonInnerCircle.backgroundColor = .ows_white + // element sizes + outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonRecordingSize + innerCircleSizeConstraint.constant = CameraCaptureControl.recordingLockControlSize + } + + setNeedsLayout() + layoutIfNeeded() + } + + private func prepareRecordingControlsIfNecessary() { + guard slidingCirclePositionContstraint == nil else { return } + + addSubview(stopButton) + stopButton.autoPin(toAspectRatio: 1) + stopButton.autoSetDimension(.width, toSize: CameraCaptureControl.recordingLockControlSize) + stopButton.centerXAnchor.constraint(equalTo: shutterButtonOuterCircle.centerXAnchor).isActive = true + stopButton.centerYAnchor.constraint(equalTo: shutterButtonOuterCircle.centerYAnchor).isActive = true + + insertSubview(slidingCircleView, belowSubview: shutterButtonInnerCircle) + slidingCircleView.autoVCenterInSuperview() + slidingCirclePositionContstraint = slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonOuterCircle.centerXAnchor, constant: 0) + addConstraint(slidingCirclePositionContstraint) + + addSubview(lockIconView) + lockIconView.autoVCenterInSuperview() + lockIconView.autoPinTrailing(toEdgeOf: self) + + setNeedsLayout() + UIView.performWithoutAnimation { + self.layoutIfNeeded() + } + } + + // MARK: - Gestures + + private var longPressGesture: UILongPressGestureRecognizer! + private static let longPressDurationThreshold = 0.5 + private var initialTouchLocation: CGPoint? + private var touchTimer: Timer? + + @objc + private func handleLongPress(gesture: UILongPressGestureRecognizer) { + guard let gestureView = gesture.view else { + owsFailDebug("gestureView was unexpectedly nil") + return + } + + switch gesture.state { + case .possible: + break + + case .began: + guard state == .initial else { break } + + initialTouchLocation = gesture.location(in: gesture.view) + + touchTimer?.invalidate() + touchTimer = WeakTimer.scheduledTimer( + timeInterval: CameraCaptureControl.longPressDurationThreshold, + target: self, + userInfo: nil, + repeats: false + ) { [weak self] _ in + guard let self = self else { return } + + self.setState(.recording, animationDuration: 0.4) + + self.delegate?.cameraCaptureControlDidRequestStartVideoRecording(self) + } + + case .changed: + guard state == .recording else { break } + + guard let referenceHeight = delegate?.zoomScaleReferenceHeight else { + owsFailDebug("referenceHeight was unexpectedly nil") + return + } + + guard referenceHeight > 0 else { + owsFailDebug("referenceHeight was unexpectedly <= 0") + return + } + + guard let initialTouchLocation = initialTouchLocation else { + owsFailDebug("initialTouchLocation was unexpectedly nil") + return + } + + let currentLocation = gesture.location(in: gestureView) + + // Zoom + let minDistanceBeforeActivatingZoom: CGFloat = 30 + let yDistance = initialTouchLocation.y - currentLocation.y - minDistanceBeforeActivatingZoom + let distanceForFullZoom = referenceHeight / 4 + let yRatio = yDistance / distanceForFullZoom + let yAlpha = yRatio.clamp(0, 1) + delegate?.cameraCaptureControl(self, didUpdate: yAlpha) + + // Video Recording Lock + let xOffset = currentLocation.x - initialTouchLocation.x + updateTracking(xOffset: xOffset) + + case .ended: + touchTimer?.invalidate() + touchTimer = nil + + guard state != .recordingLocked else { return } + + if state == .recording { + setState(.initial, animationDuration: 0.2) + + delegate?.cameraCaptureControlDidRequestFinishVideoRecording(self) + } else { + delegate?.cameraCaptureControlDidRequestCapturePhoto(self) + } + + case .cancelled, .failed: + if state == .recording { + setState(.initial, animationDuration: 0.2) + + delegate?.cameraCaptureControlDidRequestCancelVideoRecording(self) + } + + touchTimer?.invalidate() + touchTimer = nil + + @unknown default: + owsFailDebug("unexpected gesture state: \(gesture.state.rawValue)") + } + } + + private func updateTracking(xOffset: CGFloat) { + let minDistanceBeforeActivatingLockSlider: CGFloat = 30 + let effectiveDistance = xOffset - minDistanceBeforeActivatingLockSlider + let distanceToLock = abs(lockIconView.center.x - stopButton.center.x) + let trackingPosition = effectiveDistance.clamp(0, distanceToLock) + slidingCirclePositionContstraint.constant = trackingPosition + sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1) + + Logger.verbose("xOffset: \(xOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), trackingPosition: \(trackingPosition), progress: \(sliderTrackingProgress)") + } + + // MARK: - Button Actions + + private func didTapStopButton() { + delegate?.cameraCaptureControlDidRequestFinishVideoRecording(self) + } + + // MARK: - LockView + + private class LockView: UIView { + + private var imageViewLock = UIImageView(image: UIImage(named: "media-composer-lock-outline-24")) + private var blurBackgroundView = CircleBlurView(effect: UIBlurEffect(style: .dark)) + private var whiteBackgroundView = CircleView() + private var whiteCircleView = CircleView() + + enum State { + case unlocked + case locking + case locked + } + private var _internalState: State = .unlocked + var state: State { + set { + guard _internalState != newValue else { return } + setState(newValue) + } + get { + _internalState + } + } + + func setState(_ state: State, animated: Bool = false) { + _internalState = state + if animated { + UIView.animate(withDuration: 0.25, + delay: 0, + options: [ .beginFromCurrentState ]) { + self.updateAppearance() + } + } else { + updateAppearance() + } + } + + private func updateAppearance() { + switch state { + case .unlocked: + blurBackgroundView.alpha = 1 + whiteCircleView.alpha = 0 + whiteBackgroundView.alpha = 0 + imageViewLock.alpha = 1 + imageViewLock.tintColor = .ows_white + + case .locking: + blurBackgroundView.alpha = 1 + whiteCircleView.alpha = 1 + whiteBackgroundView.alpha = 0 + imageViewLock.alpha = 0 + + case .locked: + blurBackgroundView.alpha = 0 + whiteCircleView.alpha = 0 + whiteBackgroundView.alpha = 1 + imageViewLock.alpha = 1 + imageViewLock.tintColor = .ows_black + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + isUserInteractionEnabled = false + + addSubview(blurBackgroundView) + blurBackgroundView.autoPinEdgesToSuperviewEdges() + + addSubview(whiteCircleView) + whiteCircleView.backgroundColor = .clear + whiteCircleView.layer.borderColor = UIColor.ows_white.cgColor + whiteCircleView.layer.borderWidth = 3 + whiteCircleView.autoPinEdgesToSuperviewEdges() + + addSubview(whiteBackgroundView) + whiteBackgroundView.backgroundColor = .ows_white + whiteBackgroundView.autoPinEdgesToSuperviewEdges() + + addSubview(imageViewLock) + imageViewLock.tintColor = .ows_white + imageViewLock.autoCenterInSuperview() + + updateAppearance() + } + + override var intrinsicContentSize: CGSize { + CGSize(width: CameraCaptureControl.recordingLockControlSize, height: CameraCaptureControl.recordingLockControlSize) + } + } +} diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 8e1d2c3af9..4deb8995e2 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -6,14 +6,14 @@ import Foundation import Photos protocol ImagePickerGridControllerDelegate: AnyObject { - func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) + func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController) + func imagePickerDidRequestPresentCamera(_ imagePicker: ImagePickerGridController) func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) - var isInBatchSelectMode: Bool { get } func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController) } @@ -61,30 +61,12 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat owsFailDebug("collectionView was unexpectedly nil") return } + collectionView.allowsMultipleSelection = true collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier) - // ensure images at the end of the list can be scrolled above the bottom buttons - let bottomButtonInset = -1 * SendMediaNavigationController.bottomButtonsCenterOffset + SendMediaNavigationController.bottomButtonWidth / 2 - collectionView.contentInset.bottom = bottomButtonInset + 8 - view.backgroundColor = .ows_gray95 - - // The PhotoCaptureVC needs a shadow behind it's cancel button, so we use a custom icon. - // This VC has a visible navbar so doesn't need the shadow, but because the user can - // quickly toggle between the Capture and the Picker VC's, we use the same custom "X" - // icon here rather than the system "stop" icon so that the spacing matches exactly. - // Otherwise there's a noticable shift in the icon placement. - if UIDevice.current.isIPad { - let cancelButton = OWSButton.shadowedCancelButton { [weak self] in - self?.didPressCancel() - } - navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton) - } else { - let cancelImage = UIImage(imageLiteralResourceName: "ic_x_with_shadow") - let cancelButton = UIBarButtonItem(image: cancelImage, style: .plain, target: self, action: #selector(didPressCancel)) - - cancelButton.tintColor = .ows_gray05 - navigationItem.leftBarButtonItem = cancelButton - } + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancel)) + cancelButton.tintColor = .ows_gray05 + navigationItem.leftBarButtonItem = cancelButton let titleView = TitleView() titleView.delegate = self @@ -116,10 +98,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - guard delegate.isInBatchSelectMode else { - return - } - switch selectionPanGesture.state { case .possible: break @@ -180,11 +158,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - guard delegate.isInBatchSelectMode else { - owsFailDebug("isInBatchSelectMode was unexpectedly false") - return - } - let asset = photoCollectionContents.asset(at: indexPath.item) switch selectionPanGestureMode { case .select: @@ -258,7 +231,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - // MARK: + // MARK: private var lastPageYOffset: CGFloat { let yOffset = collectionView.contentSize.height - collectionView.frame.height + collectionView.contentInset.bottom + view.safeAreaInsets.bottom @@ -360,24 +333,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return selectedIndexPaths.contains(indexPath) } - func batchSelectModeDidChange() { - applyBatchSelectMode() - } - - func applyBatchSelectMode() { - guard let delegate = delegate else { - return - } - - guard let collectionView = collectionView else { - owsFailDebug("collectionView was unexpectedly nil") - return - } - - collectionView.allowsMultipleSelection = delegate.isInBatchSelectMode - updateVisibleCells() - } - // MARK: - PhotoLibraryDelegate func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) { @@ -482,12 +437,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) let attachmentPromise: Promise = photoCollectionContents.outgoingAttachment(for: asset) delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise) - - if !delegate.isInBatchSelectMode { - // Don't show "selected" badge unless we're in batch mode - collectionView.deselectItem(at: indexPath, animated: false) - delegate.imagePickerDidCompleteSelection(self) - } } override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { @@ -593,7 +542,7 @@ private class TitleView: UIView { label.font = UIFont.ows_dynamicTypeBody.ows_semibold iconView.tintColor = .ows_gray05 - iconView.image = UIImage(named: "navbar_disclosure_down")?.withRenderingMode(.alwaysTemplate) + iconView.image = UIImage(named: "media-composer-navbar-chevron") addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped))) } diff --git a/Signal/src/ViewControllers/Photos/MediaDoneButton.swift b/Signal/src/ViewControllers/Photos/MediaDoneButton.swift new file mode 100644 index 0000000000..e6e7a6bf31 --- /dev/null +++ b/Signal/src/ViewControllers/Photos/MediaDoneButton.swift @@ -0,0 +1,106 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +import SignalUI +import UIKit + +class MediaDoneButton: UIButton { + + var badgeNumber: Int = 0 { + didSet { + textLabel.text = numberFormatter.string(for: badgeNumber) + invalidateIntrinsicContentSize() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private let numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + return numberFormatter + }() + + private let textLabel: UILabel = { + let label = UILabel() + label.textColor = .ows_white + label.textAlignment = .center + label.font = .ows_dynamicTypeSubheadline.ows_monospaced + return label + }() + private var pillView: PillView! + private var chevronImageView: UIImageView! + private var dimmerView: UIView! + + private func commonInit() { + pillView = PillView(frame: bounds) + pillView.isUserInteractionEnabled = false + pillView.layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 7) + addSubview(pillView) + pillView.autoPinEdgesToSuperviewEdges() + + let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + pillView.addSubview(blurBackgroundView) + blurBackgroundView.autoPinEdgesToSuperviewEdges() + + let blueBadgeView = PillView(frame: bounds) + blueBadgeView.backgroundColor = .ows_accentBlue + blueBadgeView.layoutMargins = UIEdgeInsets(margin: 4) + blueBadgeView.addSubview(textLabel) + textLabel.autoPinEdgesToSuperviewMargins() + + let image: UIImage? + if #available(iOS 13, *) { + image = CurrentAppContext().isRTL ? UIImage(systemName: "chevron.backward") : UIImage(systemName: "chevron.right") + } else { + image = CurrentAppContext().isRTL ? UIImage(named: "chevron-left-20") : UIImage(named: "chevron-right-20") + } + chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate)) + chevronImageView.contentMode = .center + chevronImageView.tintColor = .ows_white + if #available(iOS 13, *) { + chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) + } + + let hStack = UIStackView(arrangedSubviews: [blueBadgeView, chevronImageView]) + hStack.spacing = 6 + pillView.addSubview(hStack) + hStack.autoPinEdgesToSuperviewMargins() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { + return + } + textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced + if #available(iOS 13, *) { + chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) + } + } + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + if dimmerView == nil { + dimmerView = UIView(frame: bounds) + dimmerView.isUserInteractionEnabled = false + dimmerView.backgroundColor = .ows_black + pillView.addSubview(dimmerView) + dimmerView.autoPinEdgesToSuperviewEdges() + } + dimmerView.alpha = 0.5 + } else if let dimmerView = dimmerView { + dimmerView.alpha = 0 + } + } + } +} diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index ffc4cf9761..b54233a681 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -1,23 +1,23 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // -import Foundation import CoreServices +import Foundation protocol PhotoCaptureDelegate: AnyObject { // MARK: Still Photo - func photoCaptureDidStartPhotoCapture(_ photoCapture: PhotoCapture) - func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment) - func photoCapture(_ photoCapture: PhotoCapture, processingDidError error: Error) + func photoCaptureDidStart(_ photoCapture: PhotoCapture) + func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessing attachment: SignalAttachment) + func photoCapture(_ photoCapture: PhotoCapture, didFailProcessing error: Error) - // MARK: Movie + // MARK: Video - func photoCaptureDidBeginMovie(_ photoCapture: PhotoCapture) - func photoCaptureDidCompleteMovie(_ photoCapture: PhotoCapture) - func photoCaptureDidCancelMovie(_ photoCapture: PhotoCapture) + func photoCaptureDidBeginRecording(_ photoCapture: PhotoCapture) + func photoCaptureDidFinishRecording(_ photoCapture: PhotoCapture) + func photoCaptureDidCancelRecording(_ photoCapture: PhotoCapture) // MARK: Utility @@ -29,7 +29,7 @@ protocol PhotoCaptureDelegate: AnyObject { func beginCaptureButtonAnimation(_ duration: TimeInterval) func endCaptureButtonAnimation(_ duration: TimeInterval) - func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusingAtPoint focusPoint: CGPoint) + func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusing focusPoint: CGPoint) } @@ -82,7 +82,7 @@ class PhotoCapture: NSObject { let focusPoint = currentCaptureInput.device.focusPointOfInterest DispatchQueue.main.async { - self.delegate?.photoCapture(self, didCompleteFocusingAtPoint: focusPoint) + self.delegate?.photoCapture(self, didCompleteFocusing: focusPoint) } } @@ -292,20 +292,20 @@ class PhotoCapture: NSObject { focusObservation.invalidate() } self.focusObservation = newInput.observe(\.device.isAdjustingFocus, - options: [.old, .new]) { [weak self] _, change in - guard let self = self else { return } + options: [.old, .new]) { [weak self] _, change in + guard let self = self else { return } - guard let oldValue = change.oldValue else { - return - } + guard let oldValue = change.oldValue else { + return + } - guard let newValue = change.newValue else { - return - } + guard let newValue = change.newValue else { + return + } - if oldValue == true && newValue == false { - self.didCompleteFocusing() - } + if oldValue == true && newValue == false { + self.didCompleteFocusing() + } } currentCaptureInput = newInput @@ -333,9 +333,9 @@ class PhotoCapture: NSObject { } public func focus(with focusMode: AVCaptureDevice.FocusMode, - exposureMode: AVCaptureDevice.ExposureMode, - at devicePoint: CGPoint, - monitorSubjectAreaChange: Bool) { + exposureMode: AVCaptureDevice.ExposureMode, + at devicePoint: CGPoint, + monitorSubjectAreaChange: Bool) { sessionQueue.async { Logger.debug("focusMode: \(focusMode), exposureMode: \(exposureMode), devicePoint: \(devicePoint), monitorSubjectAreaChange:\(monitorSubjectAreaChange)") guard let device = self.captureDevice else { @@ -460,7 +460,7 @@ class PhotoCapture: NSObject { } let captureRect = captureOutputPhotoRect - delegate.photoCaptureDidStartPhotoCapture(self) + delegate.photoCaptureDidStart(self) sessionQueue.async { self.captureOutput.takePhoto(delegate: self, captureRect: captureRect) } @@ -516,7 +516,7 @@ class PhotoCapture: NSObject { throw PhotoCaptureError.invalidVideo } - self.delegate?.photoCaptureDidBeginMovie(self) + self.delegate?.photoCaptureDidBeginRecording(self) } }.catch { error in self.handleMovieCaptureError(error) @@ -555,7 +555,7 @@ class PhotoCapture: NSObject { // Inform UI that capture is stopping. self.lastMovieRecordingEndDate = movieRecordingStartDate - self.delegate?.photoCaptureDidCompleteMovie(self) + self.delegate?.photoCaptureDidFinishRecording(self) }.catch { error in self.handleMovieCaptureError(error) } @@ -573,7 +573,7 @@ class PhotoCapture: NSObject { self.shouldHaveAudioCapture = false } self.lastMovieRecordingEndDate = Date() - self.delegate?.photoCapture(self, processingDidError: error) + self.delegate?.photoCapture(self, didFailProcessing: error) } private func cancelMovieCapture() { @@ -592,7 +592,7 @@ class PhotoCapture: NSObject { // Inform UI that capture is stopping. self.lastMovieRecordingEndDate = Date() - self.delegate?.photoCaptureDidCancelMovie(self) + self.delegate?.photoCaptureDidCancelRecording(self) }.catch { error in self.handleMovieCaptureError(error) } @@ -668,24 +668,25 @@ extension PhotoCapture: VolumeButtonObserver { // MARK: - -extension PhotoCapture: CaptureButtonDelegate { - func didTapCaptureButton(_ captureButton: CaptureButton) { +extension PhotoCapture: CameraCaptureControlDelegate { + + func cameraCaptureControlDidRequestCapturePhoto(_ control: CameraCaptureControl) { takePhoto() } - func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) { + func cameraCaptureControlDidRequestStartVideoRecording(_ control: CameraCaptureControl) { beginMovieCapture() } - func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) { + func cameraCaptureControlDidRequestFinishVideoRecording(_ control: CameraCaptureControl) { completeMovieCapture() } - func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) { + func cameraCaptureControlDidRequestCancelVideoRecording(_ control: CameraCaptureControl) { cancelMovieCapture() } - func didPressStopCaptureButton(_ captureButton: CaptureButton) { + func didPressStopCaptureButton(_ control: CameraCaptureControl) { completeMovieCapture() } @@ -693,7 +694,7 @@ extension PhotoCapture: CaptureButtonDelegate { return delegate?.zoomScaleReferenceHeight } - func longPressCaptureButton(_ captureButton: CaptureButton, didUpdateZoomAlpha zoomAlpha: CGFloat) { + func cameraCaptureControl(_ control: CameraCaptureControl, didUpdate zoomAlpha: CGFloat) { updateZoom(alpha: zoomAlpha) } } @@ -730,12 +731,12 @@ extension PhotoCapture: CaptureOutputDelegate { switch photoData { case .failure(let error): - delegate.photoCapture(self, processingDidError: error) + delegate.photoCapture(self, didFailProcessing: error) case .success(let photoData): let dataSource = DataSourceValue.dataSource(with: photoData, utiType: kUTTypeJPEG as String) let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeJPEG as String) - delegate.photoCapture(self, didFinishProcessingAttachment: attachment) + delegate.photoCapture(self, didFinishProcessing: attachment) } } @@ -761,7 +762,7 @@ extension PhotoCapture: CaptureOutputDelegate { let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String) BenchEventComplete(eventId: "Movie Processing") - delegate.photoCapture(self, didFinishProcessingAttachment: attachment) + delegate.photoCapture(self, didFinishProcessing: attachment) } } @@ -1165,7 +1166,7 @@ class PhotoCaptureOutputAdaptee: NSObject, ImageCaptureOutput { photoSettings.isHighResolutionPhotoEnabled = true photoSettings.isAutoStillImageStabilizationEnabled = - photoOutput.isStillImageStabilizationSupported + photoOutput.isStillImageStabilizationSupported return photoSettings } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 55ec41721e..90e233ce07 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -1,17 +1,26 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // -import Foundation import AVFoundation +import Foundation import Lottie +import Photos +import UIKit +import SignalMessaging protocol PhotoCaptureViewControllerDelegate: AnyObject { - func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) + func photoCaptureViewControllerDidFinish(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewControllerDidTryToCaptureTooMany(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewControllerCanCaptureMoreItems(_ photoCaptureViewController: PhotoCaptureViewController) -> Bool - func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, isRecordingMovie: Bool) + func photoCaptureViewControllerDidRequestPresentPhotoLibrary(_ photoCaptureViewController: PhotoCaptureViewController) + func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didRequestSwitchBatchMode batchMode: Bool) -> Bool +} + +protocol PhotoCaptureViewControllerDataSource: AnyObject { + var numberOfMediaItems: Int { get } + func addMedia(attachment: SignalAttachment) } enum PhotoCaptureError: Error { @@ -38,12 +47,13 @@ extension PhotoCaptureError: LocalizedError, UserErrorDescriptionProvider { @objc class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate { - + weak var delegate: PhotoCaptureViewControllerDelegate? + weak var dataSource: PhotoCaptureViewControllerDataSource? var interactiveDismiss: PhotoCaptureInteractiveDismiss! - + @objc public lazy var photoCapture = PhotoCapture() - + lazy var tapToFocusView: AnimationView = { let view = AnimationView(name: "tap_to_focus") view.animationSpeed = 1 @@ -53,85 +63,135 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate view.setContentHuggingHigh() return view }() - + deinit { UIDevice.current.endGeneratingDeviceOrientationNotifications() photoCapture.stopCapture().done { Logger.debug("stopCapture completed") } } - + // MARK: - Overrides - + override func loadView() { - self.view = UIView() - self.view.backgroundColor = Theme.darkThemeBackgroundColor + view = UIView() + view.backgroundColor = Theme.darkThemeBackgroundColor + view.preservesSuperviewLayoutMargins = true + definesPresentationContext = true - + view.addSubview(previewView) - - previewView.autoPinEdgesToSuperviewEdges() - - view.addSubview(captureButton) if UIDevice.current.isIPad { - captureButton.autoVCenterInSuperview() - captureButton.centerXAnchor.constraint(equalTo: view.trailingAnchor, constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true - captureButton.movieLockView.autoSetDimension(.width, toSize: 120) - } else { - captureButton.autoHCenterInSuperview() - // we pin to edges rather than margin, because on notched devices the margin changes - // as the device rotates *EVEN THOUGH* the interface is locked to portrait. - captureButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, - constant: SendMediaNavigationController.bottomButtonsCenterOffset).isActive = true - captureButton.movieLockView.autoPinEdge(.trailing, to: .trailing, of: view, withOffset: -16) + previewView.autoPinEdgesToSuperviewEdges() } - + + view.addSubview(topBar) + topBar.mode = .cameraControls + topBar.autoPinWidthToSuperview() + topBarOffsetFromTop = topBar.autoPinEdge(toSuperviewEdge: .top) + + view.addSubview(bottomBar) + bottomBar.autoPinWidthToSuperview() + bottomBarOffsetFromBottom = bottomBar.autoPinEdge(toSuperviewEdge: .bottom) + + view.addSubview(cameraCaptureControl) + if UIDevice.current.isIPad { + // captureButton.autoVCenterInSuperview() + // captureButton.autoPinTrailing(toEdgeOf: view, offset: -captureButtonMargin) + // captureButton.movieLockView.autoSetDimension(.width, toSize: 120) + } else { + captureButtonVPositionConstraint = cameraCaptureControl.autoPinEdge(toSuperviewEdge: .bottom) + cameraCaptureControl.autoPinLeadingToSuperviewMargin() + cameraCaptureControl.autoPinTrailingToSuperviewMargin() + } + + view.addSubview(doneButton) + doneButton.isHidden = true + doneButton.autoPinTrailingToSuperviewMargin() + doneButton.centerYAnchor.constraint(equalTo: cameraCaptureControl.centerYAnchor).isActive = true + doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) + // If the view is already visible, setup the volume button listener // now that the capture UI is ready. Otherwise, we'll wait until // we're visible. if isVisible { VolumeButtons.shared?.addObserver(observer: photoCapture) } - + view.addSubview(tapToFocusView) tapToFocusView.isUserInteractionEnabled = false tapToFocusLeftConstraint = tapToFocusView.centerXAnchor.constraint(equalTo: view.leftAnchor) tapToFocusLeftConstraint.isActive = true tapToFocusTopConstraint = tapToFocusView.centerYAnchor.constraint(equalTo: view.topAnchor) tapToFocusTopConstraint.isActive = true - - view.addSubview(topBar) - topBar.autoPinWidthToSuperview() - topBarOffset = topBar.autoPinEdge(toSuperviewEdge: .top) - topBar.autoSetDimension(.height, toSize: 44) } - - var topBarOffset: NSLayoutConstraint! - + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + guard !UIDevice.current.isIPad else { + return + } + + // Clamp capture view to 16:9 + var previewFrame = view.bounds + var cornerRadius: CGFloat = 0 + let targetAspectRatio: CGFloat = 16/9 + let currentAspectRatio: CGFloat = previewFrame.height / previewFrame.width + + if abs(currentAspectRatio - targetAspectRatio) > 0.001 { + previewFrame.y = view.safeAreaInsets.top + previewFrame.height = previewFrame.width * targetAspectRatio + cornerRadius = 18 + } + previewView.frame = previewFrame + previewView.previewLayer.cornerRadius = cornerRadius + + // Bottom bar is pinned to the bottom of the screen, residing either directly above safe area + // or (for taller screens) floating in the center of the black area between the bottom of the capture view and safe area. + let bottomBarHeight = bottomBar.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .fittingSizeLevel).height + let blackBarHeight = max(0, view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom) + bottomBarOffsetFromBottom.constant = -(max(0, (blackBarHeight - bottomBarHeight) / 2) + view.safeAreaInsets.bottom) + + // Bottom edge of the capture button is 16pts above either bottom edge of the camera capture view + // or top of the bottom bar, whatever is higher. + let captureButtonBaseline = view.bounds.maxY - view.safeAreaInsets.bottom - max(blackBarHeight, bottomBarHeight) + captureButtonVPositionConstraint.constant = -(view.bounds.height - captureButtonBaseline + 16) + } + + private var topBarOffsetFromTop: NSLayoutConstraint! + private var bottomBarOffsetFromBottom: NSLayoutConstraint! + private var captureButtonVPositionConstraint: NSLayoutConstraint! + override func viewDidLoad() { super.viewDidLoad() - + setupPhotoCapture() - - updateNavigationItems() + updateFlashModeControl() - + view.addGestureRecognizer(pinchZoomGesture) view.addGestureRecognizer(tapToFocusGesture) view.addGestureRecognizer(doubleTapToSwitchCameraGesture) - + if let navController = self.navigationController { interactiveDismiss = PhotoCaptureInteractiveDismiss(viewController: navController) interactiveDismiss.interactiveDismissDelegate = self interactiveDismiss.addGestureRecognizer(to: view) } - + tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture) + + mediaPickerThumbnailButton.configure() } - + private var isVisible = false + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + isVisible = true let previewOrientation: AVCaptureVideoOrientation if UIDevice.current.isIPad { @@ -144,39 +204,44 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate updateIconOrientations(isAnimated: false, captureOrientation: previewOrientation) resumePhotoCapture() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + if hasCaptureStarted { BenchEventComplete(eventId: "Show-Camera") VolumeButtons.shared?.addObserver(observer: photoCapture) } UIApplication.shared.isIdleTimerDisabled = true } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + isVisible = false VolumeButtons.shared?.removeObserver(photoCapture) pausePhotoCapture() UIApplication.shared.isIdleTimerDisabled = false } - + override var prefersStatusBarHidden: Bool { guard !CurrentAppContext().hasActiveCall else { return false } - return true } - - override var prefersHomeIndicatorAutoHidden: Bool { - return true + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent } - + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - + if UIDevice.current.isIPad { // Since we support iPad multitasking, we cannot *disable* rotation of our views. // Rotating the preview layer is really distracting, so we fade out the preview layer @@ -189,176 +254,209 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } } } - + override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() - if !UIDevice.current.isIPad { - // we pin to a constant rather than margin, because on notched devices the - // safeAreaInsets/margins change as the device rotates *EVEN THOUGH* the interface - // is locked to portrait. - // Only grab this once -- otherwise when we swipe to dismiss this is updated and the top bar jumps to having zero offset - if topBarOffset.constant == 0 { - topBarOffset.constant = max(view.safeAreaInsets.top, view.safeAreaInsets.left, view.safeAreaInsets.bottom) - } + + // we pin to a constant rather than margin, because on notched devices the + // safeAreaInsets/margins change as the device rotates *EVEN THOUGH* the interface + // is locked to portrait. + // Only grab this once -- otherwise when we swipe to dismiss this is updated and the top bar jumps to having zero offset + if topBarOffsetFromTop.constant == 0 { + let maxInsetDimension = max(view.safeAreaInsets.top, view.safeAreaInsets.left, view.safeAreaInsets.bottom) + topBarOffsetFromTop.constant = max(maxInsetDimension, previewView.frame.minY) } } - + + // MARK: - Interactive Dismiss + func interactiveDismissDidBegin(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { } + func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { dismiss(animated: true) } + func interactiveDismissDidCancel(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { } - - // MARK: - - var isRecordingMovie: Bool = false - + + // MARK: - Top Bar + + private var isRecordingVideo: Bool = false { + didSet { + if isRecordingVideo { + topBar.mode = .videoRecording + topBar.recordingTimerView.startCounting() + + cameraCaptureControl.setState(.recording, animationDuration: 0.4) + } else { + topBar.mode = .cameraControls + topBar.recordingTimerView.stopCounting() + + cameraCaptureControl.setState(.initial, animationDuration: 0.2) + } + + doneButton.isHidden = isRecordingVideo || doneButton.badgeNumber == 0 + bottomBar.isHidden = isRecordingVideo + } + } + private var isInBatchMode: Bool = false + private class TopBar: UIView { - let recordingTimerView = RecordingTimerView() - let navStack: UIStackView - + let recordingTimerView = RecordingTimerView(frame: .zero) + private let navStack: UIStackView + init(navbarItems: [UIView]) { self.navStack = UIStackView(arrangedSubviews: navbarItems) - navStack.spacing = 16 - + super.init(frame: .zero) - + + layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4) + addSubview(navStack) - navStack.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 16)) - + navStack.spacing = 16 + navStack.autoPinEdgesToSuperviewMargins() + addSubview(recordingTimerView) - recordingTimerView.isHidden = true - recordingTimerView.autoCenterInSuperview() + recordingTimerView.autoPinHeightToSuperview(withMargin: 8) + recordingTimerView.autoHCenterInSuperview() } - - required init?(coder aDecoder: NSCoder) { + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + enum Mode { - case navigation, recordingMovie + case cameraControls, videoRecording } - - var mode: Mode = .navigation { + + var mode: Mode = .cameraControls { didSet { switch mode { - case .recordingMovie: + case .videoRecording: navStack.isHidden = true - recordingTimerView.sizeToFit() recordingTimerView.isHidden = false - case .navigation: + case .cameraControls: navStack.isHidden = false recordingTimerView.isHidden = true } } } } - + private lazy var topBar: TopBar = { - let dismissButton: UIButton - if UIDevice.current.isIPad { - dismissButton = OWSButton.shadowedCancelButton { [weak self] in - self?.didTapClose() - } - dismissButton.contentEdgeInsets = UIEdgeInsets(top: 7, leading: 20, bottom: 6, trailing: 20) - } else { - dismissButton = dismissControl.button - dismissButton.contentEdgeInsets = UIEdgeInsets(top: 1, leading: 16, bottom: 6, trailing: 20) - } - - return TopBar(navbarItems: [dismissButton, + return TopBar(navbarItems: [dismissControl, UIView.hStretchingSpacer(), - switchCameraControl.button, - flashModeControl.button]) + batchModeControl, + flashModeControl]) }() - - func updateNavigationItems() { - if isRecordingMovie { - topBar.mode = .recordingMovie - } else { - topBar.mode = .navigation + + // MARK: - Bottom Bar + + private class BottomBar: UIView { + let buttonStack: UIStackView + + init(items: [UIView]) { + self.buttonStack = UIStackView(arrangedSubviews: items) + + super.init(frame: .zero) + + layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) + + addSubview(buttonStack) + buttonStack.spacing = 16 + buttonStack.autoPinEdgesToSuperviewMargins() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") } } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return .portrait + + private lazy var bottomBar: BottomBar = { + return BottomBar(items: [mediaPickerThumbnailButton.embeddedInContainerView(layoutMargins: UIEdgeInsets(margin: 4)), + UIView.hStretchingSpacer(), + switchCameraControl]) + }() + + func updateBottomBarItems () { + } - + // MARK: - Views - - let captureButton = CaptureButton() - - var previewView: CapturePreviewView { + + private let cameraCaptureControl = CameraCaptureControl() + + private var previewView: CapturePreviewView { return photoCapture.previewView } - - class PhotoControl { - let button: OWSButton - - init(imageName: String, block: @escaping () -> Void) { - self.button = OWSButton(imageName: imageName, tintColor: .ows_white, block: block) - button.setCompressionResistanceHigh() - button.layer.shadowOffset = CGSize.zero - button.layer.shadowOpacity = 0.35 - button.layer.shadowRadius = 4 - button.contentEdgeInsets = UIEdgeInsets(top: 6, leading: 4, bottom: 0, trailing: 4) - } - - func setImage(imageName: String) { - button.setImage(imageName: imageName) - } - } - + + private lazy var mediaPickerThumbnailButton: MediaPickerThumbnailButton = { + let button = MediaPickerThumbnailButton(frame: .zero) + button.addTarget(self, action: #selector(didTapPhotoLibrary), for: .touchUpInside) + return button + }() + private lazy var dismissControl: PhotoControl = { - return PhotoControl(imageName: "ic_x_with_shadow") { [weak self] in + return PhotoControl(imageName: "media-composer-close") { [weak self] in self?.didTapClose() } }() - + private lazy var switchCameraControl: PhotoControl = { - return PhotoControl(imageName: "ic_switch_camera") { [weak self] in + return PhotoControl(imageName: "media-composer-switch-camera-24") { [weak self] in self?.didTapSwitchCamera() } }() - + private lazy var flashModeControl: PhotoControl = { - return PhotoControl(imageName: "ic_flash_mode_auto") { [weak self] in + return PhotoControl(imageName: "media-composer-flash-auto-24") { [weak self] in self?.didTapFlashMode() } }() - + + private lazy var batchModeControl: PhotoControl = { + let control = PhotoControl(imageName: "media-composer-create-album-outline-24") { [weak self] in + self?.didTapBatchMode() + } + return control + }() + + private lazy var doneButton: MediaDoneButton = { + let button = MediaDoneButton(type: .custom) + button.badgeNumber = 0 + return button + }() + lazy var pinchZoomGesture: UIPinchGestureRecognizer = { return UIPinchGestureRecognizer(target: self, action: #selector(didPinchZoom(pinchGesture:))) }() - + lazy var tapToFocusGesture: UITapGestureRecognizer = { return UITapGestureRecognizer(target: self, action: #selector(didTapFocusExpose(tapGesture:))) }() - + lazy var doubleTapToSwitchCameraGesture: UITapGestureRecognizer = { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTapToSwitchCamera(tapGesture:))) tapGesture.numberOfTapsRequired = 2 return tapGesture }() - + // MARK: - Events - + @objc func didTapClose() { - self.delegate?.photoCaptureViewControllerDidCancel(self) + delegate?.photoCaptureViewControllerDidCancel(self) } - + @objc func didTapSwitchCamera() { - Logger.debug("") switchCamera() } - + @objc func didDoubleTapToSwitchCamera(tapGesture: UITapGestureRecognizer) { - Logger.debug("") - guard !isRecordingMovie else { + guard !isRecordingVideo else { // - Orientation gets out of sync when switching cameras mid movie. // - Audio gets out of sync when switching cameras mid movie // https://stackoverflow.com/questions/13951182/audio-video-out-of-sync-after-switch-camera @@ -366,20 +464,19 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } switchCamera() } - + private func switchCamera() { UIView.animate(withDuration: 0.2) { let epsilonToForceCounterClockwiseRotation: CGFloat = 0.00001 - self.switchCameraControl.button.transform = self.switchCameraControl.button.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation) + self.switchCameraControl.transform = self.switchCameraControl.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation) } photoCapture.switchCamera().catch { error in self.showFailureUI(error: error) } } - + @objc func didTapFlashMode() { - Logger.debug("") firstly { photoCapture.switchFlashMode() }.done { @@ -388,7 +485,27 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate owsFailDebug("Error: \(error)") } } - + + @objc + func didTapBatchMode() { + guard let delegate = delegate else { + return + } + isInBatchMode = delegate.photoCaptureViewController(self, didRequestSwitchBatchMode: !isInBatchMode) + let imageName = isInBatchMode ? "media-composer-create-album-solid-24" : "media-composer-create-album-outline-24" + batchModeControl.setImage(imageName: imageName) + } + + @objc + func didTapPhotoLibrary() { + delegate?.photoCaptureViewControllerDidRequestPresentPhotoLibrary(self) + } + + @objc + func didTapDoneButton() { + delegate?.photoCaptureViewControllerDidFinish(self) + } + @objc func didPinchZoom(pinchGesture: UIPinchGestureRecognizer) { switch pinchGesture.state { @@ -401,40 +518,40 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate break } } - + @objc func didTapFocusExpose(tapGesture: UITapGestureRecognizer) { let viewLocation = tapGesture.location(in: view) let devicePoint = previewView.previewLayer.captureDevicePointConverted(fromLayerPoint: viewLocation) - + photoCapture.focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true) - // If the user taps near the capture button, it's more likely a mis-tap than intentional. - // Skip the focus animation in that case, since it looks bad. - let captureButtonOrigin = captureButton.superview!.convert(captureButton.frame.origin, to: view) - if UIDevice.current.isIPad { - guard viewLocation.x < captureButtonOrigin.x else { - Logger.verbose("Skipping animation for right edge on iPad") - - // Finish any outstanding focus animation, otherwise it will remain in an - // uncompleted state. - if let lastUserFocusTapPoint = lastUserFocusTapPoint { - completeFocusAnimation(forFocusPoint: lastUserFocusTapPoint) - } - return - } - } else { - guard viewLocation.y < captureButtonOrigin.y else { - Logger.verbose("Skipping animation for bottom row on iPhone") - - // Finish any outstanding focus animation, otherwise it will remain in an - // uncompleted state. - if let lastUserFocusTapPoint = lastUserFocusTapPoint { - completeFocusAnimation(forFocusPoint: lastUserFocusTapPoint) - } - return - } - } +// // If the user taps near the capture button, it's more likely a mis-tap than intentional. +// // Skip the focus animation in that case, since it looks bad. +// let captureButtonOrigin = captureButton.superview!.convert(captureButton.frame.origin, to: view) +// if UIDevice.current.isIPad { +// guard viewLocation.x < captureButtonOrigin.x else { +// Logger.verbose("Skipping animation for right edge on iPad") +// +// // Finish any outstanding focus animation, otherwise it will remain in an +// // uncompleted state. +// if let lastUserFocusTapPoint = lastUserFocusTapPoint { +// completeFocusAnimation(forFocusPoint: lastUserFocusTapPoint) +// } +// return +// } +// } else { +// guard viewLocation.y < captureButtonOrigin.y else { +// Logger.verbose("Skipping animation for bottom row on iPhone") +// +// // Finish any outstanding focus animation, otherwise it will remain in an +// // uncompleted state. +// if let lastUserFocusTapPoint = lastUserFocusTapPoint { +// completeFocusAnimation(forFocusPoint: lastUserFocusTapPoint) +// } +// return +// } +// } lastUserFocusTapPoint = devicePoint do { @@ -444,46 +561,44 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate startFocusAnimation() } } - + // MARK: - Focus Animations - + var tapToFocusLeftConstraint: NSLayoutConstraint! var tapToFocusTopConstraint: NSLayoutConstraint! func positionTapToFocusView(center: CGPoint) { tapToFocusLeftConstraint.constant = center.x tapToFocusTopConstraint.constant = center.y } - + func startFocusAnimation() { tapToFocusView.stop() tapToFocusView.play(fromProgress: 0.0, toProgress: 0.9) } - + var lastUserFocusTapPoint: CGPoint? func completeFocusAnimation(forFocusPoint focusPoint: CGPoint) { guard let lastUserFocusTapPoint = lastUserFocusTapPoint else { return } - + guard lastUserFocusTapPoint.within(0.005, of: focusPoint) else { Logger.verbose("focus completed for obsolete focus point. User has refocused.") return } - + tapToFocusView.play(toProgress: 1.0) } - + // MARK: - Orientation - - // MARK: - - + private func updateIconOrientations(isAnimated: Bool, captureOrientation: AVCaptureVideoOrientation) { guard !UIDevice.current.isIPad else { return } - + Logger.verbose("captureOrientation: \(captureOrientation)") - + let transformFromOrientation: CGAffineTransform switch captureOrientation { case .portrait: @@ -498,38 +613,40 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate owsFailDebug("unexpected captureOrientation: \(captureOrientation.rawValue)") transformFromOrientation = .identity } - + // Don't "unrotate" the switch camera icon if the front facing camera had been selected. let tranformFromCameraType: CGAffineTransform = photoCapture.desiredPosition == .front ? CGAffineTransform(rotationAngle: -.pi) : .identity - + let updateOrientation = { - self.flashModeControl.button.transform = transformFromOrientation - self.switchCameraControl.button.transform = transformFromOrientation.concatenating(tranformFromCameraType) + self.flashModeControl.transform = transformFromOrientation + self.switchCameraControl.transform = transformFromOrientation.concatenating(tranformFromCameraType) } - + if isAnimated { UIView.animate(withDuration: 0.3, animations: updateOrientation) } else { updateOrientation() } } - + + // MARK: - Photo Capture + var hasCaptureStarted = false - + private func captureReady() { - self.hasCaptureStarted = true - BenchEventComplete(eventId: "Show-Camera") + self.hasCaptureStarted = true + BenchEventComplete(eventId: "Show-Camera") } - + private func setupPhotoCapture() { photoCapture.delegate = self - captureButton.delegate = photoCapture - + cameraCaptureControl.delegate = photoCapture + // If the session is already running, we're good to go. guard !photoCapture.session.isRunning else { return self.captureReady() } - + firstly { photoCapture.prepareVideoCapture() }.catch { [weak self] error in @@ -537,7 +654,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self.showFailureUI(error: error) } } - + private func pausePhotoCapture() { guard photoCapture.session.isRunning else { return } firstly { @@ -548,7 +665,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self?.showFailureUI(error: error) } } - + private func resumePhotoCapture() { guard !photoCapture.session.isRunning else { return } firstly { @@ -559,66 +676,72 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self?.showFailureUI(error: error) } } - + private func showFailureUI(error: Error) { Logger.error("error: \(error)") - + OWSActionSheets.showActionSheet(title: nil, message: error.userErrorDescription, buttonTitle: CommonStrings.dismissButton, buttonAction: { [weak self] _ in self?.dismiss(animated: true) }) } - + private func updateFlashModeControl() { let imageName: String switch photoCapture.flashMode { case .auto: - imageName = "ic_flash_mode_auto" + imageName = "media-composer-flash-auto-24" case .on: - imageName = "ic_flash_mode_on" + imageName = "media-composer-flash-filled-24" case .off: - imageName = "ic_flash_mode_off" + imageName = "media-composer-flash-outline-24" @unknown default: owsFailDebug("unexpected photoCapture.flashMode: \(photoCapture.flashMode.rawValue)") - - imageName = "ic_flash_mode_auto" + + imageName = "media-composer-flash-auto-24" } - + self.flashModeControl.setImage(imageName: imageName) } } extension PhotoCaptureViewController: PhotoCaptureDelegate { - + // MARK: - Photo - - func photoCaptureDidStartPhotoCapture(_ photoCapture: PhotoCapture) { + + func photoCaptureDidStart(_ photoCapture: PhotoCapture) { let captureFeedbackView = UIView() 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() } } - - func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment) { - delegate?.photoCaptureViewController(self, didFinishProcessingAttachment: attachment) + + func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessing attachment: SignalAttachment) { + dataSource?.addMedia(attachment: attachment) + + if isInBatchMode, let badgeNumber = dataSource?.numberOfMediaItems { + doneButton.badgeNumber = badgeNumber + doneButton.isHidden = false + } else { + doneButton.isHidden = true + + delegate?.photoCaptureViewControllerDidFinish(self) + } } - - func photoCapture(_ photoCapture: PhotoCapture, processingDidError error: Error) { - isRecordingMovie = false - topBar.recordingTimerView.stopCounting() - updateNavigationItems() - delegate?.photoCaptureViewController(self, isRecordingMovie: isRecordingMovie) - + + func photoCapture(_ photoCapture: PhotoCapture, didFailProcessing error: Error) { + isRecordingVideo = false + if case PhotoCaptureError.invalidVideo = error { // Don't show an error if the user aborts recording before video // recording has begun. @@ -626,301 +749,146 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { } showFailureUI(error: error) } - + func photoCaptureCanCaptureMoreItems(_ photoCapture: PhotoCapture) -> Bool { - guard let delegate = delegate else { return false } - return delegate.photoCaptureViewControllerCanCaptureMoreItems(self) + return delegate?.photoCaptureViewControllerCanCaptureMoreItems(self) ?? false } - + func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture) { delegate?.photoCaptureViewControllerDidTryToCaptureTooMany(self) } - - // MARK: - Movie - - func photoCaptureDidBeginMovie(_ photoCapture: PhotoCapture) { - isRecordingMovie = true - updateNavigationItems() - topBar.recordingTimerView.startCounting() - delegate?.photoCaptureViewController(self, isRecordingMovie: isRecordingMovie) + + // MARK: - Video + + func photoCaptureDidBeginRecording(_ photoCapture: PhotoCapture) { + isRecordingVideo = true } - - func photoCaptureDidCompleteMovie(_ photoCapture: PhotoCapture) { - isRecordingMovie = false - topBar.recordingTimerView.stopCounting() - updateNavigationItems() - delegate?.photoCaptureViewController(self, isRecordingMovie: isRecordingMovie) + + func photoCaptureDidFinishRecording(_ photoCapture: PhotoCapture) { + isRecordingVideo = false } - - func photoCaptureDidCancelMovie(_ photoCapture: PhotoCapture) { - isRecordingMovie = false - topBar.recordingTimerView.stopCounting() - updateNavigationItems() - delegate?.photoCaptureViewController(self, isRecordingMovie: isRecordingMovie) + + func photoCaptureDidCancelRecording(_ photoCapture: PhotoCapture) { + isRecordingVideo = false } - + // MARK: - - + var zoomScaleReferenceHeight: CGFloat? { return view.bounds.height } - + func beginCaptureButtonAnimation(_ duration: TimeInterval) { - captureButton.beginRecordingAnimation(duration: duration) + cameraCaptureControl.setState(.recording, animationDuration: duration) } - + func endCaptureButtonAnimation(_ duration: TimeInterval) { - captureButton.endRecordingAnimation(duration: duration) + cameraCaptureControl.setState(.initial, animationDuration: duration) } - + func photoCapture(_ photoCapture: PhotoCapture, didChangeOrientation orientation: AVCaptureVideoOrientation) { updateIconOrientations(isAnimated: true, captureOrientation: orientation) if UIDevice.current.isIPad { photoCapture.updateVideoPreviewConnection(toOrientation: orientation) } } - - func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusingAtPoint focusPoint: CGPoint) { + + func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusing focusPoint: CGPoint) { completeFocusAnimation(forFocusPoint: focusPoint) } } // MARK: - Views -protocol CaptureButtonDelegate: AnyObject { - // MARK: Photo - func didTapCaptureButton(_ captureButton: CaptureButton) - - // MARK: Video - func didBeginLongPressCaptureButton(_ captureButton: CaptureButton) - func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) - func didCancelLongPressCaptureButton(_ captureButton: CaptureButton) - func didPressStopCaptureButton(_ captureButton: CaptureButton) - - var zoomScaleReferenceHeight: CGFloat? { get } - func longPressCaptureButton(_ captureButton: CaptureButton, didUpdateZoomAlpha zoomAlpha: CGFloat) -} - -extension CaptureButton: MovieLockViewDelegate { - func videoLockViewDidTapStop(_ videoLockView: MovieLockView) { - assert(movieLockView.isLocked) - movieLockView.unlock(isAnimated: true) - UIView.animate(withDuration: 0.2) { - self.movieLockView.alpha = 0 - } - delegate?.didPressStopCaptureButton(self) +private class PhotoControl: UIView { + let button: OWSButton + + private static let visibleButtonSize: CGFloat = 36 // both height and width + private static let layoutMargin: CGFloat = 4 // both horizontal and vertical + + init(imageName: String, block: @escaping () -> Void) { + self.button = OWSButton(imageName: imageName, tintColor: .ows_white, block: block) + + super.init(frame: CGRect(origin: .zero, size: CGSize(square: Self.visibleButtonSize + 2*Self.layoutMargin))) + + layoutMargins = UIEdgeInsets(margin: Self.layoutMargin) + + let blurView = CircleBlurView(effect: UIBlurEffect(style: .dark)) + addSubview(blurView) + blurView.autoPinEdgesToSuperviewMargins() + + addSubview(button) + button.autoPinEdgesToSuperviewMargins() } -} - -class CaptureButton: UIView { - - let innerButton = CircleView() - let movieLockView = MovieLockView(swipeDirectionToLock: UIDevice.current.isIPad ? .leading : .trailing) - - var longPressGesture: UILongPressGestureRecognizer! - let longPressDuration = 0.5 - - let zoomIndicator = CircleView() - - weak var delegate: CaptureButtonDelegate? - - let defaultDiameter: CGFloat = min(ScaleFromIPhone5To7Plus(60, 80), 80) - static let recordingDiameter: CGFloat = min(ScaleFromIPhone5To7Plus(68, 120), 120) - var innerButtonSizeConstraints: [NSLayoutConstraint]! - var zoomIndicatorSizeConstraints: [NSLayoutConstraint]! - - override init(frame: CGRect) { - super.init(frame: frame) - - // 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 = 0 - innerButton.addGestureRecognizer(longPressGesture) - - addSubview(innerButton) - innerButtonSizeConstraints = autoSetDimensions(to: CGSize(square: defaultDiameter)) - innerButton.backgroundColor = UIColor.ows_white.withAlphaComponent(0.33) - innerButton.layer.shadowOffset = .zero - innerButton.layer.shadowOpacity = 0.33 - innerButton.layer.shadowRadius = 2 - innerButton.autoPinEdgesToSuperviewEdges() - - addSubview(zoomIndicator) - zoomIndicatorSizeConstraints = zoomIndicator.autoSetDimensions(to: CGSize(square: defaultDiameter)) - zoomIndicator.isUserInteractionEnabled = false - zoomIndicator.layer.borderColor = UIColor.ows_white.cgColor - zoomIndicator.layer.borderWidth = 1.5 - zoomIndicator.autoAlignAxis(.horizontal, toSameAxisOf: innerButton) - zoomIndicator.autoAlignAxis(.vertical, toSameAxisOf: innerButton) - - addSubview(movieLockView) - movieLockView.autoSetDimension(.height, toSize: 50) - movieLockView.stopButton.autoAlignAxis(.horizontal, toSameAxisOf: self) - movieLockView.stopButton.autoAlignAxis(.vertical, toSameAxisOf: self) - movieLockView.alpha = 0 - movieLockView.delegate = self - } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - 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 - ) + + override var intrinsicContentSize: CGSize { + return CGSize(width: Self.visibleButtonSize + layoutMargins.leading + layoutMargins.trailing, + height: Self.visibleButtonSize + layoutMargins.top + layoutMargins.bottom) } - - 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 - ) + + func setImage(imageName: String) { + button.setImage(imageName: imageName) } +} - // MARK: - Gestures - - var initialTouchLocation: CGPoint? - var touchTimer: Timer? - var isLongPressing = false - - @objc - func didLongPress(_ gesture: UILongPressGestureRecognizer) { - guard let gestureView = gesture.view else { - owsFailDebug("gestureView was unexpectedly nil") - return - } - - switch gesture.state { - case .possible: break - case .began: - guard !movieLockView.isLocked else { - return - } - - initialTouchLocation = gesture.location(in: gesture.view) - beginRecordingAnimation(duration: 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.movieLockView.unlock(isAnimated: false) - UIView.animate(withDuration: 0.2) { - self.movieLockView.alpha = 1 - } - self.delegate?.didBeginLongPressCaptureButton(self) - } - case .changed: - guard isLongPressing else { break } - - guard let referenceHeight = delegate?.zoomScaleReferenceHeight else { - owsFailDebug("referenceHeight was unexpectedly nil") - return - } - - guard referenceHeight > 0 else { - owsFailDebug("referenceHeight was unexpectedly <= 0") - return - } - - guard let initialTouchLocation = initialTouchLocation else { - owsFailDebug("initialTouchLocation was unexpectedly nil") - return - } - - let currentLocation = gesture.location(in: gestureView) - - // Zoom - let minDistanceBeforeActivatingZoom: CGFloat = 30 - let yDistance = initialTouchLocation.y - currentLocation.y - minDistanceBeforeActivatingZoom - let distanceForFullZoom = referenceHeight / 4 - let yRatio = yDistance / distanceForFullZoom - let yAlpha = yRatio.clamp(0, 1) - - let zoomIndicatorDiameter = CGFloatLerp(type(of: self).recordingDiameter, 3, yAlpha) - self.zoomIndicatorSizeConstraints.forEach { $0.constant = zoomIndicatorDiameter } - zoomIndicator.superview?.layoutIfNeeded() - - delegate?.longPressCaptureButton(self, didUpdateZoomAlpha: yAlpha) - - // Lock - - guard !movieLockView.isLocked else { - return - } - let xOffset = currentLocation.x - initialTouchLocation.x - movieLockView.update(xOffset: xOffset) - case .ended: - endRecordingAnimation(duration: 0.2) - touchTimer?.invalidate() - touchTimer = nil - - guard !movieLockView.isLocked else { - return - } - - if isLongPressing { - UIView.animate(withDuration: 0.2) { - self.movieLockView.alpha = 0 - } - delegate?.didCompleteLongPressCaptureButton(self) - } else { - delegate?.didTapCaptureButton(self) - } - case .cancelled, .failed: - endRecordingAnimation(duration: 0.2) - - if isLongPressing { - self.movieLockView.unlock(isAnimated: true) - UIView.animate(withDuration: 0.2) { - self.movieLockView.alpha = 0 - } - delegate?.didCancelLongPressCaptureButton(self) - } - - touchTimer?.invalidate() - touchTimer = nil - @unknown default: - owsFailDebug("unexpected gesture state: \(gesture.state.rawValue)") +private class MediaPickerThumbnailButton: UIButton { + + private static let visibleSize = CGSize(square: 36) + + func configure() { + layer.cornerRadius = 10 + layer.borderWidth = 1.5 + layer.borderColor = UIColor.ows_whiteAlpha80.cgColor + clipsToBounds = true + + let placeholderView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + insertSubview(placeholderView, at: 0) + placeholderView.autoPinEdgesToSuperviewEdges() + + // Async Fetch last image + DispatchQueue.global(qos: .userInteractive).async { + let fetchOptions = PHFetchOptions() + fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + fetchOptions.fetchLimit = 1 + + let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions) + if fetchResult.count > 0, let asset = fetchResult.firstObject { + let targetImageSize = MediaPickerThumbnailButton.visibleSize + PHImageManager.default().requestImage(for: asset, targetSize: targetImageSize, contentMode: .aspectFill, options: nil) { (image, _) in + DispatchQueue.main.async { + self.setImage(image, for: .normal) + placeholderView.alpha = 0 + } + } + } } } + + override var intrinsicContentSize: CGSize { + return Self.visibleSize + } } class CapturePreviewView: UIView { - + let previewLayer: AVCaptureVideoPreviewLayer - + override var bounds: CGRect { didSet { previewLayer.frame = bounds } } - + + override var frame: CGRect { + didSet { + previewLayer.frame = bounds + } + } + override var contentMode: UIView.ContentMode { set { switch newValue { @@ -948,7 +916,7 @@ class CapturePreviewView: UIView { } } } - + init(session: AVCaptureSession) { previewLayer = AVCaptureVideoPreviewLayer(session: session) if Platform.isSimulator { @@ -960,66 +928,59 @@ class CapturePreviewView: UIView { previewLayer.frame = bounds layer.addSublayer(previewLayer) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } -class RecordingTimerView: UIView { - - let stackViewSpacing: CGFloat = 4 - +private class RecordingTimerView: PillView { + override init(frame: CGRect) { super.init(frame: frame) - + + layoutMargins = UIEdgeInsets(hMargin: 16, vMargin: 0) + + let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + addSubview(backgroundView) + backgroundView.autoPinEdgesToSuperviewEdges() + let stackView = UIStackView(arrangedSubviews: [icon, label]) stackView.axis = .horizontal stackView.alignment = .center - stackView.spacing = stackViewSpacing - + stackView.spacing = 5 addSubview(stackView) stackView.autoPinEdgesToSuperviewMargins() - + updateView() } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Subviews - + private lazy var label: UILabel = { let label = UILabel() label.font = UIFont.ows_monospacedDigitFont(withSize: 20) label.textAlignment = .center label.textColor = UIColor.white - label.layer.shadowOffset = CGSize.zero - label.layer.shadowOpacity = 0.35 - label.layer.shadowRadius = 4 - return label }() - - static let iconWidth: CGFloat = 6 - + private let icon: UIView = { let icon = CircleView() - icon.layer.shadowOffset = CGSize.zero - icon.layer.shadowOpacity = 0.35 - icon.layer.shadowRadius = 4 - icon.backgroundColor = .red - icon.autoSetDimensions(to: CGSize(square: iconWidth)) + icon.autoSetDimensions(to: CGSize(square: 6)) icon.alpha = 0 - return icon }() - + // MARK: - + var recordingStartTime: TimeInterval? - + func startCounting() { recordingStartTime = CACurrentMediaTime() timer = Timer.weakScheduledTimer(withTimeInterval: 0.1, target: self, selector: #selector(updateView), userInfo: nil, repeats: true) @@ -1028,7 +989,7 @@ class RecordingTimerView: UIView { options: [.autoreverse, .repeat], animations: { self.icon.alpha = 1 }) } - + func stopCounting() { timer?.invalidate() timer = nil @@ -1038,28 +999,28 @@ class RecordingTimerView: UIView { } label.text = nil } - + // MARK: - - + private var timer: Timer? - + private lazy var timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "mm:ss" formatter.timeZone = TimeZone(identifier: "UTC")! - + return formatter }() - + // This method should only be called when the call state is "connected". var recordingDuration: TimeInterval { guard let recordingStartTime = recordingStartTime else { return 0 } - + return CACurrentMediaTime() - recordingStartTime } - + @objc private func updateView() { let recordingDuration = self.recordingDuration @@ -1068,164 +1029,16 @@ class RecordingTimerView: UIView { } } -// MARK: Movie Lock - -protocol MovieLockViewDelegate: AnyObject { - func videoLockViewDidTapStop(_ videoLockView: MovieLockView) -} - -@objc -public class MovieLockView: UIView { - - weak var delegate: MovieLockViewDelegate? - - public enum SwipeDirection { - case trailing - case leading - } - - public let swipeDirectionToLock: SwipeDirection - - public init(swipeDirectionToLock: SwipeDirection) { - self.swipeDirectionToLock = swipeDirectionToLock - super.init(frame: .zero) - - addSubview(stopButton) - stopButton.autoVCenterInSuperview() - stopButton.alpha = 0 - - addSubview(highlightView) - highlightView.autoVCenterInSuperview() - highlightView.alpha = 0 - - addSubview(lockIconView) - lockIconView.autoVCenterInSuperview() - - let trailingView: UIView - let leadingView: UIView - switch swipeDirectionToLock { - case .trailing: - trailingView = lockIconView - leadingView = stopButton - highlightEdgeConstraint = highlightView.autoPinEdge(toSuperviewEdge: .leading) - case .leading: - trailingView = stopButton - leadingView = lockIconView - highlightEdgeConstraint = highlightView.autoPinEdge(toSuperviewEdge: .trailing) - } - - trailingView.centerXAnchor.constraint(equalTo: trailingAnchor, - constant: -highlightViewWidth/2).isActive = true - leadingView.centerXAnchor.constraint(equalTo: leadingAnchor, - constant: highlightViewWidth/2).isActive = true - - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func update(xOffset: CGFloat) { - let effectiveDistance: CGFloat - let distanceToLock: CGFloat - let highlightOffset: CGFloat - switch swipeDirectionToLock { - case .trailing: - let minDistanceBeforeActivatingLockSlider: CGFloat = 30 - effectiveDistance = xOffset - minDistanceBeforeActivatingLockSlider - distanceToLock = frame.width - highlightView.frame.width - highlightOffset = effectiveDistance.clamp(0, distanceToLock) - case .leading: - // On iPad, the gesture already feels right, without applying the additional - // minDistanceBeforeActivatingLockSlider padding. - effectiveDistance = xOffset - distanceToLock = -1 * (frame.width - highlightView.frame.width) - highlightOffset = effectiveDistance.clamp(distanceToLock, 0) - } - highlightEdgeConstraint.constant = highlightOffset - - let alpha = (effectiveDistance/distanceToLock).clamp(0, 1) - highlightView.alpha = alpha - - if alpha == 1.0 { - lock(isAnimated: true) - } - Logger.verbose("xOffset: \(xOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), highlightOffset: \(highlightOffset), alpha: \(alpha)") - } - - // MARK: - - - private(set) var isLocked = false - - public func unlock(isAnimated: Bool) { - Logger.debug("") - guard isLocked else { - Logger.debug("ignoring redundant request") - return - } - Logger.debug("unlocking") - - isLocked = false - let changes = { - self.lockIconView.tintColor = .white - self.stopButton.alpha = 0 - self.highlightView.alpha = 0 - } - - if isAnimated { - UIView.animate(withDuration: 0.2, animations: changes) - } else { - changes() - } - } - - private func lock(isAnimated: Bool) { - guard !isLocked else { - Logger.debug("ignoring redundant request") - return - } - Logger.debug("locking") - - isLocked = true - let changes = { - self.lockIconView.tintColor = .black - self.stopButton.alpha = 1.0 - } - - if isAnimated { - UIView.animate(withDuration: 0.2, animations: changes) - } else { - changes() - } - } - - // MARK: - Subviews - - let lockIconWidth: CGFloat = 24 - private lazy var lockIconView: UIImageView = { - let imageView = UIImageView.withTemplateImage(#imageLiteral(resourceName: "ic_lock_outline"), tintColor: .white) - imageView.autoSetDimensions(to: CGSize(square: lockIconWidth)) - return imageView - }() - - let highlightViewWidth = SendMediaNavigationController.bottomButtonWidth - private var highlightEdgeConstraint: NSLayoutConstraint! - private lazy var highlightView: UIView = { - let view = CircleView(diameter: highlightViewWidth) - view.backgroundColor = .white - return view - }() - - let stopButtonWidth: CGFloat = 30 - public lazy var stopButton: UIButton = { - let view = OWSButton { [weak self] in - guard let self = self else { return } - self.delegate?.videoLockViewDidTapStop(self) - } - - view.backgroundColor = .white - view.autoSetDimensions(to: CGSize(square: stopButtonWidth)) - - return view - }() +private extension UIView { + + func embeddedInContainerView(layoutMargins: UIEdgeInsets = .zero) -> UIView { + var containerViewFrame = bounds + containerViewFrame.width += layoutMargins.leading + layoutMargins.trailing + containerViewFrame.height += layoutMargins.top + layoutMargins.bottom + let containerView = UIView(frame: containerViewFrame) + containerView.layoutMargins = layoutMargins + containerView.addSubview(self) + autoPinEdgesToSuperviewMargins() + return containerView + } } diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index e476c8bd18..294a2ba544 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -38,23 +38,8 @@ class CameraFirstCaptureNavigationController: SendMediaNavigationController { } } -public let fixedBottomSafeAreaInset: CGFloat = 20 -public let fixedHorizontalMargin: CGFloat = 16 - @objc class SendMediaNavigationController: OWSNavigationController { - static var bottomButtonsCenterOffset: CGFloat { - if UIDevice.current.hasIPhoneXNotch { - // we pin to a constant rather than margin, because on notched devices the - // safeAreaInsets/margins change as the device rotates *EVEN THOUGH* the interface - // is locked to portrait. - return -1 * (CaptureButton.recordingDiameter / 2 + 4) - fixedBottomSafeAreaInset - } else { - return -1 * (CaptureButton.recordingDiameter / 2 + 4) - } - } - - static var trailingButtonsOffset: CGFloat = -28 var attachmentCount: Int { return attachmentDraftCollection.count @@ -66,7 +51,6 @@ class SendMediaNavigationController: OWSNavigationController { guard !CurrentAppContext().hasActiveCall else { return false } - return true } @@ -74,61 +58,12 @@ class SendMediaNavigationController: OWSNavigationController { super.viewDidLoad() self.delegate = self - - let bottomButtonsCenterOffset = SendMediaNavigationController.bottomButtonsCenterOffset - - view.addSubview(batchModeButton) - batchModeButton.setCompressionResistanceHigh() - - view.addSubview(doneButton) - doneButton.setCompressionResistanceHigh() - - view.addSubview(cameraModeButton) - cameraModeButton.setCompressionResistanceHigh() - - view.addSubview(mediaLibraryModeButton) - mediaLibraryModeButton.setCompressionResistanceHigh() - - if UIDevice.current.isIPad { - let buttonSpacing: CGFloat = 28 - // `doneButton` is our widest button, so we position it relative to the superview - // margin, and position other buttons relative to `doneButton`. This ensures - // `donebutton` has a good distance from the edge *and* that all the buttons in the - // cluster are centered WRT eachother. - doneButton.autoPinEdge(toSuperviewMargin: .trailing) - doneButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor, constant: -buttonSpacing).isActive = true - - batchModeButton.autoAlignAxis(.vertical, toSameAxisOf: doneButton) - batchModeButton.autoAlignAxis(.horizontal, toSameAxisOf: doneButton) - - cameraModeButton.autoAlignAxis(.vertical, toSameAxisOf: doneButton) - cameraModeButton.autoPinEdge(.bottom, to: .top, of: doneButton, withOffset: -buttonSpacing) - - mediaLibraryModeButton.autoAlignAxis(.vertical, toSameAxisOf: cameraModeButton) - mediaLibraryModeButton.autoAlignAxis(.horizontal, toSameAxisOf: cameraModeButton) - } else { - // we pin to edges rather than margin, because on notched devices the safeAreaInsets/margins change - // as the device rotates *EVEN THOUGH* the interface is locked to portrait. - - batchModeButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true - batchModeButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 16) - - doneButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true - doneButton.autoPinEdge(toSuperviewEdge: .trailing, withInset: 16) - - cameraModeButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true - cameraModeButton.autoPinEdge(toSuperviewEdge: .leading, withInset: 16) - - mediaLibraryModeButton.centerYAnchor.constraint(equalTo: view.bottomAnchor, constant: bottomButtonsCenterOffset).isActive = true - mediaLibraryModeButton.autoPinEdge(toSuperviewEdge: .leading, withInset: 16) - } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) DispatchQueue.main.async { - // pre-layout views for snappier response should the user - // decide to switch + // Pre-layout views for snappier response should the user decide to switch. if PHPhotoLibrary.authorizationStatus() == .authorized { self.mediaLibraryViewController.view.layoutIfNeeded() @@ -184,96 +119,6 @@ class SendMediaNavigationController: OWSNavigationController { return navController } - private var isForcingBatchSelectInMediaLibrary = true - - private var isShowingMediaLibrary = false - private var isRecordingMovie = false - - var isInBatchSelectMode: Bool { - get { - if isForcingBatchSelectInMediaLibrary && isShowingMediaLibrary { - return true - } - return self.batchModeButton.isSelected - } - - set { - let didChange = newValue != isInBatchSelectMode - self.batchModeButton.isSelected = newValue - - if didChange { - mediaLibraryViewController.batchSelectModeDidChange() - guard let topViewController = viewControllers.last else { - return - } - updateViewState(topViewController: topViewController, animated: false) - } - } - } - - func updateViewState(topViewController: UIViewController, animated: Bool) { - let changes: () -> Void - switch topViewController { - case is AttachmentApprovalViewController: - changes = { - self.isShowingMediaLibrary = false - self.batchModeButton.alpha = 0 - self.doneButton.alpha = 0 - self.cameraModeButton.alpha = 0 - self.mediaLibraryModeButton.alpha = 0 - } - case let mediaLibraryView as ImagePickerGridController: - changes = { - self.isShowingMediaLibrary = true - let showDoneButton = self.isInBatchSelectMode && self.attachmentCount > 0 - self.doneButton.alpha = showDoneButton ? 1 : 0 - - self.batchModeButton.alpha = showDoneButton || self.isForcingBatchSelectInMediaLibrary ? 0 : 1 - self.batchModeButton.isBeingPresentedOverPhotoCapture = false - - self.cameraModeButton.alpha = 1 - self.cameraModeButton.isBeingPresentedOverPhotoCapture = false - - self.mediaLibraryModeButton.alpha = 0 - self.mediaLibraryModeButton.isBeingPresentedOverPhotoCapture = false - - mediaLibraryView.applyBatchSelectMode() - } - case is PhotoCaptureViewController: - changes = { - self.isShowingMediaLibrary = false - let showDoneButton = self.isInBatchSelectMode && self.attachmentCount > 0 - self.doneButton.alpha = !showDoneButton || self.isRecordingMovie ? 0 : 1 - - self.batchModeButton.alpha = showDoneButton || self.isRecordingMovie ? 0 : 1 - self.batchModeButton.isBeingPresentedOverPhotoCapture = true - - self.cameraModeButton.alpha = 0 - self.cameraModeButton.isBeingPresentedOverPhotoCapture = true - - self.mediaLibraryModeButton.alpha = self.isRecordingMovie ? 0 : 1 - self.mediaLibraryModeButton.isBeingPresentedOverPhotoCapture = true - } - case is ConversationPickerViewController: - changes = { - self.doneButton.alpha = 0 - self.batchModeButton.alpha = 0 - self.cameraModeButton.alpha = 0 - self.mediaLibraryModeButton.alpha = 0 - } - default: - owsFailDebug("unexpected topViewController: \(topViewController)") - changes = { } - } - - if animated { - UIView.animate(withDuration: 0.3, animations: changes) - } else { - changes() - } - doneButton.updateCount() - } - func fadeTo(viewControllers: [UIViewController], duration: CFTimeInterval) { AssertIsOnMainThread() @@ -284,63 +129,6 @@ class SendMediaNavigationController: OWSNavigationController { setViewControllers(viewControllers, animated: false) } - // MARK: - Events - - private func didTapBatchModeButton() { - isInBatchSelectMode = !isInBatchSelectMode - owsAssertDebug(isInBatchSelectMode || attachmentCount <= 1) - } - - private func didTapCameraModeButton() { - self.ows_askForCameraPermissions { isGranted in - guard isGranted else { return } - - BenchEventStart(title: "Show-Camera", eventId: "Show-Camera") - self.fadeTo(viewControllers: [self.captureViewController], duration: 0.08) - } - } - - private func didTapMediaLibraryModeButton() { - self.ows_askForMediaLibraryPermissions { isGranted in - guard isGranted else { return } - - BenchEventStart(title: "Show-Media-Library", eventId: "Show-Media-Library") - self.fadeTo(viewControllers: [self.mediaLibraryViewController], duration: 0.08) - } - } - - // MARK: Views - public static let bottomButtonWidth: CGFloat = 44 - - private lazy var doneButton: DoneButton = { - let button = DoneButton() - button.delegate = self - button.setShadow() - - return button - }() - - private lazy var batchModeButton: SendMediaBottomButton = { - return SendMediaBottomButton(imageName: "create-album-filled-28", - tintColor: .ows_white, - diameter: type(of: self).bottomButtonWidth, - block: { [weak self] in self?.didTapBatchModeButton() }) - }() - - private lazy var cameraModeButton: SendMediaBottomButton = { - return SendMediaBottomButton(imageName: "camera-outline-28", - tintColor: .ows_white, - diameter: type(of: self).bottomButtonWidth, - block: { [weak self] in self?.didTapCameraModeButton() }) - }() - - private lazy var mediaLibraryModeButton: SendMediaBottomButton = { - return SendMediaBottomButton(imageName: "photo-outline-28", - tintColor: .ows_white, - diameter: type(of: self).bottomButtonWidth, - block: { [weak self] in self?.didTapMediaLibraryModeButton() }) - }() - // MARK: State private var attachmentDraftCollection: AttachmentDraftCollection = .empty @@ -354,22 +142,19 @@ class SendMediaNavigationController: OWSNavigationController { fileprivate lazy var captureViewController: PhotoCaptureViewController = { let vc = PhotoCaptureViewController() vc.delegate = self - + vc.dataSource = self return vc }() private lazy var mediaLibraryViewController: ImagePickerGridController = { let vc = ImagePickerGridController() vc.delegate = self - return vc }() - private func pushApprovalViewController( - attachmentApprovalItems: [AttachmentApprovalItem], - options: AttachmentApprovalViewControllerOptions = .canAddMore, - animated: Bool - ) { + private func pushApprovalViewController(attachmentApprovalItems: [AttachmentApprovalItem], + options: AttachmentApprovalViewControllerOptions = .canAddMore, + animated: Bool) { guard let sendMediaNavDelegate = self.sendMediaNavDelegate else { owsFailDebug("sendMediaNavDelegate was unexpectedly nil") return @@ -396,49 +181,44 @@ class SendMediaNavigationController: OWSNavigationController { let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken") let confirmAbandonAction = ActionSheetAction(title: confirmAbandonText, - style: .destructive, - handler: { [weak self] _ in - guard let self = self else { return } - self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) + style: .destructive, + handler: { [weak self] _ in + guard let self = self else { return } + self.sendMediaNavDelegate?.sendMediaNavDidCancel(self) }) alert.addAction(confirmAbandonAction) let dontAbandonAction = ActionSheetAction(title: dontAbandonText, - style: .default, - handler: { _ in }) + style: .default, + handler: { _ in }) alert.addAction(dontAbandonAction) self.presentActionSheet(alert) } } + } extension SendMediaNavigationController: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { updateNavbarTheme(for: viewController, animated: animated) switch viewController { case is PhotoCaptureViewController: - if attachmentDraftCollection.count == 1 && !isInBatchSelectMode { + if attachmentDraftCollection.count == 1 { // User is navigating "back" to the previous view, indicating // they want to discard the previously captured item discardDraft() } - case is ImagePickerGridController: - if attachmentDraftCollection.count == 1 && !isInBatchSelectMode { - isInBatchSelectMode = true - mediaLibraryViewController.reloadData() - } + default: break } - - updateViewState(topViewController: viewController, animated: false) } // In case back navigation was canceled, we re-apply whatever is showing. func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { updateNavbarTheme(for: viewController, animated: animated) - updateViewState(topViewController: viewController, animated: false) } func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask { @@ -479,34 +259,23 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { // MARK: - Too Many func showTooManySelectedToast() { - Logger.info("") - let toastFormat = NSLocalizedString("IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT", comment: "Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared.") let toastText = String(format: toastFormat, NSNumber(value: SignalAttachment.maxAttachmentsAllowed)) - let toastController = ToastController(text: toastText) - - let kToastInset: CGFloat = 10 - let bottomInset = kToastInset + view.layoutMargins.bottom - - toastController.presentToastView(fromBottomOfView: view, inset: bottomInset) + toastController.presentToastView(fromBottomOfView: view, inset: view.layoutMargins.bottom + 10) } } extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { - func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didFinishProcessingAttachment attachment: SignalAttachment) { - guard let sendMediaNavDelegate = self.sendMediaNavDelegate else { return } - let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment, canSave: sendMediaNavDelegate.sendMediaNavCanSaveAttachments) - attachmentDraftCollection.append(.camera(attachment: cameraCaptureAttachment)) - if isInBatchSelectMode { - updateViewState(topViewController: photoCaptureViewController, animated: false) - } else { - pushApprovalViewController(attachmentApprovalItems: [cameraCaptureAttachment.attachmentApprovalItem], - animated: true) + func photoCaptureViewControllerDidFinish(_ photoCaptureViewController: PhotoCaptureViewController) { + guard attachmentDraftCollection.count > 0 else { + owsFailDebug("No camera attachments found") + return } + showApprovalAfterProcessingAnyMediaLibrarySelections() } func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) { @@ -530,18 +299,53 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { owsAssertDebug(attachmentDraftCollection.attachmentDrafts.count == 0) } - func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, isRecordingMovie: Bool) { - self.isRecordingMovie = isRecordingMovie - updateViewState(topViewController: photoCaptureViewController, animated: true) + func photoCaptureViewControllerDidRequestPresentPhotoLibrary(_ photoCaptureViewController: PhotoCaptureViewController) { + self.ows_askForMediaLibraryPermissions { isGranted in + guard isGranted else { return } + + BenchEventStart(title: "Show-Media-Library", eventId: "Show-Media-Library") + self.pushViewController(self.mediaLibraryViewController, animated: true) + } + } + + func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didRequestSwitchBatchMode batchMode: Bool) -> Bool { + if batchMode { + // Always can be enabled + return true + } + // Can only be disabled if there's one or less media item. + return attachmentCount > 1 + } +} + +extension SendMediaNavigationController: PhotoCaptureViewControllerDataSource { + + var numberOfMediaItems: Int { + attachmentCount + } + + func addMedia(attachment: SignalAttachment) { + guard let sendMediaNavDelegate = self.sendMediaNavDelegate else { return } + let cameraCaptureAttachment = CameraCaptureAttachment(signalAttachment: attachment, canSave: sendMediaNavDelegate.sendMediaNavCanSaveAttachments) + attachmentDraftCollection.append(.camera(attachment: cameraCaptureAttachment)) } } extension SendMediaNavigationController: ImagePickerGridControllerDelegate { - func imagePickerDidCompleteSelection(_ imagePicker: ImagePickerGridController) { + func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController) { showApprovalAfterProcessingAnyMediaLibrarySelections() } + func imagePickerDidRequestPresentCamera(_ imagePicker: ImagePickerGridController) { + if let cameraViewController = viewControllers.first as? PhotoCaptureViewController { + popToViewController(cameraViewController, animated: true) + return + } + + fadeTo(viewControllers: [ captureViewController ], duration: 0.2) + } + func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) { let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY", comment: "alert action when the user decides not to cancel the media flow after all.") didRequestExit(dontAbandonText: dontAbandonText) @@ -600,8 +404,6 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { let libraryMedia = MediaLibraryAttachment(asset: asset, attachmentApprovalItemPromise: attachmentApprovalItemPromise) attachmentDraftCollection.append(.picker(attachment: libraryMedia)) - - updateViewState(topViewController: imagePicker, animated: false) } func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) { @@ -609,8 +411,6 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { return } attachmentDraftCollection.remove(.picker(attachment: draft)) - - updateViewState(topViewController: imagePicker, animated: false) } func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool { @@ -625,7 +425,6 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate { func attachmentApprovalDidAppear(_ attachmentApproval: AttachmentApprovalViewController) { - updateViewState(topViewController: attachmentApproval, animated: true) } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageBody newMessageBody: MessageBody?) { @@ -653,9 +452,6 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat // Current design dicates we'll go "back" to the single thing before us. owsAssertDebug(viewControllers.count == 2) - // regardless of which VC we're going "back" to, we're in "batch" mode at this point. - isInBatchSelectMode = true - popViewController(animated: true) } @@ -796,98 +592,3 @@ private struct MediaLibraryAttachment: Hashable, Equatable { return lhs.asset == rhs.asset } } - -extension SendMediaNavigationController: DoneButtonDelegate { - var doneButtonCount: Int { - return attachmentCount - } - - fileprivate func doneButtonWasTapped(_ doneButton: DoneButton) { - owsAssertDebug(attachmentDraftCollection.count > 0) - showApprovalAfterProcessingAnyMediaLibrarySelections() - } -} - -private protocol DoneButtonDelegate: AnyObject { - func doneButtonWasTapped(_ doneButton: DoneButton) - var doneButtonCount: Int { get } -} - -private class DoneButton: UIView { - weak var delegate: DoneButtonDelegate? - - init() { - super.init(frame: .zero) - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:))) - addGestureRecognizer(tapGesture) - - let container = PillView() - container.backgroundColor = .ows_white - container.layoutMargins = UIEdgeInsets(top: 7, leading: 8, bottom: 7, trailing: 8) - - addSubview(container) - container.autoPinEdgesToSuperviewMargins() - - let stackView = UIStackView(arrangedSubviews: [badge, chevron]) - stackView.axis = .horizontal - stackView.alignment = .center - stackView.spacing = 9 - - container.addSubview(stackView) - stackView.autoPinEdgesToSuperviewMargins() - } - - let numberFormatter: NumberFormatter = NumberFormatter() - - func updateCount() { - guard let delegate = delegate else { - return - } - - badgeLabel.text = numberFormatter.string(for: delegate.doneButtonCount) - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: - Subviews - - private lazy var badge: UIView = { - let badge = PillView() - badge.layoutMargins = UIEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4) - badge.backgroundColor = .ows_accentBlue - badge.addSubview(badgeLabel) - badgeLabel.autoPinEdgesToSuperviewMargins() - - return badge - }() - - private lazy var badgeLabel: UILabel = { - let label = UILabel() - label.textColor = .ows_white - label.font = UIFont.ows_dynamicTypeSubheadline.ows_monospaced - label.textAlignment = .center - return label - }() - - private lazy var chevron: UIView = { - let image: UIImage - if CurrentAppContext().isRTL { - image = #imageLiteral(resourceName: "small_chevron_left") - } else { - image = #imageLiteral(resourceName: "small_chevron_right") - } - let chevron = UIImageView(image: image.withRenderingMode(.alwaysTemplate)) - chevron.contentMode = .scaleAspectFit - chevron.tintColor = .ows_gray60 - chevron.autoSetDimensions(to: CGSize(width: 10, height: 18)) - - return chevron - }() - - @objc - func didTap(tapGesture: UITapGestureRecognizer) { - delegate?.doneButtonWasTapped(self) - } -} diff --git a/SignalUI/Views/CircleView.swift b/SignalUI/Views/CircleView.swift index cd3f84f060..0fc874e036 100644 --- a/SignalUI/Views/CircleView.swift +++ b/SignalUI/Views/CircleView.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import UIKit From a35f5ed1f0ce283acb5faa945806c02fef891fcc Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 14 Feb 2022 13:31:37 -0800 Subject: [PATCH 06/32] Reorganize classes: move shared UI components into one file. --- Signal.xcodeproj/project.pbxproj | 16 +- ...ptureControl.swift => MediaControls.swift} | 137 ++++++++++++++++++ .../Photos/MediaDoneButton.swift | 106 -------------- .../Photos/PhotoCaptureViewController.swift | 35 ----- .../Photos/SendMediaBottomButton.swift | 89 ------------ 5 files changed, 141 insertions(+), 242 deletions(-) rename Signal/src/ViewControllers/Photos/{CameraCaptureControl.swift => MediaControls.swift} (76%) delete mode 100644 Signal/src/ViewControllers/Photos/MediaDoneButton.swift delete mode 100644 Signal/src/ViewControllers/Photos/SendMediaBottomButton.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 83f57363b7..49bcb7dc84 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -754,7 +754,6 @@ 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */; }; 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */; }; 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; }; - 4C4F5EBC22711EEB00F3DD01 /* SendMediaBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */; }; 4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */; }; 4C5250D421E7C51900CE3D95 /* PhoneNumberValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5250D321E7C51900CE3D95 /* PhoneNumberValidatorTest.swift */; }; 4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.supp */; }; @@ -791,10 +790,9 @@ 641CECC436F5F3EE2AC07EE9 /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6657FDE7B91C2845BB3BEAB5 /* Pods_SignalShareExtension.framework */; }; 760D93AB27A0E28600F351AC /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 760D93AA27A0E28600F351AC /* CoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; - 76B90A0327B5B9220013D510 /* MediaDoneButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B90A0227B5B9220013D510 /* MediaDoneButton.swift */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; 76EB054018170B33006006FC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 76EB03C318170B33006006FC /* AppDelegate.m */; }; - 76FCCDBC27AB8FBE00BAA7F0 /* CameraCaptureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCCDBB27AB8FBE00BAA7F0 /* CameraCaptureControl.swift */; }; + 76FCCDBC27AB8FBE00BAA7F0 /* MediaControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCCDBB27AB8FBE00BAA7F0 /* MediaControls.swift */; }; 8806EF19248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */; }; 8806EF1B248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806EF1A248DBFC100E764C7 /* ContactPermissionReminderMegaphone.swift */; }; 8809CE8722F8FE6D00D38867 /* AttachmentKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8809CE8622F8FE6D00D38867 /* AttachmentKeyboard.swift */; }; @@ -1945,7 +1943,6 @@ 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMediaNavigationController.swift; sourceTree = ""; }; 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableTextField.swift; sourceTree = ""; }; 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ContactDiscoveryOperationTest.swift; path = contact/ContactDiscoveryOperationTest.swift; sourceTree = ""; }; - 4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMediaBottomButton.swift; sourceTree = ""; }; 4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidator.swift; sourceTree = ""; }; 4C5250D321E7C51900CE3D95 /* PhoneNumberValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidatorTest.swift; sourceTree = ""; }; 4C63CBFF210A620B003AE45C /* SignalTSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalTSan.supp; sourceTree = ""; }; @@ -1988,11 +1985,10 @@ 748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 760D93AA27A0E28600F351AC /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 768A1A2A17FC9CD300E00ED8 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; - 76B90A0227B5B9220013D510 /* MediaDoneButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDoneButton.swift; sourceTree = ""; }; 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 = ""; }; - 76FCCDBB27AB8FBE00BAA7F0 /* CameraCaptureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureControl.swift; sourceTree = ""; }; + 76FCCDBB27AB8FBE00BAA7F0 /* MediaControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaControls.swift; sourceTree = ""; }; 7856A9F703AAD99E22B75A9B /* Pods-SignalShareExtension.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.profiling.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.profiling.xcconfig"; sourceTree = ""; }; 7BB1CB6F2D7841356BE367EA /* Pods-Signal.testable release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.testable release.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.testable release.xcconfig"; sourceTree = ""; }; 7C5EABE2C09180BC71C4E097 /* Pods-SignalShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.app store release.xcconfig"; sourceTree = ""; }; @@ -2981,16 +2977,14 @@ isa = PBXGroup; children = ( 32C584A725B81C6600256804 /* AvatarViewController.swift */, - 76FCCDBB27AB8FBE00BAA7F0 /* CameraCaptureControl.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */, + 76FCCDBB27AB8FBE00BAA7F0 /* MediaControls.swift */, 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */, E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */, 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */, 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */, 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */, - 4C4F5EBB22711EEB00F3DD01 /* SendMediaBottomButton.swift */, 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, - 76B90A0227B5B9220013D510 /* MediaDoneButton.swift */, ); path = Photos; sourceTree = ""; @@ -6043,7 +6037,6 @@ 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */, 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */, 348EE28E25B897BF00814FC2 /* CVMediaCache.swift in Sources */, - 4C4F5EBC22711EEB00F3DD01 /* SendMediaBottomButton.swift in Sources */, 343417F12530A7480034FE0C /* CVComponentReactions.swift in Sources */, 88D1D40222EBB5A100F472C5 /* MessageRequestView.swift in Sources */, 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, @@ -6323,7 +6316,6 @@ 342FFE6B271EF502000AC89F /* OWSWindowManager.m in Sources */, 347C382E252CE69400F3D941 /* CVComponentState.swift in Sources */, 342FFE6A271EF502000AC89F /* ConversationSearch.swift in Sources */, - 76B90A0327B5B9220013D510 /* MediaDoneButton.swift in Sources */, 340D900024FEE6A9007B5504 /* GroupInviteLinksUI.swift in Sources */, 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */, 887B381325F0681400685845 /* AdvancedPrivacySettingsViewController.swift in Sources */, @@ -6366,7 +6358,7 @@ 34ACA7DE2733159600E47AD4 /* OnboardingPhoneNumberDiscoverabilityViewController.swift in Sources */, 88A9729222FA5D4B004B4FBF /* AttachmentFormatPickerView.swift in Sources */, 346C19E125ACE9AE00061D3A /* MediaDownloadSettingsViewController.swift in Sources */, - 76FCCDBC27AB8FBE00BAA7F0 /* CameraCaptureControl.swift in Sources */, + 76FCCDBC27AB8FBE00BAA7F0 /* MediaControls.swift in Sources */, 8827004E23208A1900F01C46 /* AppearanceSettingsTableViewController.swift in Sources */, 344A761324B36C8C009D69A5 /* TestingViewController.swift in Sources */, 3495FF0D25F934C500959D6E /* PaymentsRestoreWalletCompleteViewController.swift in Sources */, diff --git a/Signal/src/ViewControllers/Photos/CameraCaptureControl.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift similarity index 76% rename from Signal/src/ViewControllers/Photos/CameraCaptureControl.swift rename to Signal/src/ViewControllers/Photos/MediaControls.swift index 11e9d0256e..cde2a05aa1 100644 --- a/Signal/src/ViewControllers/Photos/CameraCaptureControl.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -422,3 +422,140 @@ class CameraCaptureControl: UIView { } } } + + +class PhotoControl: UIView { + let button: OWSButton + + private static let visibleButtonSize: CGFloat = 36 // both height and width + private static let layoutMargin: CGFloat = 4 // both horizontal and vertical + + init(imageName: String, block: @escaping () -> Void) { + self.button = OWSButton(imageName: imageName, tintColor: .ows_white, block: block) + + super.init(frame: CGRect(origin: .zero, size: CGSize(square: Self.visibleButtonSize + 2*Self.layoutMargin))) + + layoutMargins = UIEdgeInsets(margin: Self.layoutMargin) + + let blurView = CircleBlurView(effect: UIBlurEffect(style: .dark)) + addSubview(blurView) + blurView.autoPinEdgesToSuperviewMargins() + + addSubview(button) + button.autoPinEdgesToSuperviewMargins() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + return CGSize(width: Self.visibleButtonSize + layoutMargins.leading + layoutMargins.trailing, + height: Self.visibleButtonSize + layoutMargins.top + layoutMargins.bottom) + } + + func setImage(imageName: String) { + button.setImage(imageName: imageName) + } +} + + +class MediaDoneButton: UIButton { + + var badgeNumber: Int = 0 { + didSet { + textLabel.text = numberFormatter.string(for: badgeNumber) + invalidateIntrinsicContentSize() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private let numberFormatter: NumberFormatter = { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + return numberFormatter + }() + + private let textLabel: UILabel = { + let label = UILabel() + label.textColor = .ows_white + label.textAlignment = .center + label.font = .ows_dynamicTypeSubheadline.ows_monospaced + return label + }() + private var pillView: PillView! + private var chevronImageView: UIImageView! + private var dimmerView: UIView! + + private func commonInit() { + pillView = PillView(frame: bounds) + pillView.isUserInteractionEnabled = false + pillView.layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 7) + addSubview(pillView) + pillView.autoPinEdgesToSuperviewEdges() + + let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + pillView.addSubview(blurBackgroundView) + blurBackgroundView.autoPinEdgesToSuperviewEdges() + + let blueBadgeView = PillView(frame: bounds) + blueBadgeView.backgroundColor = .ows_accentBlue + blueBadgeView.layoutMargins = UIEdgeInsets(margin: 4) + blueBadgeView.addSubview(textLabel) + textLabel.autoPinEdgesToSuperviewMargins() + + let image: UIImage? + if #available(iOS 13, *) { + image = CurrentAppContext().isRTL ? UIImage(systemName: "chevron.backward") : UIImage(systemName: "chevron.right") + } else { + image = CurrentAppContext().isRTL ? UIImage(named: "chevron-left-20") : UIImage(named: "chevron-right-20") + } + chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate)) + chevronImageView.contentMode = .center + chevronImageView.tintColor = .ows_white + if #available(iOS 13, *) { + chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) + } + + let hStack = UIStackView(arrangedSubviews: [blueBadgeView, chevronImageView]) + hStack.spacing = 6 + pillView.addSubview(hStack) + hStack.autoPinEdgesToSuperviewMargins() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { + return + } + textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced + if #available(iOS 13, *) { + chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) + } + } + + override var isHighlighted: Bool { + didSet { + if isHighlighted { + if dimmerView == nil { + dimmerView = UIView(frame: bounds) + dimmerView.isUserInteractionEnabled = false + dimmerView.backgroundColor = .ows_black + pillView.addSubview(dimmerView) + dimmerView.autoPinEdgesToSuperviewEdges() + } + dimmerView.alpha = 0.5 + } else if let dimmerView = dimmerView { + dimmerView.alpha = 0 + } + } + } +} diff --git a/Signal/src/ViewControllers/Photos/MediaDoneButton.swift b/Signal/src/ViewControllers/Photos/MediaDoneButton.swift deleted file mode 100644 index e6e7a6bf31..0000000000 --- a/Signal/src/ViewControllers/Photos/MediaDoneButton.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// Copyright (c) 2022 Open Whisper Systems. All rights reserved. -// - -import SignalUI -import UIKit - -class MediaDoneButton: UIButton { - - var badgeNumber: Int = 0 { - didSet { - textLabel.text = numberFormatter.string(for: badgeNumber) - invalidateIntrinsicContentSize() - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private let numberFormatter: NumberFormatter = { - let numberFormatter = NumberFormatter() - numberFormatter.numberStyle = .decimal - return numberFormatter - }() - - private let textLabel: UILabel = { - let label = UILabel() - label.textColor = .ows_white - label.textAlignment = .center - label.font = .ows_dynamicTypeSubheadline.ows_monospaced - return label - }() - private var pillView: PillView! - private var chevronImageView: UIImageView! - private var dimmerView: UIView! - - private func commonInit() { - pillView = PillView(frame: bounds) - pillView.isUserInteractionEnabled = false - pillView.layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 7) - addSubview(pillView) - pillView.autoPinEdgesToSuperviewEdges() - - let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) - pillView.addSubview(blurBackgroundView) - blurBackgroundView.autoPinEdgesToSuperviewEdges() - - let blueBadgeView = PillView(frame: bounds) - blueBadgeView.backgroundColor = .ows_accentBlue - blueBadgeView.layoutMargins = UIEdgeInsets(margin: 4) - blueBadgeView.addSubview(textLabel) - textLabel.autoPinEdgesToSuperviewMargins() - - let image: UIImage? - if #available(iOS 13, *) { - image = CurrentAppContext().isRTL ? UIImage(systemName: "chevron.backward") : UIImage(systemName: "chevron.right") - } else { - image = CurrentAppContext().isRTL ? UIImage(named: "chevron-left-20") : UIImage(named: "chevron-right-20") - } - chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate)) - chevronImageView.contentMode = .center - chevronImageView.tintColor = .ows_white - if #available(iOS 13, *) { - chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) - } - - let hStack = UIStackView(arrangedSubviews: [blueBadgeView, chevronImageView]) - hStack.spacing = 6 - pillView.addSubview(hStack) - hStack.autoPinEdgesToSuperviewMargins() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { - return - } - textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced - if #available(iOS 13, *) { - chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) - } - } - - override var isHighlighted: Bool { - didSet { - if isHighlighted { - if dimmerView == nil { - dimmerView = UIView(frame: bounds) - dimmerView.isUserInteractionEnabled = false - dimmerView.backgroundColor = .ows_black - pillView.addSubview(dimmerView) - dimmerView.autoPinEdgesToSuperviewEdges() - } - dimmerView.alpha = 0.5 - } else if let dimmerView = dimmerView { - dimmerView.alpha = 0 - } - } - } -} diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 90e233ce07..88767142c4 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -800,41 +800,6 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { // MARK: - Views -private class PhotoControl: UIView { - let button: OWSButton - - private static let visibleButtonSize: CGFloat = 36 // both height and width - private static let layoutMargin: CGFloat = 4 // both horizontal and vertical - - init(imageName: String, block: @escaping () -> Void) { - self.button = OWSButton(imageName: imageName, tintColor: .ows_white, block: block) - - super.init(frame: CGRect(origin: .zero, size: CGSize(square: Self.visibleButtonSize + 2*Self.layoutMargin))) - - layoutMargins = UIEdgeInsets(margin: Self.layoutMargin) - - let blurView = CircleBlurView(effect: UIBlurEffect(style: .dark)) - addSubview(blurView) - blurView.autoPinEdgesToSuperviewMargins() - - addSubview(button) - button.autoPinEdgesToSuperviewMargins() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var intrinsicContentSize: CGSize { - return CGSize(width: Self.visibleButtonSize + layoutMargins.leading + layoutMargins.trailing, - height: Self.visibleButtonSize + layoutMargins.top + layoutMargins.bottom) - } - - func setImage(imageName: String) { - button.setImage(imageName: imageName) - } -} - private class MediaPickerThumbnailButton: UIButton { private static let visibleSize = CGSize(square: 36) diff --git a/Signal/src/ViewControllers/Photos/SendMediaBottomButton.swift b/Signal/src/ViewControllers/Photos/SendMediaBottomButton.swift deleted file mode 100644 index 8641a39f2d..0000000000 --- a/Signal/src/ViewControllers/Photos/SendMediaBottomButton.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. -// - -import UIKit - -class SendMediaBottomButton: UIView { - let button: OWSButton - let blurView: UIVisualEffectView - - init(imageName: String, tintColor: UIColor, diameter: CGFloat, block: @escaping () -> Void) { - self.button = OWSButton(imageName: imageName, tintColor: tintColor, block: block) - button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) - - let blurEffect = UIBlurEffect(style: .dark) - self.blurView = UIVisualEffectView(effect: blurEffect) - - super.init(frame: .zero) - - layer.cornerRadius = diameter / 2 - blurView.layer.cornerRadius = diameter / 2 - blurView.clipsToBounds = true - - addSubview(blurView) - blurView.autoPinEdgesToSuperviewEdges() - - let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect) - let vibrancyView = UIVisualEffectView(effect: vibrancyEffect) - blurView.contentView.addSubview(vibrancyView) - vibrancyView.autoPinEdgesToSuperviewEdges() - - addSubview(button) - button.autoSetDimensions(to: CGSize(square: diameter)) - button.autoPinEdgesToSuperviewEdges() - updateViewState() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - var isSelected: Bool = false { - didSet { - updateViewState() - } - } - - var isBeingPresentedOverPhotoCapture: Bool = false { - didSet { - updateViewState() - } - } - - private enum Mode { - case selected, unselectedOverMediaLibrary, unselectedOverPhotoCapture - } - - private var mode: Mode { - if isSelected { - return .selected - } - - if isBeingPresentedOverPhotoCapture { - return .unselectedOverPhotoCapture - } - - return .unselectedOverMediaLibrary - } - - func updateViewState() { - switch mode { - case .selected: - button.tintColor = .ows_black - blurView.isHidden = true - backgroundColor = .ows_white - setShadow() - case .unselectedOverMediaLibrary: - button.tintColor = .ows_white - blurView.isHidden = false - backgroundColor = .clear - layer.shadowRadius = 0 - case .unselectedOverPhotoCapture: - button.tintColor = .ows_white - blurView.isHidden = true - backgroundColor = .clear - setShadow() - } - } -} From 1f65ba51111718b01b4103633044bb39e333005b Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 14 Feb 2022 16:42:51 -0800 Subject: [PATCH 07/32] Improvements to share media UI buttons with blurred background. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • allow the button to be configured for dark/light UI styles. • make content insets configurable. --- .../Photos/MediaControls.swift | 130 +++++++++++++++--- .../Photos/PhotoCaptureViewController.swift | 10 +- 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index cde2a05aa1..99e87cdcff 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -423,29 +423,92 @@ class CameraCaptureControl: UIView { } } +@available(iOS, deprecated: 13.0, message: "Use `overrideUserInterfaceStyle` instead.") +private protocol UserInterfaceStyleOverride { -class PhotoControl: UIView { - let button: OWSButton + var userInterfaceStyleOverride: UIUserInterfaceStyle { get set } + + var effectiveUserInterfaceStyle: UIUserInterfaceStyle { get } + +} + +private extension UserInterfaceStyleOverride { + + var effectiveUserInterfaceStyle: UIUserInterfaceStyle { + if userInterfaceStyleOverride != .unspecified { + return userInterfaceStyleOverride + } + if let uiView = self as? UIView { + return uiView.traitCollection.userInterfaceStyle + } + return .unspecified + } + + static func blurEffectStyle(for userInterfaceStyle: UIUserInterfaceStyle) -> UIBlurEffect.Style { + switch userInterfaceStyle { + case .dark: + return .dark + case .light: + return .prominent + default: + fatalError("It is an error to pass UIUserInterfaceStyleUnspecified.") + } + } + + static func tintColor(for userInterfaceStyle: UIUserInterfaceStyle) -> UIColor { + switch userInterfaceStyle { + case .dark: + return Theme.darkThemePrimaryColor + case .light: + return Theme.lightThemePrimaryColor + default: + fatalError("It is an error to pass UIUserInterfaceStyleUnspecified.") + } + } +} + + +class PhotoControl: UIView, UserInterfaceStyleOverride { + + var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified { + didSet { + if oldValue != userInterfaceStyleOverride { + updateStyle() + } + } + } + + private var backgroundView: UIVisualEffectView! + private let button: OWSButton private static let visibleButtonSize: CGFloat = 36 // both height and width - private static let layoutMargin: CGFloat = 4 // both horizontal and vertical + private static let defaultInset: CGFloat = 4 - init(imageName: String, block: @escaping () -> Void) { - self.button = OWSButton(imageName: imageName, tintColor: .ows_white, block: block) + var contentInsets: UIEdgeInsets = UIEdgeInsets(margin: PhotoControl.defaultInset) { + didSet { + layoutMargins = contentInsets + } + } - super.init(frame: CGRect(origin: .zero, size: CGSize(square: Self.visibleButtonSize + 2*Self.layoutMargin))) + init(imageName: String, userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified, block: @escaping () -> Void) { + button = OWSButton(imageName: imageName, tintColor: nil, block: block) - layoutMargins = UIEdgeInsets(margin: Self.layoutMargin) + super.init(frame: CGRect(origin: .zero, size: CGSize(square: Self.visibleButtonSize + 2*Self.defaultInset))) - let blurView = CircleBlurView(effect: UIBlurEffect(style: .dark)) - addSubview(blurView) - blurView.autoPinEdgesToSuperviewMargins() + layoutMargins = contentInsets + self.userInterfaceStyleOverride = userInterfaceStyleOverride + + backgroundView = CircleBlurView(effect: UIBlurEffect(style: PhotoControl.blurEffectStyle(for: effectiveUserInterfaceStyle))) + addSubview(backgroundView) + backgroundView.autoPinEdgesToSuperviewMargins() addSubview(button) button.autoPinEdgesToSuperviewMargins() + + updateStyle() } - required init?(coder aDecoder: NSCoder) { + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -454,13 +517,25 @@ class PhotoControl: UIView { height: Self.visibleButtonSize + layoutMargins.top + layoutMargins.bottom) } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle, userInterfaceStyleOverride == .unspecified { + updateStyle() + } + } + + private func updateStyle() { + backgroundView.effect = UIBlurEffect(style: PhotoControl.blurEffectStyle(for: effectiveUserInterfaceStyle)) + button.tintColor = PhotoControl.tintColor(for: effectiveUserInterfaceStyle) + } + func setImage(imageName: String) { button.setImage(imageName: imageName) } } -class MediaDoneButton: UIButton { +class MediaDoneButton: UIButton, UserInterfaceStyleOverride { var badgeNumber: Int = 0 { didSet { @@ -469,6 +544,14 @@ class MediaDoneButton: UIButton { } } + var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified { + didSet { + if oldValue != userInterfaceStyleOverride { + updateStyle() + } + } + } + override init(frame: CGRect) { super.init(frame: frame) commonInit() @@ -493,6 +576,7 @@ class MediaDoneButton: UIButton { return label }() private var pillView: PillView! + private var blurBackgroundView: UIVisualEffectView! private var chevronImageView: UIImageView! private var dimmerView: UIView! @@ -503,7 +587,7 @@ class MediaDoneButton: UIButton { addSubview(pillView) pillView.autoPinEdgesToSuperviewEdges() - let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: MediaDoneButton.blurEffectStyle(for: effectiveUserInterfaceStyle))) pillView.addSubview(blurBackgroundView) blurBackgroundView.autoPinEdgesToSuperviewEdges() @@ -521,7 +605,6 @@ class MediaDoneButton: UIButton { } chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate)) chevronImageView.contentMode = .center - chevronImageView.tintColor = .ows_white if #available(iOS 13, *) { chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) } @@ -530,15 +613,19 @@ class MediaDoneButton: UIButton { hStack.spacing = 6 pillView.addSubview(hStack) hStack.autoPinEdgesToSuperviewMargins() + + updateStyle() } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - guard traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { - return + if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { + textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced + if #available(iOS 13, *) { + chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) + } } - textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced - if #available(iOS 13, *) { - chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) + if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle { + updateStyle() } } @@ -558,4 +645,9 @@ class MediaDoneButton: UIButton { } } } + + private func updateStyle() { + blurBackgroundView.effect = UIBlurEffect(style: MediaDoneButton.blurEffectStyle(for: effectiveUserInterfaceStyle)) + chevronImageView.tintColor = MediaDoneButton.tintColor(for: effectiveUserInterfaceStyle) + } } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 88767142c4..4d7778bee4 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -398,33 +398,33 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate }() private lazy var dismissControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-close") { [weak self] in + return PhotoControl(imageName: "media-composer-close", userInterfaceStyleOverride: .dark) { [weak self] in self?.didTapClose() } }() private lazy var switchCameraControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-switch-camera-24") { [weak self] in + return PhotoControl(imageName: "media-composer-switch-camera-24", userInterfaceStyleOverride: .dark) { [weak self] in self?.didTapSwitchCamera() } }() private lazy var flashModeControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-flash-auto-24") { [weak self] in + return PhotoControl(imageName: "media-composer-flash-auto-24", userInterfaceStyleOverride: .dark) { [weak self] in self?.didTapFlashMode() } }() private lazy var batchModeControl: PhotoControl = { - let control = PhotoControl(imageName: "media-composer-create-album-outline-24") { [weak self] in + return PhotoControl(imageName: "media-composer-create-album-outline-24", userInterfaceStyleOverride: .dark) { [weak self] in self?.didTapBatchMode() } - return control }() private lazy var doneButton: MediaDoneButton = { let button = MediaDoneButton(type: .custom) button.badgeNumber = 0 + button.userInterfaceStyleOverride = .dark return button }() From 34bb80a551e71774e15889b48c6e645078b3b8a1 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 14 Feb 2022 22:28:26 -0800 Subject: [PATCH 08/32] Add Camera and Done button to photo library picker. --- .../Contents.json | 16 ++ .../media-composer-camera-outline-28.pdf | 129 +++++++++++++++ .../Photos/ImagePickerController.swift | 153 ++++++++++++++---- .../Photos/PhotoCaptureViewController.swift | 34 ++-- .../SendMediaNavigationController.swift | 32 ++-- 5 files changed, 302 insertions(+), 62 deletions(-) create mode 100644 Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf diff --git a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json new file mode 100644 index 0000000000..7c5d5a832f --- /dev/null +++ b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-camera-outline-28.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf new file mode 100644 index 0000000000..ab69c4365b --- /dev/null +++ b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf @@ -0,0 +1,129 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.965088 2.899902 cm +0.000000 0.000000 0.000000 scn +12.034847 16.100098 m +15.034847 16.100098 17.534847 13.600098 17.534847 10.600098 c +17.534847 7.600098 15.034847 5.100098 12.034847 5.100098 c +9.034847 5.100098 6.534847 7.600098 6.534847 10.600098 c +6.534847 13.600098 9.034847 16.100098 12.034847 16.100098 c +12.034847 16.100098 l +h +12.034847 17.600098 m +8.134847 17.600098 5.034847 14.500097 5.034847 10.600098 c +5.034847 6.700098 8.134847 3.600098 12.034847 3.600098 c +15.934847 3.600098 19.034847 6.700098 19.034847 10.600098 c +19.034847 14.500097 15.934847 17.600098 12.034847 17.600098 c +h +14.734847 21.100098 m +14.834847 21.100098 15.034846 21.000097 15.134847 20.900097 c +17.034847 19.000097 l +17.434847 18.600098 l +18.934847 18.600098 l +20.634848 18.600098 20.934847 18.400097 21.334846 18.200098 c +21.734846 18.000097 22.034845 17.700098 22.234846 17.300098 c +22.434847 17.000097 22.634848 16.600098 22.634848 14.900098 c +22.634848 5.200098 l +22.634848 3.500097 22.434847 3.200098 22.234846 2.800098 c +22.034845 2.400099 21.734846 2.100098 21.334846 1.900097 c +21.034847 1.700096 20.634848 1.500097 18.934847 1.500097 c +5.134847 1.500097 l +3.434847 1.500097 3.134847 1.700096 2.734847 1.900097 c +2.334847 2.100098 2.034847 2.400099 1.834847 2.800098 c +1.634847 3.100098 1.434847 3.500097 1.434847 5.200098 c +1.434847 14.900098 l +1.434847 16.600098 1.634847 17.000097 1.834847 17.300098 c +2.034847 17.700098 2.334847 18.000097 2.734847 18.200098 c +3.034847 18.400097 3.434847 18.600098 5.134847 18.600098 c +6.634847 18.600098 l +7.034847 19.000097 l +8.834846 20.800098 l +9.034847 21.000097 9.334847 21.100098 9.534847 21.100098 c +14.734847 21.100098 l +14.734847 21.100098 l +h +14.734847 22.600098 m +9.634847 22.600098 l +8.934847 22.600098 8.334846 22.300098 7.834847 21.900097 c +6.034847 20.100098 l +5.134847 20.100098 l +4.134847 20.200098 3.034847 20.000097 2.134847 19.600098 c +1.434847 19.200098 0.934847 18.700098 0.534847 18.000097 c +0.034847 17.000097 -0.065153 16.000097 0.034847 14.900098 c +0.034847 5.200098 l +-0.065153 4.200098 0.134847 3.100098 0.534847 2.100098 c +0.834847 1.500097 1.434847 0.900097 2.034847 0.600098 c +3.034847 0.200098 4.034847 0.000097 5.134847 0.100098 c +18.834846 0.100098 l +19.934847 0.000097 21.034847 0.200098 21.934847 0.600098 c +22.534847 0.900097 23.034847 1.400097 23.434847 2.100098 c +23.934847 3.100098 24.034847 4.200098 23.934847 5.200098 c +23.934847 14.900098 l +24.034847 16.000097 23.834846 17.000097 23.434847 18.000097 c +23.134848 18.600098 22.634848 19.200098 21.934847 19.500097 c +20.934847 20.000097 19.834846 20.100098 18.834846 20.000097 c +18.034847 20.000097 l +16.134848 21.900097 l +15.734848 22.400097 15.234847 22.600098 14.734847 22.600098 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2785 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 28.000000 28.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002875 00000 n +0000002898 00000 n +0000003071 00000 n +0000003145 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3204 +%%EOF \ No newline at end of file diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 4deb8995e2..47106ba440 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -4,23 +4,29 @@ import Foundation import Photos +import UIKit protocol ImagePickerGridControllerDelegate: AnyObject { func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController) func imagePickerDidRequestPresentCamera(_ imagePicker: ImagePickerGridController) func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) - func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) func imagePicker(_ imagePicker: ImagePickerGridController, didDeselectAsset asset: PHAsset) - func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController) } +protocol ImagePickerGridControllerDataSource: AnyObject { + func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool + func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool + var numberOfMediaItems: Int { get } +} + class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate { weak var delegate: ImagePickerGridControllerDelegate? + weak var dataSource: ImagePickerGridControllerDataSource? private let library: PhotoLibrary = PhotoLibrary() private var photoCollection: PhotoCollection @@ -30,6 +36,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat private var collectionViewFlowLayout: UICollectionViewFlowLayout private var titleView: TitleView! + private var bottomBar: UIView! + private var cameraButton: PhotoControl! + private var doneButton: MediaDoneButton! + init() { collectionViewFlowLayout = type(of: self).buildLayout() photoCollection = library.defaultPhotoCollection() @@ -62,10 +72,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } collectionView.allowsMultipleSelection = true + collectionView.backgroundColor = Theme.backgroundColor collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier) let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancel)) - cancelButton.tintColor = .ows_gray05 navigationItem.leftBarButtonItem = cancelButton let titleView = TitleView() @@ -74,6 +84,30 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat navigationItem.titleView = titleView self.titleView = titleView + bottomBar = UIView() + view.addSubview(bottomBar) + bottomBar.preservesSuperviewLayoutMargins = true + bottomBar.autoPinBottomToSuperviewMargin() + bottomBar.autoPinHorizontalEdges(toEdgesOf: view) + + cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28") { [weak self] in + guard let self = self else { return } + self.didTapCameraButton() + } + bottomBar.addSubview(cameraButton) + cameraButton.contentInsets = .zero + cameraButton.autoSetDimensions(to: .square(40)) + cameraButton.autoPinLeadingToSuperviewMargin() + cameraButton.autoPinTopToSuperviewMargin() + cameraButton.autoPinBottomToSuperviewMargin() + + doneButton = MediaDoneButton() + doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) + bottomBar.addSubview(doneButton) + doneButton.autoPinTopToSuperviewMargin() + doneButton.autoPinBottomToSuperviewMargin() + doneButton.autoPinTrailingToSuperviewMargin() + let selectionPanGesture = DirectionalPanGestureRecognizer(direction: [.horizontal], target: self, action: #selector(didPanSelection)) selectionPanGesture.delegate = self self.selectionPanGesture = selectionPanGesture @@ -93,8 +127,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - guard let delegate = delegate else { - owsFailDebug("delegate was unexpectedly nil") + guard let dataSource = dataSource else { + owsFailDebug("dataSource was unexpectedly nil") return } @@ -111,7 +145,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } let asset = photoCollectionContents.asset(at: indexPath.item) - if delegate.imagePicker(self, isAssetSelected: asset) { + if dataSource.imagePicker(self, isAssetSelected: asset) { selectionPanGestureMode = .deselect } else { selectionPanGestureMode = .select @@ -153,8 +187,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - guard let delegate = delegate else { - owsFailDebug("delegate was unexpectedly nil") + guard let delegate = delegate, let dataSource = dataSource else { + owsFailDebug("delegate or dataSource was unexpectedly nil") return } @@ -165,7 +199,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } - guard delegate.imagePickerCanSelectMoreItems(self) else { + guard dataSource.imagePickerCanSelectMoreItems(self) else { delegate.imagePickerDidTryToSelectTooMany(self) return } @@ -201,6 +235,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat if !hasEverAppeared { scrollToBottom(animated: false) } + + updateDoneButtonAppearance() } override func viewSafeAreaInsetsDidChange() { @@ -231,7 +267,19 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - // MARK: + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + collectionView.backgroundColor = Theme.backgroundColor + } + + private func updateDoneButtonAppearance() { + guard let dataSource = dataSource else { return } + + doneButton.badgeNumber = dataSource.numberOfMediaItems + doneButton.isHidden = doneButton.badgeNumber == 0 + } + + // MARK: - Scrolling private var lastPageYOffset: CGFloat { let yOffset = collectionView.contentSize.height - collectionView.frame.height + collectionView.contentInset.bottom + view.safeAreaInsets.bottom @@ -287,6 +335,15 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat self.delegate?.imagePickerDidCancel(self) } + private func didTapCameraButton() { + delegate?.imagePickerDidRequestPresentCamera(self) + } + + @objc + private func didTapDoneButton() { + delegate?.imagePickerDidRequestSendMedia(self) + } + // MARK: - Layout private static let kInterItemSpacing: CGFloat = 2 @@ -350,8 +407,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat }() private func showCollectionPicker() { - Logger.debug("") - guard let collectionPickerView = collectionPickerController.view else { owsFailDebug("collectionView was unexpectedly nil") return @@ -376,8 +431,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } private func hideCollectionPicker() { - Logger.debug("") - assert(isShowingCollectionPickerController) isShowingCollectionPickerController = false @@ -418,9 +471,9 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat // MARK: - UICollectionView override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - guard let delegate = delegate else { return false } + guard let delegate = delegate, let dataSource = dataSource else { return false } - if delegate.imagePickerCanSelectMoreItems(self) { + if dataSource.imagePickerCanSelectMoreItems(self) { return true } @@ -437,6 +490,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) let attachmentPromise: Promise = photoCollectionContents.outgoingAttachment(for: asset) delegate.imagePicker(self, didSelectAsset: asset, attachmentPromise: attachmentPromise) + updateDoneButtonAppearance() } override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { @@ -447,6 +501,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let asset = photoCollectionContents.asset(at: indexPath.item) delegate.imagePicker(self, didDeselectAsset: asset) + updateDoneButtonAppearance() } override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { @@ -465,13 +520,13 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let delegate = delegate else { return } + guard let dataSource = dataSource else { return } guard let photoGridViewCell = cell as? PhotoGridViewCell else { owsFailDebug("unexpected cell: \(cell)") return } let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) - let isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset) + let isSelected = dataSource.imagePicker(self, isAssetSelected: assetItem.asset) if isSelected { collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) } else { @@ -482,7 +537,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } private func updateVisibleCells() { - guard let delegate = delegate else { return } + guard let dataSource = dataSource else { return } + for cell in collectionView.visibleCells { guard let photoGridViewCell = cell as? PhotoGridViewCell else { owsFailDebug("unexpected cell: \(cell)") @@ -494,13 +550,14 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat continue } - photoGridViewCell.isSelected = delegate.imagePicker(self, isAssetSelected: assetItem.asset) + photoGridViewCell.isSelected = dataSource.imagePicker(self, isAssetSelected: assetItem.asset) photoGridViewCell.allowsMultipleSelection = collectionView.allowsMultipleSelection } } } extension ImagePickerGridController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == selectionPanGesture else { return true @@ -516,39 +573,65 @@ private protocol TitleViewDelegate: AnyObject { private class TitleView: UIView { - // MARK: - Private - private let label = UILabel() private let iconView = UIImageView() - private let stackView: UIStackView + private var stackView: UIStackView! + + // Returns same font as UIBarButtonItem uses. + private func titleLabelFont() -> UIFont { + let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) + return UIFont(descriptor: fontDescriptor, size: fontDescriptor.pointSize.clamp(17, 21)) + } // MARK: - Initializers override init(frame: CGRect) { - let stackView = UIStackView(arrangedSubviews: [label, iconView]) + super.init(frame: frame) + commonInit() + + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + stackView = UIStackView(arrangedSubviews: [label, iconView]) stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = 5 stackView.isUserInteractionEnabled = true - - self.stackView = stackView - - super.init(frame: frame) - addSubview(stackView) stackView.autoPinEdgesToSuperviewEdges() - label.textColor = .ows_gray05 - label.font = UIFont.ows_dynamicTypeBody.ows_semibold + label.textColor = tintColor + label.font = titleLabelFont() - iconView.tintColor = .ows_gray05 - iconView.image = UIImage(named: "media-composer-navbar-chevron") + iconView.tintColor = tintColor + if #available(iOS 13, *) { + iconView.image = UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(font: label.font)) + } else { + iconView.image = UIImage(named: "media-composer-navbar-chevron") + } addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped))) } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + override func tintColorDidChange() { + super.tintColorDidChange() + label.textColor = tintColor + iconView.tintColor = tintColor + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { + label.font = titleLabelFont() + if #available(iOS 13, *) { + iconView.image = UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(font: label.font)) + } + } } // MARK: - Public diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 4d7778bee4..ad482fd444 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -203,6 +203,11 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate photoCapture.updateVideoPreviewConnection(toOrientation: previewOrientation) updateIconOrientations(isAnimated: false, captureOrientation: previewOrientation) resumePhotoCapture() + + if let dataSource = dataSource, dataSource.numberOfMediaItems > 0 { + isInBatchMode = true + } + updateDoneButtonAppearance() } override func viewDidAppear(_ animated: Bool) { @@ -300,7 +305,12 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate bottomBar.isHidden = isRecordingVideo } } - private var isInBatchMode: Bool = false + private var isInBatchMode: Bool = false { + didSet { + let imageName = isInBatchMode ? "media-composer-create-album-solid-24" : "media-composer-create-album-outline-24" + batchModeControl.setImage(imageName: imageName) + } + } private class TopBar: UIView { let recordingTimerView = RecordingTimerView(frame: .zero) @@ -379,8 +389,13 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate switchCameraControl]) }() - func updateBottomBarItems () { - + func updateDoneButtonAppearance () { + if isInBatchMode, let badgeNumber = dataSource?.numberOfMediaItems { + doneButton.badgeNumber = badgeNumber + doneButton.isHidden = false + } else { + doneButton.isHidden = true + } } // MARK: - Views @@ -492,8 +507,6 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate return } isInBatchMode = delegate.photoCaptureViewController(self, didRequestSwitchBatchMode: !isInBatchMode) - let imageName = isInBatchMode ? "media-composer-create-album-solid-24" : "media-composer-create-album-outline-24" - batchModeControl.setImage(imageName: imageName) } @objc @@ -728,13 +741,10 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessing attachment: SignalAttachment) { dataSource?.addMedia(attachment: attachment) - - if isInBatchMode, let badgeNumber = dataSource?.numberOfMediaItems { - doneButton.badgeNumber = badgeNumber - doneButton.isHidden = false - } else { - doneButton.isHidden = true - + + updateDoneButtonAppearance() + + if !isInBatchMode { delegate?.photoCaptureViewControllerDidFinish(self) } } diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 294a2ba544..2c99a0f96c 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -149,6 +149,7 @@ class SendMediaNavigationController: OWSNavigationController { private lazy var mediaLibraryViewController: ImagePickerGridController = { let vc = ImagePickerGridController() vc.delegate = self + vc.dataSource = self return vc }() @@ -205,7 +206,7 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { switch viewController { case is PhotoCaptureViewController: - if attachmentDraftCollection.count == 1 { + if attachmentCount == 1, navigationController.topViewController is AttachmentApprovalViewController { // User is navigating "back" to the previous view, indicating // they want to discard the previously captured item discardDraft() @@ -247,7 +248,7 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { case is AttachmentApprovalViewController: showNavbar(.alwaysDarkAndClear) case is ImagePickerGridController: - showNavbar(.alwaysDark) + showNavbar(.default) case is ConversationPickerViewController: showNavbar(.default) default: @@ -313,8 +314,8 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { // Always can be enabled return true } - // Can only be disabled if there's one or less media item. - return attachmentCount > 1 + // Can only be disabled if there's no media attachments yet. + return attachmentCount > 0 } } @@ -386,16 +387,9 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { backgroundBlock: backgroundBlock) } - func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool { - return attachmentDraftCollection.hasPickerAttachment(forAsset: asset) - } - func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) { guard let sendMediaNavDelegate = sendMediaNavDelegate else { return } - - guard !attachmentDraftCollection.hasPickerAttachment(forAsset: asset) else { - return - } + guard !attachmentDraftCollection.hasPickerAttachment(forAsset: asset) else { return } let attachmentApprovalItemPromise = attachmentPromise.map { attachment in AttachmentApprovalItem(attachment: attachment, @@ -413,13 +407,21 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { attachmentDraftCollection.remove(.picker(attachment: draft)) } + func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController) { + showTooManySelectedToast() + } +} + +extension SendMediaNavigationController: ImagePickerGridControllerDataSource { + + func imagePicker(_ imagePicker: ImagePickerGridController, isAssetSelected asset: PHAsset) -> Bool { + return attachmentDraftCollection.hasPickerAttachment(forAsset: asset) + } + func imagePickerCanSelectMoreItems(_ imagePicker: ImagePickerGridController) -> Bool { return attachmentCount < SignalAttachment.maxAttachmentsAllowed } - func imagePickerDidTryToSelectTooMany(_ imagePicker: ImagePickerGridController) { - showTooManySelectedToast() - } } extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegate { From ea2f1a0372760dab999985d6ec4c0b0528607ff9 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 15 Feb 2022 10:32:54 -0800 Subject: [PATCH 09/32] Change photo library picker to always be presented in "dark mode". --- .../Photos/ImagePickerController.swift | 14 +++++++------- .../Photos/SendMediaNavigationController.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 47106ba440..a535b057b1 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -65,6 +65,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = Theme.darkThemeBackgroundColor + library.add(delegate: self) guard let collectionView = collectionView else { @@ -72,14 +74,16 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return } collectionView.allowsMultipleSelection = true - collectionView.backgroundColor = Theme.backgroundColor + collectionView.backgroundColor = Theme.darkThemeBackgroundColor collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier) let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancel)) + cancelButton.tintColor = .ows_gray05 navigationItem.leftBarButtonItem = cancelButton let titleView = TitleView() titleView.delegate = self + titleView.tintColor = .ows_gray05 titleView.text = photoCollection.localizedTitle() navigationItem.titleView = titleView self.titleView = titleView @@ -90,7 +94,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat bottomBar.autoPinBottomToSuperviewMargin() bottomBar.autoPinHorizontalEdges(toEdgesOf: view) - cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28") { [weak self] in + cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28", userInterfaceStyleOverride: .dark) { [weak self] in guard let self = self else { return } self.didTapCameraButton() } @@ -102,6 +106,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat cameraButton.autoPinBottomToSuperviewMargin() doneButton = MediaDoneButton() + doneButton.userInterfaceStyleOverride = .dark doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) bottomBar.addSubview(doneButton) doneButton.autoPinTopToSuperviewMargin() @@ -267,11 +272,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - collectionView.backgroundColor = Theme.backgroundColor - } - private func updateDoneButtonAppearance() { guard let dataSource = dataSource else { return } diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 2c99a0f96c..8ae2be7ddf 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -248,7 +248,7 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { case is AttachmentApprovalViewController: showNavbar(.alwaysDarkAndClear) case is ImagePickerGridController: - showNavbar(.default) + showNavbar(.alwaysDarkAndClear) case is ConversationPickerViewController: showNavbar(.default) default: From bc9944c36f1a6e74bc9344e3a59c99911ca1bed0 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 15 Feb 2022 12:11:41 -0800 Subject: [PATCH 10/32] Photo library picker UI updates. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • new visual treatment for item selection indicator (checkmark) • Done and Camera buttons are more visible. --- .../Contents.json | 16 ++++ .../media-composer-checkmark.pdf | Bin 0 -> 17239 bytes .../Photos/ImagePickerController.swift | 4 +- .../Photos/MediaControls.swift | 4 +- Signal/src/views/PhotoGridViewCell.swift | 78 ++++++++++-------- 5 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json create mode 100644 Signal/Images.xcassets/media-composer-checkmark.imageset/media-composer-checkmark.pdf diff --git a/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json b/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json new file mode 100644 index 0000000000..3ce4894655 --- /dev/null +++ b/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "media-composer-checkmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Signal/Images.xcassets/media-composer-checkmark.imageset/media-composer-checkmark.pdf b/Signal/Images.xcassets/media-composer-checkmark.imageset/media-composer-checkmark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5e62edf61c2682be2c8f465ad267bca5d9a3079e GIT binary patch literal 17239 zcmeHPcbF4Z*9TOZVi(H?8bE4TW+r8lthB*!_s{OT~0*VL-3euZO z@ui5MG--m;K}0%AQ$awASorQtwz2F2`ueBy?CdkSbMEind&<4%+&ME{jdEoP(g$tV z_28k6Tidl2a9|GQ3AHOLgQ|it5>EyTB*8(TDh?vz@gh5aik+L1AW0pT=yw96$)AJdY!U0b;NKu`{Q#fjCH2_?uCWvKAVG6^ba zNQb~M?}{=+ENocD7xS;61EZ|chXrtggYY0TMbR7p#W6#gn2+M{iDa3`S@&2`M8uP0C3x zMUaq&BLXrcV}3fo5sDxShcB+ENYP0WkG5+Y*y|ZEU;sH_fCq2Z;=Lztnf}}zuek0L zKQMHabHb?Su{WPr{oLXP|H%7aT>s^={YP)uKJNz3vSx4TZXCwQxf-Ne;tXG^)kjCt zIDykR2e26M%82{P6o=`h8M5Z%BqRg(B9y;QTL9Z@>S9S+O6B^YeRyI-B;cTZL=pt$ za}czTSS;Znfe`d@!YM#X3{MyEJpwn*klU_0fbVf!+ zsH7#1dr9Vp1?GsD&Qw%DiX5%7&_F(*Tm{Mz(0}FzxMil8at;hxlfh^c4YCYmI9kU& z2ASb}KS(2!N%0i0T3{C(bN?D+WcW}mnno0)S~m~`s9lX-s9$|}FxK{S_?8<_7FIp8 z>@{M2CVgVUkuBZt8}{HZ>8VQ-LMv7*`x1Zqw@SF%cO4F2*mU}pdAf_zjnm`j9`6;~ zJ@@|A*rlh8>+UG+b7FW~(MIEmucR+2dJX&U+js8TIe*2$886u~;e`vgzH2XRF+A72 z*_p!~%CFQYW)RoxHV;@3n2-4N`#xr5-fGFVU-K;W#d||X%=z5guHSQWS`HOP@Hac& zXlku#-TY3edgo1;{HD7V@)zG9!P^m~l?KJwKC`?tZ{6|q@hvx;g^wQ<&ujnn>tpW~ ztH$d6Q@5Uc=;V3H`RvJ)d-i>He(afM z{N$KN60_bH9j!Wd|Cs!g5#3%k-Sxw%8xsozABEog?8sAxCA(I(9b$ieZcqPM=_L46 zaE#;Y`$veo%n)a{&lqu5{KTqnrmUN8I`sSQyFW0k3pIcAShvMkPiW0IxA_-Ubg%r+ znHJ5~(%T<-?BcEeH@scTty3P}zDW6*+ZJ24`}+q+^$=JGckTJZ2g6&w0L!|~9)9ee zQIp-1-U|L-^HEFBj1f=W<;lJIf?~q*<AG? zzDFG8ZJ*1vpZ(Rvj~8qyX*PKFjmu6S?)UEc- zTM?SFdyRFI=;F9rmp*#((~WEB-S3_rkXXkX{n5JTMj!7sd)E8t@sHeNt7ffyv2VwN zPjtL+CZF^7TtB=prTrUgKc7}L({|ve9lf@-OwVs`3170@^(?v5An5z`!iC=1&1dgy zHK)(%W~V>Cx!LzynwjUd+xPt5UrI&{YsLFnxbMK;-*<0pbMd_8K*iQc_L=VtdW-me zKyuQO*fSe{akXeMzD2kF&A9_^8}s}lx3_rq_7*EJ!7Hu4;B@@%xfXNoUW-XKH7lRq zs$_ge`uPXvJQ!*5`G806yJ7M2XX3XHe&LPfZGKk8AAG9Ktmpd;xU=eofdg8W-87ri zx-)jmD>u8aTZeMLyXjY%r^lVI4Kz>aJ-u14=dDBA^Q0x4+D>nEeBjFyI&O*`J-^9x z^hWyDMK?ZulMcIW{K@fe!!5Sr4@O!oUcO|`eXl=n+20}8=EMJl_jg-(*W;~jTR!RM ziVN^lEh=t%;)U)92L9Z;#qAHQP<6PYQ)lJF>QeJ7!#fow-*Zga@=X)bH+hR>&|_G) zH&ttI9{x(wbjv*x4+QRfXVALM56rvgC&j?c{g2|m2nsiimHIb*ePhezBljy>cTu+) z)Vi0ne8M1Dy;F1HmH5l?A+Ro|efZ8_<;{^7N6CA0?6<%B>SrrwuDoI8p_P>@7q4{P z`6{6}-B~eoi-tGs`E80_(p{>HgXz%%T|qzf&5mpT{O-nNCs*YO&TYfa5AECaiEjJ4 zES)%~$E@M7wp4)yC;wtv-D6FUdwXsj?y#b^M~93*Y~)V=X(=XKh3GB9rI)Btp}!=mo}rO?FxJNCoThqk=2b?L;uIDB7ha6sAV^JUI8 zJHHaIy711a-Gjr2z1@VCQS(O+8PyS8962{igD>KFM^%p2eTg0JbXak?OZ%6;Up;IN z@u7W3>mPdj&@w#oD{|1zW07`?-~Mdi)2}~0PIho%;-I9hzWb3IHuT)kzt4*FVeP`? ziMA*1JAX^|9o2Vja^}ZxnmzD*zr8n7tH*dB47}&xJD|ez=c~Uy{Lq2Lr>6Yg>zRix zaW0i!n)1?E#o(8=c!sQcX3+Zf8+%84PbwKPa?i+Cy=S|Y%vjVTeW9);r8;C4~}@SU-^<>UMzp1Y<;<^ z3@R*teEH*{*)t!1`_ndiC+u~fG@blE=^|L^?7o{j=JbKzCSRm44F0X`T>4bWFMIYs zx#Rr&Cx$*TV&ABJC3mQX_db4m$Ge_Ba`G5-{-IM#A6V8}-8yiy_|BiB;~nF9@qYWq zy|54qVjuiR-|~+x;)k6NecJZ|_DZE%v-ZhOqLpQdmlryI&-dsgKuIfBD_?)*3w7`RK79I*wO;@obZrZ46K&qG z`L&~CH=p;Ed3t%yttxx{?4a#yp1J$Csyn;6x=9`!xU%c&^cvmD)WOuDfrpmr<62|a zA9bTNw`e2!viIa_t!CFyS{CX)qB2|QA0`|2cGq$G3Dbyaw@iELt>dHLHGyzJR{5oPMEqf3(-TKn0gLO1r$h zRu%#d`{BCTrQY5S*}r>~qLnla=34)4G5L5mO4 z9~}Geu@5)Rl+Ro^t7O)M+3jZ!n$v7fdd~5W27I*Rzm@-8F;_fy*1WFs#>~HK{%Z@G zFUT+W`QyOH`xm+vZvI63$%;jfESkT#&*EvHcKY<)&+hr`jU{cDytw4j(!$cS%i_y^ zT<%|fXvGsNzWvm&l^a%7u3EcVx%%@p*qWtl%h!JTMd=rxtP`*Mcs;s)!I#1> z=YJ*qYW@av!-9>XjSDwPHZ9s*wt2~xzFU@Wm2X|WO|xzNcK!CvJ8V04e*M_j`*#vM zkA4&R=G3mtuHU|WZud>QU)yv0o{@Vy?47u;=f2td(fywtkRSN+pylAVhrEY=_%8L` z@87?8xZUBAM;Z&k-Q*{?>+;g5tB!(<$56whSPF+>8UQMV~=l+)U&4H*ROwt?==?})rJD*&< zv@<72ly`Orb+9fjBLhKoAxT;adaJh(_DYD(eJk3Q#OO@uzJL4dd zCriOKQw(+Huq1T2yfen!;5cYh zT*?=f^2Ie<0-Y<`g3Gcb;VU)EmDTNmU*(+xG#xL6plmkVC(G|cCH)X0kpK)H#N(9! zijq`5M&r4XSgK1Aqy|S$ro72uoDNbk4ugw(s5D*PxiiyH?NL3jXuP(eSgH>@$Ua^w z3gz%P#0b8LN-9m%fAxxfC>ZrORo!a5Mr|63H(ohK%Y)IYx13_5LO@2wQh)`3JB<%q zb#<7=+b6uWwc_bygcS?ng~$jAnxj*IXGl|n*6C`iHp=Q~w9%TuNF)s+QJkidjn}V7 z(4@%X^%Ux|#=#9xi4WCk5KxdZp!}sWuqOhVv{F>0qNw7Ux{h#oDA-`=p~Jnw7)_-D zJ_5L|?&dER?{!5=3Z4_@zQgDZ9H+3d8k4ij)m7*v*Oj^QlI~I zV1TKNrIlub1Rbd02~n0s^@{3xb!DXh8T1F}3Pi%It5scgwQ4?Q;-@RLMg?$NYdTO} zc(r=LD6AVa7m1zaV`YUH}K4#Nbd za72%BVT?Vb7@%hk1=g)g4toSMF_pn#!lV#afXTzku!pCOU@9wHjQJoBrU5toCWREU zDWrazQtA(@q$ZbEmi4Qoet(r5qbg-ts*(kiOEH}s%QB})jtPtkOl*)#bylU+WL4y{ z9#t;oRp-RM+B2(AOS4|R9D`k;1(zydGb_y$d!GfYC9GfRI1!O@ZO;=+ziAIujtOJxoe0t5E^AsAX6f3=iD%Yvc{-ihw3e21X0`;?>AZ zR23kVfd=H5-y{QNfFl4XrUSy#nF0Tm8nY&6vMW>yo+9XVDQvp9Dj>}{a;BhB$^zPG zbwHPM2gNdT-k(uKOa-$woT$vZ_(qxDW{QPTLqQ)PY;Ltx4yQF5Ev3`S0)BTu3s{%1 zz;;@1uaqY90?4Rf0TF{0b~_zltJZ!P_DGy`Mcpi5!V}Z-$e6G)6t8qd<7hCRNTgFq9L-Q> z4AX$&YvfWBP#2R+Ap;8GVR#!*$fYucMQ2keZDxHy<9Fx$ZbhC2q&(`ZQl~ZxEP9#J zt_^xKih@>dmiq&^#g=yjU7{+Ply9`?Nv*=4BW&TUpB=I`NM8z8$mI}`ar4zYgP&U^s!Ym_Vl1XM zLYOvhh=^19ENRQ=C0RHY;hFu>7#h-83J$A|2zz7RtO63VJ(NNTiPFh)syGk&}cd7SI^v<`Z0V&|hDNhpf z`dp}7kVz#nVOKcJ0wM;Xp^~mvCvE`Zdc8ibH>CAd>FA8mB<2-pt&j-2%qWqNhf_$n zpz@mwh)xO`&8oRc*zR+ut2@a8UZ4ExhE=@EM4eDfs|Ke6$E%Wgr?4uWKv}>bX)~no zoQ%&ELvBi=QgcOQA#F?%Qn8a~cjk2#3m)^vl#Z002qsf@52n>i(^eomAtcPDnNt-F zB4U&zs#rirnu>+=6gLFaAFC=zkXXzHq%0y)jW0!5SyULvz8a4OZR#HjSZuceiBFe- zfM~#}fnldJoXI;0JE9O%gbN8OGZG=UU^3~9k_KX70mI?T#KOZYxEzeoi4>qhhybS- z*lSV{aD4OVogYq%WNkqcLErgu=)(#932i0a5(#iG{Vs2C`uRs|ECr$c6>1 z7EpcJWQ=?VNm!vYKOIzy)HX;diP$Cj%cj0Yr#Tkz@+q=D9T0mKW2Oe|dS$}`*4oWK zAR892T0sA^Y``R3bVZn{Dr}BZ>5SGFPNT_$R%k+VNkx@URFFjdewR-~iwpv84p!%U zZo5yD(`pk$-fOSXjuaM#RbW2IBx7t3p_obpP604UXCW?xX&eobI$j7h>N3C@3AYe2 z0&Bc3yI}zv*hphPf!#3ESqMy~O-$WcDQi0{pbkt!m#s`fl~${_GUo3n&MdTF5%u%6 zIlIKmNLp zV=}MZhdG6`FNz3*kS4`? zBVYw_(`uM>`@~p{s$%NKL{YaY6@~IYi=s{nEMOC&$a)xtNy3muErw_lRK>R`;g~%d zPD7PSzSe;&>~4{nXAr|0u~7(%B<2iaX90a)?*C{X_F98r0spR5TrQ5{%)tWwU8|@Q z1#>0lX1Lw}3&_kNkTYYH2yxPrBqTaR)BxK9l)y#H<+5BNN%67~9fi5_5KSa7OllVf zY8?|Y?+eShJ|5+Zh!V1*vt&&aEKXlUCJHC@vdWZ%5~)dCL8`?l!KL_UB9r!6^;Q_N z>y5O;45NZjTBH(%91dRE6_smHU1cDm$=MZ5kiquwO6BLefrA)k&cC|Uuz*dhHz1S% zz8YA-Jb^ex`wX#|1py0~pnw}GAXTt{v2Gezz^DK>hZxxfj0Ln-i>pE8kTGNm6r}Z; zGKEUG3XYTdcv3>e4H1-*rUNK9Qe`EmsL|$2Nz8mHn0WZnSTT5{lbMLq=}DR-UfMzH z&9q98H`A~NauX_47m^bWwNq(}*G|(Jw=pk;j3S#cFF~Y)7IDX`GWh_Jw?~UoRys(S z2xas#qnt|^Ae%>Gck+#K5a{|6(HM_Qp@i4WCqhInS>h;FO?1vDsv#6lqZ9Udhu{7Cd8naB%v0Ps6QP>Q*eYY zG^K@=1ye9-#0@!qG@uSZ1{N^pOX#_Yq$ma#Y!V}4wTgT;!jRU8(z&z%iO|WY)}J?o zWR%;Yvzra|0+t5;FmQ!gKqg@MNBm*n3bTMrz*65I{`;{?p~yoqYRo&~Dsi5cNh+No z9Eb2qYn)7A9*WGwEifO~h7h&LXd|+AXI3wai<7uWXh(P`Y;;!HlUAF@oYsXsgv`ie zd+0Q$Lz*n#9zb&T$~sl$S+9FFu3LzULb=}6m7l35l|u7gf*~$f6FAAum%?J zFPa4E1DkScaEfp<&q_Ls8l^ZTmLMv5LX}|w9U{A<(g57^h{pgn5p;2`mf6NCs<+ir zw}VwP&)2H=l~X(mSnF3_?>v8{XHdH@S>z%3`9IJ7)zdx;c-6VT)<<__Wmc01?A~$H zz(r;PSjMxc-2tkeP+35B2k39_GF(1>7LZvA#VBM5i6asjq)VDImWY5C&G3R@)S2+B zP<0Zu(RyCcB{6dgh}x^Pi{&KltK_A*c7jOLawL!7FrtDzAr?@p;+sN}2<)NqIz8ed zymm{2c;x!`hFHKxHi#ODZJUZe>xG_xgaypmg$S2W3#(8i*X;<9k*XZ6_t8o%oUvni zux`L{+(RpJXh;%E7zC9`fl3T>?KB}ypdtcSCnE_Yp3qZ#l|^6dVL&aaqWI>p%ah2W zf<&Zl>8-atakUvjjMd1L1;nZ@^BJ3P7aMqwP5fj5d6j?5w`|m{WC0uZEw6v$qW;Do z3s|@N_qT;x>)o<|fF1vsm{~x5x}lg&QtefysH@FREFjp^zS0%F+(a8Xs!iTy0srdU zrOV_oCVp|bja3NPmJvk7;;cE3(s({q9C8($(xp;1Z3y?L-6}^SEsQ&Kgpv#Uf*_di zSsIUH8r4J=uv$bABng+-J^U>`*=_jjw+t#i(Z2jicd_*H2jDDHX`@ffncM)73{t8u zru-l*X}yBFMr$b5c*-731w9ecn#5x%@F<;(dGi%YCU2>>b`wNbLo;N^WIPGd8JdKu zqqq`Bhv%3Pu z*60AcK>E@(>0D$aLS?T>CXIkJug0m0rI&ZOmIaLtvW5eeEKYj^sw9<;|3x1B^)!!C zKhq4u?g}~S!_yI(Q*55Yw$FB0Rgb7noR?>Q6iJxml8RzNO?4%zpf=t(C5j>Tuh$kruSTi$9lz>8?2!XwP(pyaG0d3c-dj+m02Ht^-dC3}FMB*dG zs6;3#JE6I!iOcW7e@)P2L-lO>{GqE^>FK@jnyQ3frQG&^S#QTt5Da& z1dJ**CMhEAC6m=zW8l3?P9*3_;>kSo-Wtp=W0azfs=9Vj6YFIzvU*W01$mJmCljho z%Dh}zXX8PjEGMwXJ)t1GsDDowMMjjVoVmVLnJu{n`kd%nqD7ChNQDSKF&o-g`8W z?*3R^zWZZ6!He(|ado&FN2nU_3P-3XWMv%S9<2A;v+i}5;%6aXj-d7v5X|c&s8Glk za=`Z@ioqiD<%ugk;~yO0J>lIkp=_--_c+{)A0Wa CS(t?Y literal 0 HcmV?d00001 diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index a535b057b1..605eee9920 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -94,7 +94,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat bottomBar.autoPinBottomToSuperviewMargin() bottomBar.autoPinHorizontalEdges(toEdgesOf: view) - cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28", userInterfaceStyleOverride: .dark) { [weak self] in + cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28", userInterfaceStyleOverride: .light) { [weak self] in guard let self = self else { return } self.didTapCameraButton() } @@ -106,7 +106,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat cameraButton.autoPinBottomToSuperviewMargin() doneButton = MediaDoneButton() - doneButton.userInterfaceStyleOverride = .dark + doneButton.userInterfaceStyleOverride = .light doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) bottomBar.addSubview(doneButton) doneButton.autoPinTopToSuperviewMargin() diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index 99e87cdcff..ebba6c42cd 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -449,7 +449,7 @@ private extension UserInterfaceStyleOverride { case .dark: return .dark case .light: - return .prominent + return .extraLight default: fatalError("It is an error to pass UIUserInterfaceStyleUnspecified.") } @@ -460,7 +460,7 @@ private extension UserInterfaceStyleOverride { case .dark: return Theme.darkThemePrimaryColor case .light: - return Theme.lightThemePrimaryColor + return .ows_gray60 default: fatalError("It is an error to pass UIUserInterfaceStyleUnspecified.") } diff --git a/Signal/src/views/PhotoGridViewCell.swift b/Signal/src/views/PhotoGridViewCell.swift index d7cb107a5d..2d1a55c272 100644 --- a/Signal/src/views/PhotoGridViewCell.swift +++ b/Signal/src/views/PhotoGridViewCell.swift @@ -1,7 +1,10 @@ // -// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // +import SignalUI +import UIKit + public enum PhotoGridItemType { case photo, animated, video } @@ -18,8 +21,8 @@ public class PhotoGridViewCell: UICollectionViewCell { public let imageView: UIImageView private let contentTypeBadgeView: UIImageView - private let unselectedBadgeView: UIView - private let selectedBadgeView: UIImageView + private let outlineBadgeView: UIView + private let selectedBadgeView: UIView private let highlightedMaskView: UIView private let selectedMaskView: UIView @@ -28,7 +31,7 @@ public class PhotoGridViewCell: UICollectionViewCell { private static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video") private static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif") - private static let selectedBadgeImage = #imageLiteral(resourceName: "image_editor_checkmark_full").withRenderingMode(.alwaysTemplate) + private static let selectedBadgeImage = UIImage(named: "media-composer-checkmark") public var loadingColor = Theme.washColor override public var isSelected: Bool { @@ -45,15 +48,15 @@ public class PhotoGridViewCell: UICollectionViewCell { func updateSelectionState() { if isSelected { - unselectedBadgeView.isHidden = true + outlineBadgeView.isHidden = false selectedBadgeView.isHidden = false selectedMaskView.isHidden = false } else if allowsMultipleSelection { - unselectedBadgeView.isHidden = false + outlineBadgeView.isHidden = false selectedBadgeView.isHidden = true selectedMaskView.isHidden = true } else { - unselectedBadgeView.isHidden = true + outlineBadgeView.isHidden = true selectedBadgeView.isHidden = true selectedMaskView.isHidden = true } @@ -66,43 +69,49 @@ public class PhotoGridViewCell: UICollectionViewCell { } override init(frame: CGRect) { - self.imageView = UIImageView() + let selectionBadgeSize: CGFloat = 22 + let contentTypeBadgeSize: CGFloat = 12 + + imageView = UIImageView() imageView.contentMode = .scaleAspectFill - self.contentTypeBadgeView = UIImageView() + contentTypeBadgeView = UIImageView() contentTypeBadgeView.isHidden = true - self.selectedBadgeView = UIImageView() - selectedBadgeView.image = PhotoGridViewCell.selectedBadgeImage + selectedBadgeView = CircleView(diameter: selectionBadgeSize) + selectedBadgeView.backgroundColor = .ows_accentBlue selectedBadgeView.isHidden = true - selectedBadgeView.tintColor = .white + let checkmarkImageView = UIImageView(image: PhotoGridViewCell.selectedBadgeImage) + checkmarkImageView.tintColor = .white + selectedBadgeView.addSubview(checkmarkImageView) + checkmarkImageView.autoCenterInSuperview() - self.unselectedBadgeView = CircleView() - unselectedBadgeView.backgroundColor = .clear - unselectedBadgeView.layer.borderWidth = 0.5 - unselectedBadgeView.layer.borderColor = UIColor.white.cgColor + outlineBadgeView = CircleView() + outlineBadgeView.backgroundColor = .clear + outlineBadgeView.layer.borderWidth = 1.5 + outlineBadgeView.layer.borderColor = UIColor.ows_white.cgColor selectedBadgeView.isHidden = true - self.highlightedMaskView = UIView() + highlightedMaskView = UIView() highlightedMaskView.alpha = 0.2 highlightedMaskView.backgroundColor = Theme.darkThemePrimaryColor highlightedMaskView.isHidden = true - self.selectedMaskView = UIView() + selectedMaskView = UIView() selectedMaskView.alpha = 0.3 selectedMaskView.backgroundColor = Theme.darkThemeBackgroundColor selectedMaskView.isHidden = true super.init(frame: frame) - self.clipsToBounds = true + clipsToBounds = true - self.contentView.addSubview(imageView) - self.contentView.addSubview(contentTypeBadgeView) - self.contentView.addSubview(highlightedMaskView) - self.contentView.addSubview(selectedMaskView) - self.contentView.addSubview(unselectedBadgeView) - self.contentView.addSubview(selectedBadgeView) + contentView.addSubview(imageView) + contentView.addSubview(contentTypeBadgeView) + contentView.addSubview(highlightedMaskView) + contentView.addSubview(selectedMaskView) + contentView.addSubview(selectedBadgeView) + contentView.addSubview(outlineBadgeView) imageView.autoPinEdgesToSuperviewEdges() highlightedMaskView.autoPinEdgesToSuperviewEdges() @@ -110,20 +119,17 @@ public class PhotoGridViewCell: UICollectionViewCell { // Note assets were rendered to match exactly. We don't want to re-size with // content mode lest they become less legible. - let kContentTypeBadgeSize = CGSize(square: 12) + contentTypeBadgeView.autoSetDimensions(to: CGSize(square: contentTypeBadgeSize)) contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3) contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3) - contentTypeBadgeView.autoSetDimensions(to: kContentTypeBadgeSize) - let kUnselectedBadgeSize = CGSize(square: 22) - unselectedBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 4) - unselectedBadgeView.autoPinEdge(toSuperviewEdge: .top, withInset: 4) - unselectedBadgeView.autoSetDimensions(to: kUnselectedBadgeSize) + outlineBadgeView.autoSetDimensions(to: CGSize(square: selectionBadgeSize)) + outlineBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 6) + outlineBadgeView.autoPinEdge(toSuperviewEdge: .top, withInset: 6) - let kSelectedBadgeSize = CGSize(square: 22) - selectedBadgeView.autoSetDimensions(to: kSelectedBadgeSize) - selectedBadgeView.autoAlignAxis(.vertical, toSameAxisOf: unselectedBadgeView) - selectedBadgeView.autoAlignAxis(.horizontal, toSameAxisOf: unselectedBadgeView) + selectedBadgeView.autoSetDimensions(to: CGSize(square: selectionBadgeSize)) + selectedBadgeView.autoAlignAxis(.vertical, toSameAxisOf: outlineBadgeView) + selectedBadgeView.autoAlignAxis(.horizontal, toSameAxisOf: outlineBadgeView) } @available(*, unavailable, message: "Unimplemented") @@ -191,6 +197,6 @@ public class PhotoGridViewCell: UICollectionViewCell { highlightedMaskView.isHidden = true selectedMaskView.isHidden = true selectedBadgeView.isHidden = true - unselectedBadgeView.isHidden = true + outlineBadgeView.isHidden = true } } From a48c40b2ba881f704dfdc4c307e47f1c97ab4a74 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 15 Feb 2022 12:45:47 -0800 Subject: [PATCH 11/32] Make status bar visible when media sharing UI is presented. Media sharing UI is implemented in dark-only style which means status bar needs to be updated to be visible if the rest of application UI is in light mode. --- .../Photos/ImagePickerController.swift | 4 ++++ .../Photos/SendMediaNavigationController.swift | 12 ++++++++++++ SignalUI/ViewControllers/OWSNavigationController.h | 5 ++++- SignalUI/ViewControllers/OWSNavigationController.m | 7 ++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 605eee9920..89bc5fc665 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -62,6 +62,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat return true } + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 8ae2be7ddf..cfdba70c6d 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -54,6 +54,18 @@ class SendMediaNavigationController: OWSNavigationController { return true } + override var ows_preferredStatusBarStyle: UIStatusBarStyle { + get { + if topViewController is ConversationPickerViewController { + return .default + } + return .lightContent + } + set { + super.ows_preferredStatusBarStyle = newValue + } + } + override func viewDidLoad() { super.viewDidLoad() diff --git a/SignalUI/ViewControllers/OWSNavigationController.h b/SignalUI/ViewControllers/OWSNavigationController.h index de586f38c1..c238c212aa 100644 --- a/SignalUI/ViewControllers/OWSNavigationController.h +++ b/SignalUI/ViewControllers/OWSNavigationController.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import @@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN // regardless of which view is currently visible. @property (nonatomic, nullable) NSNumber *ows_prefersStatusBarHidden; +// Defaults to UIStatusBarStyleDefault. If set to a non-default value, this status bar style will be used. +@property (nonatomic, assign) UIStatusBarStyle ows_preferredStatusBarStyle; + - (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; - (instancetype)initWithNavigationBarClass:(nullable Class)navigationBarClass toolbarClass:(nullable Class)toolbarClass NS_UNAVAILABLE; diff --git a/SignalUI/ViewControllers/OWSNavigationController.m b/SignalUI/ViewControllers/OWSNavigationController.m index 11a6c5f8dc..71eddcadb6 100644 --- a/SignalUI/ViewControllers/OWSNavigationController.m +++ b/SignalUI/ViewControllers/OWSNavigationController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // #import "OWSNavigationController.h" @@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN selector:@selector(themeDidChange:) name:ThemeDidChangeNotification object:nil]; + self.ows_preferredStatusBarStyle = UIStatusBarStyleDefault; return self; } @@ -139,6 +140,10 @@ NS_ASSUME_NONNULL_BEGIN if (!CurrentAppContext().isMainApp) { return super.preferredStatusBarStyle; } else { + if (self.ows_preferredStatusBarStyle != UIStatusBarStyleDefault) { + return self.ows_preferredStatusBarStyle; + } + UIViewController *presentedViewController = self.presentedViewController; if (presentedViewController != nil && !presentedViewController.isBeingDismissed) { return presentedViewController.preferredStatusBarStyle; From 9ead1a01f5faa9114d84dd336baea417d15193ee Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 15 Feb 2022 18:46:45 -0800 Subject: [PATCH 12/32] A couple fixes for in-app camera. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • fix bug where camera preview wasn't positioned correctly during interactive dismiss. • better position of UI elements on non-notch devices. --- .../Photos/ImagePickerController.swift | 2 +- .../Photos/PhotoCaptureViewController.swift | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 89bc5fc665..e6843ba2e1 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -95,7 +95,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat bottomBar = UIView() view.addSubview(bottomBar) bottomBar.preservesSuperviewLayoutMargins = true - bottomBar.autoPinBottomToSuperviewMargin() + bottomBar.autoPinBottomToSuperviewMargin(withInset: UIDevice.current.hasIPhoneXNotch ? 0 : 8) bottomBar.autoPinHorizontalEdges(toEdgesOf: view) cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28", userInterfaceStyleOverride: .light) { [weak self] in diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index ad482fd444..4757e26857 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -92,7 +92,8 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate view.addSubview(bottomBar) bottomBar.autoPinWidthToSuperview() - bottomBarOffsetFromBottom = bottomBar.autoPinEdge(toSuperviewEdge: .bottom) + bottomBarOffsetFromBottom = view.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor) + view.addConstraint(bottomBarOffsetFromBottom) view.addSubview(cameraCaptureControl) if UIDevice.current.isIPad { @@ -100,7 +101,8 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate // captureButton.autoPinTrailing(toEdgeOf: view, offset: -captureButtonMargin) // captureButton.movieLockView.autoSetDimension(.width, toSize: 120) } else { - captureButtonVPositionConstraint = cameraCaptureControl.autoPinEdge(toSuperviewEdge: .bottom) + captureButtonVPositionConstraint = view.bottomAnchor.constraint(equalTo: cameraCaptureControl.bottomAnchor) + view?.addConstraint(captureButtonVPositionConstraint) cameraCaptureControl.autoPinLeadingToSuperviewMargin() cameraCaptureControl.autoPinTrailingToSuperviewMargin() } @@ -129,9 +131,9 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - guard !UIDevice.current.isIPad else { - return - } + guard !UIDevice.current.isIPad else { return } + + guard !interactiveDismiss.interactionInProgress else { return } // Clamp capture view to 16:9 var previewFrame = view.bounds @@ -147,18 +149,22 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate previewView.frame = previewFrame previewView.previewLayer.cornerRadius = cornerRadius - // Bottom bar is pinned to the bottom of the screen, residing either directly above safe area + // Bottom bar is pinned to the bottom of the screen, residing either directly above safe area / bottom margin // or (for taller screens) floating in the center of the black area between the bottom of the capture view and safe area. let bottomBarHeight = bottomBar.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .fittingSizeLevel, verticalFittingPriority: .fittingSizeLevel).height - let blackBarHeight = max(0, view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom) - bottomBarOffsetFromBottom.constant = -(max(0, (blackBarHeight - bottomBarHeight) / 2) + view.safeAreaInsets.bottom) + let blackBarHeight = view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom + var bottomBarOffset = UIDevice.current.hasIPhoneXNotch ? view.safeAreaInsets.bottom : 16 + if blackBarHeight > bottomBarHeight { + bottomBarOffset += 0.5*(blackBarHeight - bottomBarHeight) + } + bottomBarOffsetFromBottom.constant = bottomBarOffset - // Bottom edge of the capture button is 16pts above either bottom edge of the camera capture view - // or top of the bottom bar, whatever is higher. - let captureButtonBaseline = view.bounds.maxY - view.safeAreaInsets.bottom - max(blackBarHeight, bottomBarHeight) - captureButtonVPositionConstraint.constant = -(view.bounds.height - captureButtonBaseline + 16) + // Bottom edge of the capture button is either 16pts above bottom edge of the camera capture view + // or directly adjacent to the top of the bottom bar, whatever is higher. + let captureButtonOffsetFromBottom = max(view.bounds.maxY - (previewFrame.maxY - 16), bottomBarOffset + bottomBarHeight) + captureButtonVPositionConstraint.constant = captureButtonOffsetFromBottom } private var topBarOffsetFromTop: NSLayoutConstraint! From b53bda869b15def84d99fa5b2c6f214fb10c9499 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 15 Feb 2022 20:13:27 -0800 Subject: [PATCH 13/32] Make interactive dismiss of in-app camera look nicer. Let VC underneath in-app camera view controller to show during interactive dismiss, but only in the upper part of the screen. --- .../Photos/PhotoCaptureViewController.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 4757e26857..aac93dcf1c 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -79,6 +79,17 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate view.preservesSuperviewLayoutMargins = true definesPresentationContext = true + + // This background footer doesn't let view controller underneath current VC + // to be visible at the bottom of the screen during interactive dismiss. + if UIDevice.current.hasIPhoneXNotch { + let blackFooter = UIView() + blackFooter.backgroundColor = view.backgroundColor + view.addSubview(blackFooter) + blackFooter.autoPinWidthToSuperview() + blackFooter.autoPinEdge(toSuperviewEdge: .bottom) + blackFooter.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true + } view.addSubview(previewView) if UIDevice.current.isIPad { @@ -282,6 +293,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate // MARK: - Interactive Dismiss func interactiveDismissDidBegin(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { + view.backgroundColor = .clear } func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { @@ -289,6 +301,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } func interactiveDismissDidCancel(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { + view.backgroundColor = Theme.darkThemeBackgroundColor } // MARK: - Top Bar From 950d979b99439b8590c351443cf660a873439740 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Thu, 17 Feb 2022 14:02:31 -0800 Subject: [PATCH 14/32] Proper layout of in-app camera controls on iPads. --- .../Photos/ImagePickerController.swift | 9 +- .../Photos/MediaControls.swift | 254 +++++-- .../ViewControllers/Photos/PhotoCapture.swift | 10 +- .../Photos/PhotoCaptureViewController.swift | 686 +++++++++++------- 4 files changed, 635 insertions(+), 324 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index e6843ba2e1..6b89a1402f 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -37,7 +37,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat private var titleView: TitleView! private var bottomBar: UIView! - private var cameraButton: PhotoControl! + private var cameraButton: CameraOverlayButton! private var doneButton: MediaDoneButton! init() { @@ -98,10 +98,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat bottomBar.autoPinBottomToSuperviewMargin(withInset: UIDevice.current.hasIPhoneXNotch ? 0 : 8) bottomBar.autoPinHorizontalEdges(toEdgesOf: view) - cameraButton = PhotoControl(imageName: "media-composer-camera-outline-28", userInterfaceStyleOverride: .light) { [weak self] in - guard let self = self else { return } - self.didTapCameraButton() - } + cameraButton = CameraOverlayButton(image: UIImage(named: "media-composer-camera-outline-28"), userInterfaceStyleOverride: .light) + cameraButton.addTarget(self, action: #selector(didTapCameraButton), for: .touchUpInside) bottomBar.addSubview(cameraButton) cameraButton.contentInsets = .zero cameraButton.autoSetDimensions(to: .square(40)) @@ -339,6 +337,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat self.delegate?.imagePickerDidCancel(self) } + @objc private func didTapCameraButton() { delegate?.imagePickerDidRequestPresentCamera(self) } diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index ebba6c42cd..05a773e9df 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -15,12 +15,24 @@ protocol CameraCaptureControlDelegate: AnyObject { func cameraCaptureControlDidRequestCancelVideoRecording(_ control: CameraCaptureControl) // MARK: Zoom - var zoomScaleReferenceHeight: CGFloat? { get } - func cameraCaptureControl(_ control: CameraCaptureControl, didUpdate zoomAlpha: CGFloat) + var zoomScaleReferenceDistance: CGFloat? { get } + func cameraCaptureControl(_ control: CameraCaptureControl, didUpdateZoomLevel zoomLevel: CGFloat) } class CameraCaptureControl: UIView { + var axis: NSLayoutConstraint.Axis = .horizontal { + didSet { + if oldValue != axis { + reactivateConstraintsForCurrentAxis() + invalidateIntrinsicContentSize() + } + } + } + private var horizontalAxisConstraints = [NSLayoutConstraint]() + private var verticalAxisConstraints = [NSLayoutConstraint]() + + let shutterButtonLayoutGuide = UILayoutGuide() // allows view controller to align to shutter button. private let shutterButtonOuterCircle = CircleBlurView(effect: UIBlurEffect(style: .light)) private let shutterButtonInnerCircle = CircleView() @@ -30,7 +42,8 @@ class CameraCaptureControl: UIView { private var outerCircleSizeConstraint: NSLayoutConstraint! private var innerCircleSizeConstraint: NSLayoutConstraint! - private var slidingCirclePositionContstraint: NSLayoutConstraint! + private var slidingCircleHPositionConstraint: NSLayoutConstraint! + private var slidingCircleVPositionConstraint: NSLayoutConstraint! private lazy var slidingCircleView: CircleView = { let view = CircleView(diameter: CameraCaptureControl.recordingLockControlSize) @@ -53,6 +66,12 @@ class CameraCaptureControl: UIView { weak var delegate: CameraCaptureControlDelegate? + convenience init(axis: NSLayoutConstraint.Axis) { + self.init(frame: CGRect(origin: .zero, size: CameraCaptureControl.intrinsicContentSize(forAxis: axis))) + self.axis = axis + reactivateConstraintsForCurrentAxis() + } + override init(frame: CGRect) { super.init(frame: frame) commonInit() @@ -64,14 +83,28 @@ class CameraCaptureControl: UIView { } private func commonInit() { - autoSetDimension(.height, toSize: CameraCaptureControl.shutterButtonDefaultSize) // Round Shutter Button + addLayoutGuide(shutterButtonLayoutGuide) + shutterButtonLayoutGuide.widthAnchor.constraint(equalToConstant: CameraCaptureControl.shutterButtonDefaultSize).isActive = true + shutterButtonLayoutGuide.heightAnchor.constraint(equalToConstant: CameraCaptureControl.shutterButtonDefaultSize).isActive = true + horizontalAxisConstraints.append(contentsOf: [ + shutterButtonLayoutGuide.centerXAnchor.constraint(equalTo: leadingAnchor, constant: 0.5*CameraCaptureControl.shutterButtonDefaultSize), + shutterButtonLayoutGuide.topAnchor.constraint(equalTo: topAnchor), + shutterButtonLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor) + + ]) + verticalAxisConstraints.append(contentsOf: [ + shutterButtonLayoutGuide.leadingAnchor.constraint(equalTo: leadingAnchor), + shutterButtonLayoutGuide.trailingAnchor.constraint(equalTo: trailingAnchor), + shutterButtonLayoutGuide.centerYAnchor.constraint(equalTo: topAnchor, constant: 0.5*CameraCaptureControl.shutterButtonDefaultSize) + ]) + addSubview(shutterButtonOuterCircle) + shutterButtonOuterCircle.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor).isActive = true + shutterButtonOuterCircle.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor).isActive = true outerCircleSizeConstraint = shutterButtonOuterCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) shutterButtonOuterCircle.autoPin(toAspectRatio: 1) - shutterButtonOuterCircle.autoVCenterInSuperview() - shutterButtonOuterCircle.autoHCenterInSuperview() addSubview(shutterButtonInnerCircle) innerCircleSizeConstraint = shutterButtonInnerCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) @@ -88,6 +121,8 @@ class CameraCaptureControl: UIView { longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) longPressGesture.minimumPressDuration = 0 shutterButtonOuterCircle.addGestureRecognizer(longPressGesture) + + reactivateConstraintsForCurrentAxis() } // MARK: - UI State @@ -140,7 +175,7 @@ class CameraCaptureControl: UIView { switch state { case .initial: // element visibility - if slidingCirclePositionContstraint != nil { + if slidingCircleHPositionConstraint != nil { stopButton.alpha = 0 slidingCircleView.alpha = 0 lockIconView.alpha = 0 @@ -184,22 +219,46 @@ class CameraCaptureControl: UIView { } private func prepareRecordingControlsIfNecessary() { - guard slidingCirclePositionContstraint == nil else { return } + guard slidingCircleHPositionConstraint == nil else { return } + // 1. Stop button. addSubview(stopButton) stopButton.autoPin(toAspectRatio: 1) stopButton.autoSetDimension(.width, toSize: CameraCaptureControl.recordingLockControlSize) - stopButton.centerXAnchor.constraint(equalTo: shutterButtonOuterCircle.centerXAnchor).isActive = true - stopButton.centerYAnchor.constraint(equalTo: shutterButtonOuterCircle.centerYAnchor).isActive = true + stopButton.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor).isActive = true + stopButton.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor).isActive = true + // 2. Slider. insertSubview(slidingCircleView, belowSubview: shutterButtonInnerCircle) - slidingCircleView.autoVCenterInSuperview() - slidingCirclePositionContstraint = slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonOuterCircle.centerXAnchor, constant: 0) - addConstraint(slidingCirclePositionContstraint) + slidingCircleView.translatesAutoresizingMaskIntoConstraints = false + slidingCircleHPositionConstraint = slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor) + var horizontalConstraints: [NSLayoutConstraint] = [ slidingCircleHPositionConstraint ] + horizontalConstraints.append(slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor)) + slidingCircleVPositionConstraint = slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor) + var verticalConstraints: [NSLayoutConstraint] = [ slidingCircleVPositionConstraint ] + verticalConstraints.append(slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor)) + + // 3. Lock Icon addSubview(lockIconView) - lockIconView.autoVCenterInSuperview() - lockIconView.autoPinTrailing(toEdgeOf: self) + lockIconView.translatesAutoresizingMaskIntoConstraints = false + // Centered vertically, pinned to trailing edge. + horizontalConstraints.append(contentsOf: [ lockIconView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor), + lockIconView.trailingAnchor.constraint(equalTo: trailingAnchor) ]) + // Centered horizontally, pinned to bottom edge. + verticalConstraints.append(contentsOf: [ lockIconView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor), + lockIconView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) + + // 4. Activate current constraints. + horizontalAxisConstraints.append(contentsOf: horizontalConstraints) + if axis == .horizontal { + addConstraints(horizontalConstraints) + } + + verticalAxisConstraints.append(contentsOf: verticalConstraints) + if axis == .vertical { + addConstraints(verticalConstraints) + } setNeedsLayout() UIView.performWithoutAnimation { @@ -207,11 +266,48 @@ class CameraCaptureControl: UIView { } } + private func reactivateConstraintsForCurrentAxis() { + switch axis { + case .horizontal: + removeConstraints(verticalAxisConstraints) + addConstraints(horizontalAxisConstraints) + + case .vertical: + removeConstraints(horizontalAxisConstraints) + addConstraints(verticalAxisConstraints) + + @unknown default: + owsFailDebug("Unsupported `axis` value: \(axis.rawValue)") + } + } + + override var intrinsicContentSize: CGSize { + return Self.intrinsicContentSize(forAxis: axis) + } + + private static func intrinsicContentSize(forAxis axis: NSLayoutConstraint.Axis) -> CGSize { + switch axis { + case .horizontal: + return CGSize(width: CameraCaptureControl.shutterButtonDefaultSize + 64 + CameraCaptureControl.recordingLockControlSize, + height: CameraCaptureControl.shutterButtonDefaultSize) + + case .vertical: + return CGSize(width: CameraCaptureControl.shutterButtonDefaultSize, + height: CameraCaptureControl.shutterButtonDefaultSize + 64 + CameraCaptureControl.recordingLockControlSize) + + @unknown default: + owsFailDebug("Unsupported `axis` value: \(axis.rawValue)") + return CGSize(square: UIView.noIntrinsicMetric) + } + } + // MARK: - Gestures private var longPressGesture: UILongPressGestureRecognizer! private static let longPressDurationThreshold = 0.5 + private static let minDistanceBeforeActivatingLockSlider: CGFloat = 30 private var initialTouchLocation: CGPoint? + private var initialZoomPosition: CGFloat? private var touchTimer: Timer? @objc @@ -229,6 +325,7 @@ class CameraCaptureControl: UIView { guard state == .initial else { break } initialTouchLocation = gesture.location(in: gesture.view) + initialZoomPosition = nil touchTimer?.invalidate() touchTimer = WeakTimer.scheduledTimer( @@ -247,12 +344,12 @@ class CameraCaptureControl: UIView { case .changed: guard state == .recording else { break } - guard let referenceHeight = delegate?.zoomScaleReferenceHeight else { + guard let referenceDistance = delegate?.zoomScaleReferenceDistance else { owsFailDebug("referenceHeight was unexpectedly nil") return } - guard referenceHeight > 0 else { + guard referenceDistance > 0 else { owsFailDebug("referenceHeight was unexpectedly <= 0") return } @@ -264,17 +361,53 @@ class CameraCaptureControl: UIView { let currentLocation = gesture.location(in: gestureView) - // Zoom - let minDistanceBeforeActivatingZoom: CGFloat = 30 - let yDistance = initialTouchLocation.y - currentLocation.y - minDistanceBeforeActivatingZoom - let distanceForFullZoom = referenceHeight / 4 - let yRatio = yDistance / distanceForFullZoom - let yAlpha = yRatio.clamp(0, 1) - delegate?.cameraCaptureControl(self, didUpdate: yAlpha) + // Zoom - only use if slide to lock hasn't been activated. + var zoomLevel: CGFloat = 0 + if sliderTrackingProgress == 0 { + let minDistanceBeforeActivatingZoom: CGFloat = 30 + let currentDistance: CGFloat = { + switch axis { + case .horizontal: + if initialZoomPosition == nil { + initialZoomPosition = currentLocation.y + } + return initialZoomPosition! - currentLocation.y - minDistanceBeforeActivatingZoom - // Video Recording Lock - let xOffset = currentLocation.x - initialTouchLocation.x - updateTracking(xOffset: xOffset) + case .vertical: + if initialZoomPosition == nil { + initialZoomPosition = currentLocation.x + } + return initialZoomPosition! - currentLocation.x - minDistanceBeforeActivatingZoom + + @unknown default: + owsFailDebug("Unsupported `axis` value: \(axis.rawValue)") + return 0 + } + }() + + let distanceForFullZoom = referenceDistance / 4 + let ratio = currentDistance / distanceForFullZoom + zoomLevel = ratio.clamp(0, 1) + delegate?.cameraCaptureControl(self, didUpdateZoomLevel: zoomLevel) + } else { + initialZoomPosition = nil + } + + // Video Recording Lock - only works if zoom level == 0 + if zoomLevel == 0 { + switch axis { + case .horizontal: + let xOffset = currentLocation.x - initialTouchLocation.x + updateHorizontalTracking(xOffset: xOffset) + + case .vertical: + let yOffset = currentLocation.y - initialTouchLocation.y + updateVerticalTracking(yOffset: yOffset) + + @unknown default: + owsFailDebug("Unsupported `axis` value: \(axis.rawValue)") + } + } case .ended: touchTimer?.invalidate() @@ -305,17 +438,26 @@ class CameraCaptureControl: UIView { } } - private func updateTracking(xOffset: CGFloat) { - let minDistanceBeforeActivatingLockSlider: CGFloat = 30 - let effectiveDistance = xOffset - minDistanceBeforeActivatingLockSlider + private func updateHorizontalTracking(xOffset: CGFloat) { + let effectiveDistance = xOffset - Self.minDistanceBeforeActivatingLockSlider let distanceToLock = abs(lockIconView.center.x - stopButton.center.x) let trackingPosition = effectiveDistance.clamp(0, distanceToLock) - slidingCirclePositionContstraint.constant = trackingPosition + slidingCircleHPositionConstraint.constant = trackingPosition sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1) Logger.verbose("xOffset: \(xOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), trackingPosition: \(trackingPosition), progress: \(sliderTrackingProgress)") } + private func updateVerticalTracking(yOffset: CGFloat) { + let effectiveDistance = yOffset - Self.minDistanceBeforeActivatingLockSlider + let distanceToLock = abs(lockIconView.center.y - stopButton.center.y) + let trackingPosition = effectiveDistance.clamp(0, distanceToLock) + slidingCircleVPositionConstraint.constant = trackingPosition + sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1) + + Logger.verbose("yOffset: \(yOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), trackingPosition: \(trackingPosition), progress: \(sliderTrackingProgress)") + } + // MARK: - Button Actions private func didTapStopButton() { @@ -468,9 +610,9 @@ private extension UserInterfaceStyleOverride { } -class PhotoControl: UIView, UserInterfaceStyleOverride { +class CameraOverlayButton: UIButton, UserInterfaceStyleOverride { - var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified { + fileprivate var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified { didSet { if oldValue != userInterfaceStyleOverride { updateStyle() @@ -479,37 +621,47 @@ class PhotoControl: UIView, UserInterfaceStyleOverride { } private var backgroundView: UIVisualEffectView! - private let button: OWSButton private static let visibleButtonSize: CGFloat = 36 // both height and width private static let defaultInset: CGFloat = 4 - var contentInsets: UIEdgeInsets = UIEdgeInsets(margin: PhotoControl.defaultInset) { + var contentInsets: UIEdgeInsets = UIEdgeInsets(margin: CameraOverlayButton.defaultInset) { didSet { layoutMargins = contentInsets } } - init(imageName: String, userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified, block: @escaping () -> Void) { - button = OWSButton(imageName: imageName, tintColor: nil, block: block) - - super.init(frame: CGRect(origin: .zero, size: CGSize(square: Self.visibleButtonSize + 2*Self.defaultInset))) - - layoutMargins = contentInsets + convenience init(image: UIImage?, userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified) { + self.init(frame: CGRect(origin: .zero, size: .square(Self.visibleButtonSize + 2*Self.defaultInset))) self.userInterfaceStyleOverride = userInterfaceStyleOverride + setImage(image, for: .normal) + updateStyle() + } - backgroundView = CircleBlurView(effect: UIBlurEffect(style: PhotoControl.blurEffectStyle(for: effectiveUserInterfaceStyle))) + private override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + layoutMargins = contentInsets + + backgroundView = CircleBlurView(effect: UIBlurEffect(style: CameraOverlayButton.blurEffectStyle(for: effectiveUserInterfaceStyle))) + backgroundView.isUserInteractionEnabled = false addSubview(backgroundView) backgroundView.autoPinEdgesToSuperviewMargins() - addSubview(button) - button.autoPinEdgesToSuperviewMargins() - updateStyle() } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + override func layoutSubviews() { + super.layoutSubviews() + sendSubviewToBack(backgroundView) } override var intrinsicContentSize: CGSize { @@ -525,12 +677,8 @@ class PhotoControl: UIView, UserInterfaceStyleOverride { } private func updateStyle() { - backgroundView.effect = UIBlurEffect(style: PhotoControl.blurEffectStyle(for: effectiveUserInterfaceStyle)) - button.tintColor = PhotoControl.tintColor(for: effectiveUserInterfaceStyle) - } - - func setImage(imageName: String) { - button.setImage(imageName: imageName) + backgroundView.effect = UIBlurEffect(style: CameraOverlayButton.blurEffectStyle(for: effectiveUserInterfaceStyle)) + tintColor = CameraOverlayButton.tintColor(for: effectiveUserInterfaceStyle) } } diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index b54233a681..59ca0d0046 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -24,7 +24,7 @@ protocol PhotoCaptureDelegate: AnyObject { func photoCapture(_ photoCapture: PhotoCapture, didChangeOrientation: AVCaptureVideoOrientation) func photoCaptureCanCaptureMoreItems(_ photoCapture: PhotoCapture) -> Bool func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture) - var zoomScaleReferenceHeight: CGFloat? { get } + var zoomScaleReferenceDistance: CGFloat? { get } func beginCaptureButtonAnimation(_ duration: TimeInterval) func endCaptureButtonAnimation(_ duration: TimeInterval) @@ -690,12 +690,12 @@ extension PhotoCapture: CameraCaptureControlDelegate { completeMovieCapture() } - var zoomScaleReferenceHeight: CGFloat? { - return delegate?.zoomScaleReferenceHeight + var zoomScaleReferenceDistance: CGFloat? { + return delegate?.zoomScaleReferenceDistance } - func cameraCaptureControl(_ control: CameraCaptureControl, didUpdate zoomAlpha: CGFloat) { - updateZoom(alpha: zoomAlpha) + func cameraCaptureControl(_ control: CameraCaptureControl, didUpdateZoomLevel zoomLevel: CGFloat) { + updateZoom(alpha: zoomLevel) } } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index aac93dcf1c..3003828d82 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -50,27 +50,17 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate weak var delegate: PhotoCaptureViewControllerDelegate? weak var dataSource: PhotoCaptureViewControllerDataSource? - var interactiveDismiss: PhotoCaptureInteractiveDismiss! + private var interactiveDismiss: PhotoCaptureInteractiveDismiss! @objc public lazy var photoCapture = PhotoCapture() - - lazy var tapToFocusView: AnimationView = { - let view = AnimationView(name: "tap_to_focus") - view.animationSpeed = 1 - view.backgroundBehavior = .forceFinish - view.contentMode = .scaleAspectFit - view.autoSetDimensions(to: CGSize(square: 150)) - view.setContentHuggingHigh() - return view - }() - + deinit { UIDevice.current.endGeneratingDeviceOrientationNotifications() photoCapture.stopCapture().done { Logger.debug("stopCapture completed") } } - + // MARK: - Overrides override func loadView() { @@ -80,113 +70,20 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate definesPresentationContext = true - // This background footer doesn't let view controller underneath current VC - // to be visible at the bottom of the screen during interactive dismiss. - if UIDevice.current.hasIPhoneXNotch { - let blackFooter = UIView() - blackFooter.backgroundColor = view.backgroundColor - view.addSubview(blackFooter) - blackFooter.autoPinWidthToSuperview() - blackFooter.autoPinEdge(toSuperviewEdge: .bottom) - blackFooter.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true - } - - view.addSubview(previewView) - if UIDevice.current.isIPad { - previewView.autoPinEdgesToSuperviewEdges() - } - - view.addSubview(topBar) - topBar.mode = .cameraControls - topBar.autoPinWidthToSuperview() - topBarOffsetFromTop = topBar.autoPinEdge(toSuperviewEdge: .top) - - view.addSubview(bottomBar) - bottomBar.autoPinWidthToSuperview() - bottomBarOffsetFromBottom = view.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor) - view.addConstraint(bottomBarOffsetFromBottom) - - view.addSubview(cameraCaptureControl) - if UIDevice.current.isIPad { - // captureButton.autoVCenterInSuperview() - // captureButton.autoPinTrailing(toEdgeOf: view, offset: -captureButtonMargin) - // captureButton.movieLockView.autoSetDimension(.width, toSize: 120) - } else { - captureButtonVPositionConstraint = view.bottomAnchor.constraint(equalTo: cameraCaptureControl.bottomAnchor) - view?.addConstraint(captureButtonVPositionConstraint) - cameraCaptureControl.autoPinLeadingToSuperviewMargin() - cameraCaptureControl.autoPinTrailingToSuperviewMargin() - } - - view.addSubview(doneButton) - doneButton.isHidden = true - doneButton.autoPinTrailingToSuperviewMargin() - doneButton.centerYAnchor.constraint(equalTo: cameraCaptureControl.centerYAnchor).isActive = true - doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) + initializeUI() + } + + override func viewDidLoad() { + super.viewDidLoad() + setupPhotoCapture() // If the view is already visible, setup the volume button listener // now that the capture UI is ready. Otherwise, we'll wait until // we're visible. if isVisible { VolumeButtons.shared?.addObserver(observer: photoCapture) } - - view.addSubview(tapToFocusView) - tapToFocusView.isUserInteractionEnabled = false - tapToFocusLeftConstraint = tapToFocusView.centerXAnchor.constraint(equalTo: view.leftAnchor) - tapToFocusLeftConstraint.isActive = true - tapToFocusTopConstraint = tapToFocusView.centerYAnchor.constraint(equalTo: view.topAnchor) - tapToFocusTopConstraint.isActive = true - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - guard !UIDevice.current.isIPad else { return } - guard !interactiveDismiss.interactionInProgress else { return } - - // Clamp capture view to 16:9 - var previewFrame = view.bounds - var cornerRadius: CGFloat = 0 - let targetAspectRatio: CGFloat = 16/9 - let currentAspectRatio: CGFloat = previewFrame.height / previewFrame.width - - if abs(currentAspectRatio - targetAspectRatio) > 0.001 { - previewFrame.y = view.safeAreaInsets.top - previewFrame.height = previewFrame.width * targetAspectRatio - cornerRadius = 18 - } - previewView.frame = previewFrame - previewView.previewLayer.cornerRadius = cornerRadius - - // Bottom bar is pinned to the bottom of the screen, residing either directly above safe area / bottom margin - // or (for taller screens) floating in the center of the black area between the bottom of the capture view and safe area. - let bottomBarHeight = bottomBar.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, - withHorizontalFittingPriority: .fittingSizeLevel, - verticalFittingPriority: .fittingSizeLevel).height - let blackBarHeight = view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom - var bottomBarOffset = UIDevice.current.hasIPhoneXNotch ? view.safeAreaInsets.bottom : 16 - if blackBarHeight > bottomBarHeight { - bottomBarOffset += 0.5*(blackBarHeight - bottomBarHeight) - } - bottomBarOffsetFromBottom.constant = bottomBarOffset - - // Bottom edge of the capture button is either 16pts above bottom edge of the camera capture view - // or directly adjacent to the top of the bottom bar, whatever is higher. - let captureButtonOffsetFromBottom = max(view.bounds.maxY - (previewFrame.maxY - 16), bottomBarOffset + bottomBarHeight) - captureButtonVPositionConstraint.constant = captureButtonOffsetFromBottom - } - - private var topBarOffsetFromTop: NSLayoutConstraint! - private var bottomBarOffsetFromBottom: NSLayoutConstraint! - private var captureButtonVPositionConstraint: NSLayoutConstraint! - - override func viewDidLoad() { - super.viewDidLoad() - - setupPhotoCapture() - updateFlashModeControl() view.addGestureRecognizer(pinchZoomGesture) @@ -200,8 +97,11 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture) - - mediaPickerThumbnailButton.configure() + + bottomBar.photoLibraryButton.configure() + if let sideBar = sideBar { + sideBar.photoLibraryButton.configure() + } } private var isVisible = false @@ -289,6 +189,230 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate topBarOffsetFromTop.constant = max(maxInsetDimension, previewView.frame.minY) } } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + isIPadUIInRegularMode = traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .regular + } + + // MARK: - Layout Code + + private var isIPadUIInRegularMode = false { + didSet { + guard oldValue != isIPadUIInRegularMode else { return } + updateIPadInterfaceLayout() + } + } + + private var isRecordingVideo: Bool = false { + didSet { + if isRecordingVideo { + topBar.mode = .videoRecording + topBar.recordingTimerView.startCounting() + + cameraCaptureControl.setState(.recording, animationDuration: 0.4) + if let sideBar = sideBar { + sideBar.cameraCaptureControl.setState(.recording, animationDuration: 0.4) + } + } else { + topBar.mode = isIPadUIInRegularMode ? .closeButton : .cameraControls + topBar.recordingTimerView.stopCounting() + + cameraCaptureControl.setState(.initial, animationDuration: 0.2) + if let sideBar = sideBar { + sideBar.cameraCaptureControl.setState(.initial, animationDuration: 0.2) + } + } + + if let sideBar = sideBar { + sideBar.isRecordingVideo = isRecordingVideo + } + + doneButton.isHidden = isRecordingVideo || doneButton.badgeNumber == 0 + bottomBar.isHidden = isRecordingVideo + } + } + + private var isInBatchMode: Bool = false { + didSet { + let buttonImage = isInBatchMode ? ButtonImages.batchModeOn : ButtonImages.batchModeOff + topBar.batchModeButton.setImage(buttonImage, for: .normal) + if let sideBar = sideBar { + sideBar.batchModeButton.setImage(buttonImage, for: .normal) + } + } + } + + private let topBar = TopBar(frame: .zero) + private var topBarOffsetFromTop: NSLayoutConstraint! + + private let bottomBar = BottomBar(frame: .zero) + private var bottomBarOffsetFromBottom: NSLayoutConstraint! + + private var sideBar: SideBar? // Optional because most devices are iPhones and will never need this. + + private let cameraCaptureControl = CameraCaptureControl(axis: .horizontal) + private var captureButtonVPositionConstraint: NSLayoutConstraint! + + private lazy var tapToFocusView: AnimationView = { + let view = AnimationView(name: "tap_to_focus") + view.animationSpeed = 1 + view.backgroundBehavior = .forceFinish + view.contentMode = .scaleAspectFit + view.autoSetDimensions(to: CGSize(square: 150)) + view.setContentHuggingHigh() + return view + }() + + private var previewView: CapturePreviewView { + return photoCapture.previewView + } + + private lazy var doneButton: MediaDoneButton = { + let button = MediaDoneButton(type: .custom) + button.badgeNumber = 0 + button.userInterfaceStyleOverride = .dark + return button + }() + private var doneButtonIPhoneConstraints: [NSLayoutConstraint]! + private var doneButtonIPadConstraints: [NSLayoutConstraint]! + + private func initializeUI() { + // Step 1. Initialize all UI elements for iPhone layout (which can also be used on an iPad). + + view.addSubview(previewView) + + view.addSubview(topBar) + topBar.mode = .cameraControls + topBar.closeButton.addTarget(self, action: #selector(didTapClose), for: .touchUpInside) + topBar.batchModeButton.addTarget(self, action: #selector(didTapBatchMode), for: .touchUpInside) + topBar.flashModeButton.addTarget(self, action: #selector(didTapFlashMode), for: .touchUpInside) + topBar.autoPinWidthToSuperview() + topBarOffsetFromTop = topBar.autoPinEdge(toSuperviewEdge: .top) + + view.addSubview(bottomBar) + bottomBar.switchCameraButton.addTarget(self, action: #selector(didTapSwitchCamera), for: .touchUpInside) + bottomBar.photoLibraryButton.addTarget(self, action: #selector(didTapPhotoLibrary), for: .touchUpInside) + bottomBar.autoPinWidthToSuperview() + bottomBarOffsetFromBottom = view.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor) + view.addConstraint(bottomBarOffsetFromBottom) + + view.addSubview(cameraCaptureControl) + captureButtonVPositionConstraint = view.bottomAnchor.constraint(equalTo: cameraCaptureControl.bottomAnchor) + view.addConstraint(captureButtonVPositionConstraint) + cameraCaptureControl.shutterButtonLayoutGuide.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + cameraCaptureControl.autoPinTrailingToSuperviewMargin() + + view.addSubview(doneButton) + doneButton.isHidden = true + doneButton.translatesAutoresizingMaskIntoConstraints = false + doneButtonIPhoneConstraints = [ doneButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + doneButton.centerYAnchor.constraint(equalTo: cameraCaptureControl.centerYAnchor) ] + view.addConstraints(doneButtonIPhoneConstraints) + doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) + + view.addSubview(tapToFocusView) + tapToFocusView.isUserInteractionEnabled = false + tapToFocusLeftConstraint = tapToFocusView.centerXAnchor.constraint(equalTo: view.leftAnchor) + tapToFocusLeftConstraint.isActive = true + tapToFocusTopConstraint = tapToFocusView.centerYAnchor.constraint(equalTo: view.topAnchor) + tapToFocusTopConstraint.isActive = true + + // Step 2. Check if we're running on an iPad and update UI accordingly. + // Note that `traitCollectionDidChange` won't be called during initial view loading process. + isIPadUIInRegularMode = traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .regular + + // This background footer doesn't let view controller underneath current VC + // to be visible at the bottom of the screen during interactive dismiss. + if UIDevice.current.hasIPhoneXNotch { + let blackFooter = UIView() + blackFooter.backgroundColor = view.backgroundColor + view.insertSubview(blackFooter, at: 0) + blackFooter.autoPinWidthToSuperview() + blackFooter.autoPinEdge(toSuperviewEdge: .bottom) + blackFooter.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true + } + } + + private func initializeIPadSpecificUIIfNecessary() { + guard sideBar == nil else { return } + + let sideBar = SideBar(frame: .zero) + sideBar.batchModeButton.addTarget(self, action: #selector(didTapBatchMode), for: .touchUpInside) + sideBar.flashModeButton.addTarget(self, action: #selector(didTapFlashMode), for: .touchUpInside) + sideBar.switchCameraButton.addTarget(self, action: #selector(didTapSwitchCamera), for: .touchUpInside) + sideBar.photoLibraryButton.addTarget(self, action: #selector(didTapPhotoLibrary), for: .touchUpInside) + view.addSubview(sideBar) + sideBar.autoPinTrailingToSuperviewMargin() + sideBar.cameraCaptureControl.shutterButtonLayoutGuide.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + self.sideBar = sideBar + + sideBar.batchModeButton.setImage(isInBatchMode ? ButtonImages.batchModeOn : ButtonImages.batchModeOff, for: .normal) + updateFlashModeControl() + + doneButtonIPadConstraints = [ doneButton.centerXAnchor.constraint(equalTo: sideBar.centerXAnchor), + doneButton.bottomAnchor.constraint(equalTo: sideBar.topAnchor, constant: -16)] + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + guard !interactiveDismiss.interactionInProgress else { return } + + // Clamp capture view to 16:9 on iPhones. + var previewFrame = view.bounds + var cornerRadius: CGFloat = 0 + if !UIDevice.current.isIPad { + let targetAspectRatio: CGFloat = 16/9 + let currentAspectRatio: CGFloat = previewFrame.height / previewFrame.width + + if abs(currentAspectRatio - targetAspectRatio) > 0.001 { + previewFrame.y = view.safeAreaInsets.top + previewFrame.height = previewFrame.width * targetAspectRatio + cornerRadius = 18 + } + } + previewView.frame = previewFrame + previewView.previewLayer.cornerRadius = cornerRadius + + // Bottom bar is pinned to the bottom of the screen, residing either directly above safe area / bottom margin + // or (for taller screens) floating in the center of the black area between the bottom of the capture view and safe area. + let bottomBarHeight = bottomBar.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .fittingSizeLevel).height + let blackBarHeight = view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom + var bottomBarOffset = UIDevice.current.hasIPhoneXNotch ? view.safeAreaInsets.bottom : 16 + if blackBarHeight > bottomBarHeight { + bottomBarOffset += 0.5*(blackBarHeight - bottomBarHeight) + } + bottomBarOffsetFromBottom.constant = bottomBarOffset + + // Bottom edge of the capture button is either 16pts above bottom edge of the camera capture view + // or directly adjacent to the top of the bottom bar, whatever is higher. + let captureButtonOffsetFromBottom = max(view.bounds.maxY - (previewFrame.maxY - 16), bottomBarOffset + bottomBarHeight) + captureButtonVPositionConstraint.constant = captureButtonOffsetFromBottom + } + + private func updateIPadInterfaceLayout() { + owsAssertDebug(UIDevice.current.isIPad) + + if isIPadUIInRegularMode { + initializeIPadSpecificUIIfNecessary() + + view.removeConstraints(doneButtonIPhoneConstraints) + view.addConstraints(doneButtonIPadConstraints) + } else { + view.removeConstraints(doneButtonIPadConstraints) + view.addConstraints(doneButtonIPhoneConstraints) + } + + if !isRecordingVideo { + topBar.mode = isIPadUIInRegularMode ? .closeButton : .cameraControls + } + cameraCaptureControl.isHidden = isIPadUIInRegularMode + bottomBar.isHidden = isIPadUIInRegularMode + sideBar?.isHidden = !isIPadUIInRegularMode + } // MARK: - Interactive Dismiss @@ -305,109 +429,177 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } // MARK: - Top Bar - - private var isRecordingVideo: Bool = false { - didSet { - if isRecordingVideo { - topBar.mode = .videoRecording - topBar.recordingTimerView.startCounting() - - cameraCaptureControl.setState(.recording, animationDuration: 0.4) - } else { - topBar.mode = .cameraControls - topBar.recordingTimerView.stopCounting() - - cameraCaptureControl.setState(.initial, animationDuration: 0.2) - } - - doneButton.isHidden = isRecordingVideo || doneButton.badgeNumber == 0 - bottomBar.isHidden = isRecordingVideo - } + + private struct ButtonImages { + static let close = UIImage(named: "media-composer-close") + static let switchCamera = UIImage(named: "media-composer-switch-camera-24") + + static let batchModeOn = UIImage(named: "media-composer-create-album-solid-24") + static let batchModeOff = UIImage(named: "media-composer-create-album-outline-24") + + static let flashOn = UIImage(named: "media-composer-flash-filled-24") + static let flashOff = UIImage(named: "media-composer-flash-outline-24") + static let flashAuto = UIImage(named: "media-composer-flash-auto-24") } - private var isInBatchMode: Bool = false { - didSet { - let imageName = isInBatchMode ? "media-composer-create-album-solid-24" : "media-composer-create-album-outline-24" - batchModeControl.setImage(imageName: imageName) - } - } - + private class TopBar: UIView { - let recordingTimerView = RecordingTimerView(frame: .zero) - private let navStack: UIStackView - - init(navbarItems: [UIView]) { - self.navStack = UIStackView(arrangedSubviews: navbarItems) - - super.init(frame: .zero) - - layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4) - - addSubview(navStack) - navStack.spacing = 16 - navStack.autoPinEdgesToSuperviewMargins() - - addSubview(recordingTimerView) - recordingTimerView.autoPinHeightToSuperview(withMargin: 8) - recordingTimerView.autoHCenterInSuperview() + private(set) var closeButton: CameraOverlayButton! + + private var cameraControlsContainerView: UIView! + private(set) var flashModeButton: CameraOverlayButton! + private(set) var batchModeButton: CameraOverlayButton! + + private(set) var recordingTimerView: RecordingTimerView! + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) + commonInit() } - + + private func commonInit() { + layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4) + + closeButton = CameraOverlayButton(image: ButtonImages.close, userInterfaceStyleOverride: .dark) + addSubview(closeButton) + closeButton.autoPinHeightToSuperviewMargins() + closeButton.autoPinLeadingToSuperviewMargin() + + recordingTimerView = RecordingTimerView(frame: .zero) + addSubview(recordingTimerView) + recordingTimerView.autoPinHeightToSuperview(withMargin: 8) + recordingTimerView.autoHCenterInSuperview() + + flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) + batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) + let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton ]) + stackView.spacing = 16 + addSubview(stackView) + stackView.autoPinHeightToSuperviewMargins() + stackView.autoPinTrailingToSuperviewMargin() + cameraControlsContainerView = stackView + } + + // MARK: - Mode + enum Mode { - case cameraControls, videoRecording + case cameraControls, closeButton, videoRecording } var mode: Mode = .cameraControls { didSet { switch mode { - case .videoRecording: - navStack.isHidden = true - recordingTimerView.isHidden = false case .cameraControls: - navStack.isHidden = false - recordingTimerView.isHidden = true + closeButton.isHidden = false + cameraControlsContainerView.isHidden = false + recordingTimerView?.isHidden = true + + case .closeButton: + closeButton.isHidden = false + cameraControlsContainerView.isHidden = true + recordingTimerView?.isHidden = true + + case .videoRecording: + closeButton.isHidden = true + cameraControlsContainerView.isHidden = true + recordingTimerView.isHidden = false } } } } - - private lazy var topBar: TopBar = { - return TopBar(navbarItems: [dismissControl, - UIView.hStretchingSpacer(), - batchModeControl, - flashModeControl]) - }() - + // MARK: - Bottom Bar private class BottomBar: UIView { - let buttonStack: UIStackView + private(set) var photoLibraryButton: MediaPickerThumbnailButton! + private(set) var switchCameraButton: CameraOverlayButton! - init(items: [UIView]) { - self.buttonStack = UIStackView(arrangedSubviews: items) - - super.init(frame: .zero) - - layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) - - addSubview(buttonStack) - buttonStack.spacing = 16 - buttonStack.autoPinEdgesToSuperviewMargins() + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) + + photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) + addSubview(photoLibraryButton) + photoLibraryButton.autoVCenterInSuperview() + photoLibraryButton.autoPinLeadingToSuperviewMargin() + + switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) + addSubview(switchCameraButton) + switchCameraButton.autoPinHeightToSuperviewMargins() + switchCameraButton.autoPinTrailingToSuperviewMargin() } } - - private lazy var bottomBar: BottomBar = { - return BottomBar(items: [mediaPickerThumbnailButton.embeddedInContainerView(layoutMargins: UIEdgeInsets(margin: 4)), - UIView.hStretchingSpacer(), - switchCameraControl]) - }() - + + // MARK: - Side Bar + + private class SideBar: UIView { + var isRecordingVideo = false { + didSet { + cameraControlsContainerView.isHidden = isRecordingVideo + photoLibraryButton.isHidden = isRecordingVideo + } + } + + private var cameraControlsContainerView: UIView! + private(set) var flashModeButton: CameraOverlayButton! + private(set) var batchModeButton: CameraOverlayButton! + private(set) var switchCameraButton: CameraOverlayButton! + + private(set) var photoLibraryButton: MediaPickerThumbnailButton! + + private(set) var cameraCaptureControl = CameraCaptureControl(axis: .vertical) + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + layoutMargins = UIEdgeInsets(margin: 8) + + flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) + switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) + batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) + let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton, switchCameraButton ]) + stackView.spacing = 16 + stackView.axis = .vertical + addSubview(stackView) + stackView.autoPinWidthToSuperviewMargins() + stackView.autoPinTopToSuperviewMargin() + cameraControlsContainerView = stackView + + addSubview(cameraCaptureControl) + cameraCaptureControl.autoHCenterInSuperview() + cameraCaptureControl.shutterButtonLayoutGuide.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 36).isActive = true + + photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) + addSubview(photoLibraryButton) + photoLibraryButton.autoHCenterInSuperview() + photoLibraryButton.topAnchor.constraint(equalTo: cameraCaptureControl.shutterButtonLayoutGuide.bottomAnchor, constant: 36).isActive = true + photoLibraryButton.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor).isActive = true + + } + } + + func updateDoneButtonAppearance () { if isInBatchMode, let badgeNumber = dataSource?.numberOfMediaItems { doneButton.badgeNumber = badgeNumber @@ -417,51 +609,8 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } } - // MARK: - Views - - private let cameraCaptureControl = CameraCaptureControl() - - private var previewView: CapturePreviewView { - return photoCapture.previewView - } - - private lazy var mediaPickerThumbnailButton: MediaPickerThumbnailButton = { - let button = MediaPickerThumbnailButton(frame: .zero) - button.addTarget(self, action: #selector(didTapPhotoLibrary), for: .touchUpInside) - return button - }() - - private lazy var dismissControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-close", userInterfaceStyleOverride: .dark) { [weak self] in - self?.didTapClose() - } - }() - - private lazy var switchCameraControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-switch-camera-24", userInterfaceStyleOverride: .dark) { [weak self] in - self?.didTapSwitchCamera() - } - }() - - private lazy var flashModeControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-flash-auto-24", userInterfaceStyleOverride: .dark) { [weak self] in - self?.didTapFlashMode() - } - }() - - private lazy var batchModeControl: PhotoControl = { - return PhotoControl(imageName: "media-composer-create-album-outline-24", userInterfaceStyleOverride: .dark) { [weak self] in - self?.didTapBatchMode() - } - }() - - private lazy var doneButton: MediaDoneButton = { - let button = MediaDoneButton(type: .custom) - button.badgeNumber = 0 - button.userInterfaceStyleOverride = .dark - return button - }() - + // MARK: - Gestures + lazy var pinchZoomGesture: UIPinchGestureRecognizer = { return UIPinchGestureRecognizer(target: self, action: #selector(didPinchZoom(pinchGesture:))) }() @@ -500,9 +649,11 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } private func switchCamera() { + let switchCameraButton = bottomBar.switchCameraButton! + ///TODO: side bar UIView.animate(withDuration: 0.2) { let epsilonToForceCounterClockwiseRotation: CGFloat = 0.00001 - self.switchCameraControl.transform = self.switchCameraControl.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation) + switchCameraButton.transform = switchCameraButton.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation) } photoCapture.switchCamera().catch { error in self.showFailureUI(error: error) @@ -596,23 +747,22 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate // MARK: - Focus Animations - var tapToFocusLeftConstraint: NSLayoutConstraint! - var tapToFocusTopConstraint: NSLayoutConstraint! - func positionTapToFocusView(center: CGPoint) { + private var tapToFocusLeftConstraint: NSLayoutConstraint! + private var tapToFocusTopConstraint: NSLayoutConstraint! + private var lastUserFocusTapPoint: CGPoint? + + private func positionTapToFocusView(center: CGPoint) { tapToFocusLeftConstraint.constant = center.x tapToFocusTopConstraint.constant = center.y } - func startFocusAnimation() { + private func startFocusAnimation() { tapToFocusView.stop() tapToFocusView.play(fromProgress: 0.0, toProgress: 0.9) } - var lastUserFocusTapPoint: CGPoint? - func completeFocusAnimation(forFocusPoint focusPoint: CGPoint) { - guard let lastUserFocusTapPoint = lastUserFocusTapPoint else { - return - } + private func completeFocusAnimation(forFocusPoint focusPoint: CGPoint) { + guard let lastUserFocusTapPoint = lastUserFocusTapPoint else { return } guard lastUserFocusTapPoint.within(0.005, of: focusPoint) else { Logger.verbose("focus completed for obsolete focus point. User has refocused.") @@ -625,9 +775,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate // MARK: - Orientation private func updateIconOrientations(isAnimated: Bool, captureOrientation: AVCaptureVideoOrientation) { - guard !UIDevice.current.isIPad else { - return - } + guard !UIDevice.current.isIPad else { return } Logger.verbose("captureOrientation: \(captureOrientation)") @@ -650,8 +798,8 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate let tranformFromCameraType: CGAffineTransform = photoCapture.desiredPosition == .front ? CGAffineTransform(rotationAngle: -.pi) : .identity let updateOrientation = { - self.flashModeControl.transform = transformFromOrientation - self.switchCameraControl.transform = transformFromOrientation.concatenating(tranformFromCameraType) + self.topBar.flashModeButton?.transform = transformFromOrientation + self.bottomBar.switchCameraButton?.transform = transformFromOrientation.concatenating(tranformFromCameraType) } if isAnimated { @@ -673,6 +821,9 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate private func setupPhotoCapture() { photoCapture.delegate = self cameraCaptureControl.delegate = photoCapture + if let sideBar = sideBar { + sideBar.cameraCaptureControl.delegate = photoCapture + } // If the session is already running, we're good to go. guard !photoCapture.session.isRunning else { @@ -719,21 +870,25 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } private func updateFlashModeControl() { - let imageName: String + let image: UIImage? switch photoCapture.flashMode { case .auto: - imageName = "media-composer-flash-auto-24" + image = ButtonImages.flashAuto + case .on: - imageName = "media-composer-flash-filled-24" + image = ButtonImages.flashOn + case .off: - imageName = "media-composer-flash-outline-24" + image = ButtonImages.flashOff + @unknown default: owsFailDebug("unexpected photoCapture.flashMode: \(photoCapture.flashMode.rawValue)") - - imageName = "media-composer-flash-auto-24" + image = ButtonImages.flashAuto + } + topBar.flashModeButton.setImage(image, for: .normal) + if let sideBar = sideBar { + sideBar.flashModeButton.setImage(image, for: .normal) } - - self.flashModeControl.setImage(imageName: imageName) } } @@ -803,16 +958,25 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { // MARK: - - var zoomScaleReferenceHeight: CGFloat? { + var zoomScaleReferenceDistance: CGFloat? { + if isIPadUIInRegularMode { + return view.bounds.width + } return view.bounds.height } func beginCaptureButtonAnimation(_ duration: TimeInterval) { cameraCaptureControl.setState(.recording, animationDuration: duration) + if let sideBar = sideBar { + sideBar.cameraCaptureControl.setState(.recording, animationDuration: duration) + } } func endCaptureButtonAnimation(_ duration: TimeInterval) { cameraCaptureControl.setState(.initial, animationDuration: duration) + if let sideBar = sideBar { + sideBar.cameraCaptureControl.setState(.initial, animationDuration: duration) + } } func photoCapture(_ photoCapture: PhotoCapture, didChangeOrientation orientation: AVCaptureVideoOrientation) { From 242cf0d32d99af0b2643691eba982e0dcb9ad359 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 22 Feb 2022 11:05:13 -0800 Subject: [PATCH 15/32] Fix rotation of in-app camera buttons when device orientation changes. --- .../Photos/PhotoCaptureViewController.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 3003828d82..476d56fd67 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -599,7 +599,6 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } } - func updateDoneButtonAppearance () { if isInBatchMode, let badgeNumber = dataSource?.numberOfMediaItems { doneButton.badgeNumber = badgeNumber @@ -649,11 +648,11 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } private func switchCamera() { - let switchCameraButton = bottomBar.switchCameraButton! - ///TODO: side bar - UIView.animate(withDuration: 0.2) { - let epsilonToForceCounterClockwiseRotation: CGFloat = 0.00001 - switchCameraButton.transform = switchCameraButton.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation) + if let switchCameraButton = isIPadUIInRegularMode ? sideBar?.switchCameraButton : bottomBar.switchCameraButton { + UIView.animate(withDuration: 0.2) { + let epsilonToForceCounterClockwiseRotation: CGFloat = 0.00001 + switchCameraButton.transform = switchCameraButton.transform.rotate(.pi + epsilonToForceCounterClockwiseRotation) + } } photoCapture.switchCamera().catch { error in self.showFailureUI(error: error) @@ -785,9 +784,9 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate transformFromOrientation = .identity case .portraitUpsideDown: transformFromOrientation = CGAffineTransform(rotationAngle: .pi) - case .landscapeLeft: - transformFromOrientation = CGAffineTransform(rotationAngle: .halfPi) case .landscapeRight: + transformFromOrientation = CGAffineTransform(rotationAngle: .halfPi) + case .landscapeLeft: transformFromOrientation = CGAffineTransform(rotationAngle: -1 * .halfPi) @unknown default: owsFailDebug("unexpected captureOrientation: \(captureOrientation.rawValue)") @@ -797,9 +796,10 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate // Don't "unrotate" the switch camera icon if the front facing camera had been selected. let tranformFromCameraType: CGAffineTransform = photoCapture.desiredPosition == .front ? CGAffineTransform(rotationAngle: -.pi) : .identity + let buttonsToUpdate: [UIView] = [ topBar.batchModeButton, topBar.flashModeButton, bottomBar.photoLibraryButton ] let updateOrientation = { - self.topBar.flashModeButton?.transform = transformFromOrientation - self.bottomBar.switchCameraButton?.transform = transformFromOrientation.concatenating(tranformFromCameraType) + buttonsToUpdate.forEach { $0.transform = transformFromOrientation } + self.bottomBar.switchCameraButton.transform = transformFromOrientation.concatenating(tranformFromCameraType) } if isAnimated { From c1504e582e4a01106592a6813a7e1c0ec96c2a39 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 22 Feb 2022 11:48:52 -0800 Subject: [PATCH 16/32] Address CI linter complaints. --- .../Photos/MediaControls.swift | 193 +++--- .../Photos/PhotoCaptureViewController.swift | 637 +++++++++--------- .../SendMediaNavigationController.swift | 23 +- 3 files changed, 424 insertions(+), 429 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index 05a773e9df..17eb11314f 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -36,7 +36,7 @@ class CameraCaptureControl: UIView { private let shutterButtonOuterCircle = CircleBlurView(effect: UIBlurEffect(style: .light)) private let shutterButtonInnerCircle = CircleView() - private static let recordingLockControlSize: CGFloat = 36 // Stop button, swipe tracking circle, lock icon + fileprivate static let recordingLockControlSize: CGFloat = 36 // Stop button, swipe tracking circle, lock icon private static let shutterButtonDefaultSize: CGFloat = 72 private static let shutterButtonRecordingSize: CGFloat = 122 @@ -135,12 +135,12 @@ class CameraCaptureControl: UIView { private var _internalState: State = .initial var state: State { - set { - setState(newValue) - } get { _internalState } + set { + setState(newValue) + } } private var sliderTrackingProgress: CGFloat = 0 { @@ -198,7 +198,8 @@ class CameraCaptureControl: UIView { // element sizes outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonRecordingSize // Inner (white) circle gets smaller as user drags the slider and reveals stop button when the slider is halfway to the lock icon. - innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize - 2 * sliderTrackingProgress * (CameraCaptureControl.shutterButtonDefaultSize - CameraCaptureControl.recordingLockControlSize) + let circleSizeOffset = 2 * sliderTrackingProgress * (CameraCaptureControl.shutterButtonDefaultSize - CameraCaptureControl.recordingLockControlSize) + innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize - circleSizeOffset case .recordingLocked: prepareRecordingControlsIfNecessary() @@ -337,7 +338,7 @@ class CameraCaptureControl: UIView { guard let self = self else { return } self.setState(.recording, animationDuration: 0.4) - + self.delegate?.cameraCaptureControlDidRequestStartVideoRecording(self) } @@ -463,106 +464,104 @@ class CameraCaptureControl: UIView { private func didTapStopButton() { delegate?.cameraCaptureControlDidRequestFinishVideoRecording(self) } +} - // MARK: - LockView +private class LockView: UIView { - private class LockView: UIView { + private var imageViewLock = UIImageView(image: UIImage(named: "media-composer-lock-outline-24")) + private var blurBackgroundView = CircleBlurView(effect: UIBlurEffect(style: .dark)) + private var whiteBackgroundView = CircleView() + private var whiteCircleView = CircleView() - private var imageViewLock = UIImageView(image: UIImage(named: "media-composer-lock-outline-24")) - private var blurBackgroundView = CircleBlurView(effect: UIBlurEffect(style: .dark)) - private var whiteBackgroundView = CircleView() - private var whiteCircleView = CircleView() - - enum State { - case unlocked - case locking - case locked + enum State { + case unlocked + case locking + case locked + } + private var _internalState: State = .unlocked + var state: State { + get { + _internalState } - private var _internalState: State = .unlocked - var state: State { - set { - guard _internalState != newValue else { return } - setState(newValue) + set { + guard _internalState != newValue else { return } + setState(newValue) + } + } + + func setState(_ state: State, animated: Bool = false) { + _internalState = state + if animated { + UIView.animate(withDuration: 0.25, + delay: 0, + options: [ .beginFromCurrentState ]) { + self.updateAppearance() } - get { - _internalState - } - } - - func setState(_ state: State, animated: Bool = false) { - _internalState = state - if animated { - UIView.animate(withDuration: 0.25, - delay: 0, - options: [ .beginFromCurrentState ]) { - self.updateAppearance() - } - } else { - updateAppearance() - } - } - - private func updateAppearance() { - switch state { - case .unlocked: - blurBackgroundView.alpha = 1 - whiteCircleView.alpha = 0 - whiteBackgroundView.alpha = 0 - imageViewLock.alpha = 1 - imageViewLock.tintColor = .ows_white - - case .locking: - blurBackgroundView.alpha = 1 - whiteCircleView.alpha = 1 - whiteBackgroundView.alpha = 0 - imageViewLock.alpha = 0 - - case .locked: - blurBackgroundView.alpha = 0 - whiteCircleView.alpha = 0 - whiteBackgroundView.alpha = 1 - imageViewLock.alpha = 1 - imageViewLock.tintColor = .ows_black - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { - isUserInteractionEnabled = false - - addSubview(blurBackgroundView) - blurBackgroundView.autoPinEdgesToSuperviewEdges() - - addSubview(whiteCircleView) - whiteCircleView.backgroundColor = .clear - whiteCircleView.layer.borderColor = UIColor.ows_white.cgColor - whiteCircleView.layer.borderWidth = 3 - whiteCircleView.autoPinEdgesToSuperviewEdges() - - addSubview(whiteBackgroundView) - whiteBackgroundView.backgroundColor = .ows_white - whiteBackgroundView.autoPinEdgesToSuperviewEdges() - - addSubview(imageViewLock) - imageViewLock.tintColor = .ows_white - imageViewLock.autoCenterInSuperview() - + } else { updateAppearance() } + } - override var intrinsicContentSize: CGSize { - CGSize(width: CameraCaptureControl.recordingLockControlSize, height: CameraCaptureControl.recordingLockControlSize) + private func updateAppearance() { + switch state { + case .unlocked: + blurBackgroundView.alpha = 1 + whiteCircleView.alpha = 0 + whiteBackgroundView.alpha = 0 + imageViewLock.alpha = 1 + imageViewLock.tintColor = .ows_white + + case .locking: + blurBackgroundView.alpha = 1 + whiteCircleView.alpha = 1 + whiteBackgroundView.alpha = 0 + imageViewLock.alpha = 0 + + case .locked: + blurBackgroundView.alpha = 0 + whiteCircleView.alpha = 0 + whiteBackgroundView.alpha = 1 + imageViewLock.alpha = 1 + imageViewLock.tintColor = .ows_black } } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + isUserInteractionEnabled = false + + addSubview(blurBackgroundView) + blurBackgroundView.autoPinEdgesToSuperviewEdges() + + addSubview(whiteCircleView) + whiteCircleView.backgroundColor = .clear + whiteCircleView.layer.borderColor = UIColor.ows_white.cgColor + whiteCircleView.layer.borderWidth = 3 + whiteCircleView.autoPinEdgesToSuperviewEdges() + + addSubview(whiteBackgroundView) + whiteBackgroundView.backgroundColor = .ows_white + whiteBackgroundView.autoPinEdgesToSuperviewEdges() + + addSubview(imageViewLock) + imageViewLock.tintColor = .ows_white + imageViewLock.autoCenterInSuperview() + + updateAppearance() + } + + override var intrinsicContentSize: CGSize { + CGSize(width: CameraCaptureControl.recordingLockControlSize, height: CameraCaptureControl.recordingLockControlSize) + } } @available(iOS, deprecated: 13.0, message: "Use `overrideUserInterfaceStyle` instead.") @@ -609,7 +608,6 @@ private extension UserInterfaceStyleOverride { } } - class CameraOverlayButton: UIButton, UserInterfaceStyleOverride { fileprivate var userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified { @@ -682,7 +680,6 @@ class CameraOverlayButton: UIButton, UserInterfaceStyleOverride { } } - class MediaDoneButton: UIButton, UserInterfaceStyleOverride { var badgeNumber: Int = 0 { diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 476d56fd67..820ba5cf44 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -43,16 +43,13 @@ extension PhotoCaptureError: LocalizedError, UserErrorDescriptionProvider { } } -// MARK: - - -@objc class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate { - + weak var delegate: PhotoCaptureViewControllerDelegate? weak var dataSource: PhotoCaptureViewControllerDataSource? private var interactiveDismiss: PhotoCaptureInteractiveDismiss! - - @objc public lazy var photoCapture = PhotoCapture() + + public lazy var photoCapture = PhotoCapture() deinit { UIDevice.current.endGeneratingDeviceOrientationNotifications() @@ -62,12 +59,12 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } // MARK: - Overrides - + override func loadView() { view = UIView() view.backgroundColor = Theme.darkThemeBackgroundColor view.preservesSuperviewLayoutMargins = true - + definesPresentationContext = true initializeUI() @@ -75,7 +72,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate override func viewDidLoad() { super.viewDidLoad() - + setupPhotoCapture() // If the view is already visible, setup the volume button listener // now that the capture UI is ready. Otherwise, we'll wait until @@ -85,17 +82,17 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } updateFlashModeControl() - + view.addGestureRecognizer(pinchZoomGesture) view.addGestureRecognizer(tapToFocusGesture) view.addGestureRecognizer(doubleTapToSwitchCameraGesture) - + if let navController = self.navigationController { interactiveDismiss = PhotoCaptureInteractiveDismiss(viewController: navController) interactiveDismiss.interactiveDismissDelegate = self interactiveDismiss.addGestureRecognizer(to: view) } - + tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture) bottomBar.photoLibraryButton.configure() @@ -103,12 +100,12 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate sideBar.photoLibraryButton.configure() } } - + private var isVisible = false - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + isVisible = true let previewOrientation: AVCaptureVideoOrientation if UIDevice.current.isIPad { @@ -126,60 +123,61 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } updateDoneButtonAppearance() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + if hasCaptureStarted { BenchEventComplete(eventId: "Show-Camera") VolumeButtons.shared?.addObserver(observer: photoCapture) } UIApplication.shared.isIdleTimerDisabled = true } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + isVisible = false VolumeButtons.shared?.removeObserver(photoCapture) pausePhotoCapture() UIApplication.shared.isIdleTimerDisabled = false } - + override var prefersStatusBarHidden: Bool { guard !CurrentAppContext().hasActiveCall else { return false } return true } - + override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } - + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } - + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - + if UIDevice.current.isIPad { // Since we support iPad multitasking, we cannot *disable* rotation of our views. // Rotating the preview layer is really distracting, so we fade out the preview layer // while the rotation occurs. self.previewView.alpha = 0 - coordinator.animate(alongsideTransition: { _ in }) { _ in + coordinator.animate(alongsideTransition: { _ in }, + completion: { _ in UIView.animate(withDuration: 0.1) { self.previewView.alpha = 1 } - } + }) } } - + override func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() - + // we pin to a constant rather than margin, because on notched devices the // safeAreaInsets/margins change as the device rotates *EVEN THOUGH* the interface // is locked to portrait. @@ -413,191 +411,6 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate bottomBar.isHidden = isIPadUIInRegularMode sideBar?.isHidden = !isIPadUIInRegularMode } - - // MARK: - Interactive Dismiss - - func interactiveDismissDidBegin(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { - view.backgroundColor = .clear - } - - func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { - dismiss(animated: true) - } - - func interactiveDismissDidCancel(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { - view.backgroundColor = Theme.darkThemeBackgroundColor - } - - // MARK: - Top Bar - - private struct ButtonImages { - static let close = UIImage(named: "media-composer-close") - static let switchCamera = UIImage(named: "media-composer-switch-camera-24") - - static let batchModeOn = UIImage(named: "media-composer-create-album-solid-24") - static let batchModeOff = UIImage(named: "media-composer-create-album-outline-24") - - static let flashOn = UIImage(named: "media-composer-flash-filled-24") - static let flashOff = UIImage(named: "media-composer-flash-outline-24") - static let flashAuto = UIImage(named: "media-composer-flash-auto-24") - } - - private class TopBar: UIView { - private(set) var closeButton: CameraOverlayButton! - - private var cameraControlsContainerView: UIView! - private(set) var flashModeButton: CameraOverlayButton! - private(set) var batchModeButton: CameraOverlayButton! - - private(set) var recordingTimerView: RecordingTimerView! - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { - layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4) - - closeButton = CameraOverlayButton(image: ButtonImages.close, userInterfaceStyleOverride: .dark) - addSubview(closeButton) - closeButton.autoPinHeightToSuperviewMargins() - closeButton.autoPinLeadingToSuperviewMargin() - - recordingTimerView = RecordingTimerView(frame: .zero) - addSubview(recordingTimerView) - recordingTimerView.autoPinHeightToSuperview(withMargin: 8) - recordingTimerView.autoHCenterInSuperview() - - flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) - batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) - let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton ]) - stackView.spacing = 16 - addSubview(stackView) - stackView.autoPinHeightToSuperviewMargins() - stackView.autoPinTrailingToSuperviewMargin() - cameraControlsContainerView = stackView - } - - // MARK: - Mode - - enum Mode { - case cameraControls, closeButton, videoRecording - } - - var mode: Mode = .cameraControls { - didSet { - switch mode { - case .cameraControls: - closeButton.isHidden = false - cameraControlsContainerView.isHidden = false - recordingTimerView?.isHidden = true - - case .closeButton: - closeButton.isHidden = false - cameraControlsContainerView.isHidden = true - recordingTimerView?.isHidden = true - - case .videoRecording: - closeButton.isHidden = true - cameraControlsContainerView.isHidden = true - recordingTimerView.isHidden = false - } - } - } - } - - // MARK: - Bottom Bar - - private class BottomBar: UIView { - private(set) var photoLibraryButton: MediaPickerThumbnailButton! - private(set) var switchCameraButton: CameraOverlayButton! - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { - layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) - - photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) - addSubview(photoLibraryButton) - photoLibraryButton.autoVCenterInSuperview() - photoLibraryButton.autoPinLeadingToSuperviewMargin() - - switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) - addSubview(switchCameraButton) - switchCameraButton.autoPinHeightToSuperviewMargins() - switchCameraButton.autoPinTrailingToSuperviewMargin() - } - } - - // MARK: - Side Bar - - private class SideBar: UIView { - var isRecordingVideo = false { - didSet { - cameraControlsContainerView.isHidden = isRecordingVideo - photoLibraryButton.isHidden = isRecordingVideo - } - } - - private var cameraControlsContainerView: UIView! - private(set) var flashModeButton: CameraOverlayButton! - private(set) var batchModeButton: CameraOverlayButton! - private(set) var switchCameraButton: CameraOverlayButton! - - private(set) var photoLibraryButton: MediaPickerThumbnailButton! - - private(set) var cameraCaptureControl = CameraCaptureControl(axis: .vertical) - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { - layoutMargins = UIEdgeInsets(margin: 8) - - flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) - switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) - batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) - let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton, switchCameraButton ]) - stackView.spacing = 16 - stackView.axis = .vertical - addSubview(stackView) - stackView.autoPinWidthToSuperviewMargins() - stackView.autoPinTopToSuperviewMargin() - cameraControlsContainerView = stackView - - addSubview(cameraCaptureControl) - cameraCaptureControl.autoHCenterInSuperview() - cameraCaptureControl.shutterButtonLayoutGuide.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 36).isActive = true - - photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) - addSubview(photoLibraryButton) - photoLibraryButton.autoHCenterInSuperview() - photoLibraryButton.topAnchor.constraint(equalTo: cameraCaptureControl.shutterButtonLayoutGuide.bottomAnchor, constant: 36).isActive = true - photoLibraryButton.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor).isActive = true - - } - } func updateDoneButtonAppearance () { if isInBatchMode, let badgeNumber = dataSource?.numberOfMediaItems { @@ -607,35 +420,49 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate doneButton.isHidden = true } } - + + // MARK: - Interactive Dismiss + + func interactiveDismissDidBegin(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { + view.backgroundColor = .clear + } + + func interactiveDismissDidFinish(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { + dismiss(animated: true) + } + + func interactiveDismissDidCancel(_ interactiveDismiss: UIPercentDrivenInteractiveTransition) { + view.backgroundColor = Theme.darkThemeBackgroundColor + } + // MARK: - Gestures lazy var pinchZoomGesture: UIPinchGestureRecognizer = { return UIPinchGestureRecognizer(target: self, action: #selector(didPinchZoom(pinchGesture:))) }() - + lazy var tapToFocusGesture: UITapGestureRecognizer = { return UITapGestureRecognizer(target: self, action: #selector(didTapFocusExpose(tapGesture:))) }() - + lazy var doubleTapToSwitchCameraGesture: UITapGestureRecognizer = { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTapToSwitchCamera(tapGesture:))) tapGesture.numberOfTapsRequired = 2 return tapGesture }() - + // MARK: - Events - + @objc func didTapClose() { delegate?.photoCaptureViewControllerDidCancel(self) } - + @objc func didTapSwitchCamera() { switchCamera() } - + @objc func didDoubleTapToSwitchCamera(tapGesture: UITapGestureRecognizer) { guard !isRecordingVideo else { @@ -646,7 +473,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } switchCamera() } - + private func switchCamera() { if let switchCameraButton = isIPadUIInRegularMode ? sideBar?.switchCameraButton : bottomBar.switchCameraButton { UIView.animate(withDuration: 0.2) { @@ -658,7 +485,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self.showFailureUI(error: error) } } - + @objc func didTapFlashMode() { firstly { @@ -669,7 +496,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate owsFailDebug("Error: \(error)") } } - + @objc func didTapBatchMode() { guard let delegate = delegate else { @@ -677,21 +504,22 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } isInBatchMode = delegate.photoCaptureViewController(self, didRequestSwitchBatchMode: !isInBatchMode) } - + @objc func didTapPhotoLibrary() { delegate?.photoCaptureViewControllerDidRequestPresentPhotoLibrary(self) } - + @objc func didTapDoneButton() { delegate?.photoCaptureViewControllerDidFinish(self) } - + @objc func didPinchZoom(pinchGesture: UIPinchGestureRecognizer) { switch pinchGesture.state { - case .began: fallthrough + case .began: + fallthrough case .changed: photoCapture.updateZoom(scaleFromPreviousZoomFactor: pinchGesture.scale) case .ended: @@ -700,12 +528,12 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate break } } - + @objc func didTapFocusExpose(tapGesture: UITapGestureRecognizer) { let viewLocation = tapGesture.location(in: view) let devicePoint = previewView.previewLayer.captureDevicePointConverted(fromLayerPoint: viewLocation) - + photoCapture.focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true) // // If the user taps near the capture button, it's more likely a mis-tap than intentional. @@ -743,9 +571,9 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate startFocusAnimation() } } - + // MARK: - Focus Animations - + private var tapToFocusLeftConstraint: NSLayoutConstraint! private var tapToFocusTopConstraint: NSLayoutConstraint! private var lastUserFocusTapPoint: CGPoint? @@ -754,30 +582,30 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate tapToFocusLeftConstraint.constant = center.x tapToFocusTopConstraint.constant = center.y } - + private func startFocusAnimation() { tapToFocusView.stop() tapToFocusView.play(fromProgress: 0.0, toProgress: 0.9) } - + private func completeFocusAnimation(forFocusPoint focusPoint: CGPoint) { guard let lastUserFocusTapPoint = lastUserFocusTapPoint else { return } - + guard lastUserFocusTapPoint.within(0.005, of: focusPoint) else { Logger.verbose("focus completed for obsolete focus point. User has refocused.") return } - + tapToFocusView.play(toProgress: 1.0) } - + // MARK: - Orientation - + private func updateIconOrientations(isAnimated: Bool, captureOrientation: AVCaptureVideoOrientation) { guard !UIDevice.current.isIPad else { return } - + Logger.verbose("captureOrientation: \(captureOrientation)") - + let transformFromOrientation: CGAffineTransform switch captureOrientation { case .portrait: @@ -792,44 +620,44 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate owsFailDebug("unexpected captureOrientation: \(captureOrientation.rawValue)") transformFromOrientation = .identity } - + // Don't "unrotate" the switch camera icon if the front facing camera had been selected. let tranformFromCameraType: CGAffineTransform = photoCapture.desiredPosition == .front ? CGAffineTransform(rotationAngle: -.pi) : .identity - + let buttonsToUpdate: [UIView] = [ topBar.batchModeButton, topBar.flashModeButton, bottomBar.photoLibraryButton ] let updateOrientation = { buttonsToUpdate.forEach { $0.transform = transformFromOrientation } self.bottomBar.switchCameraButton.transform = transformFromOrientation.concatenating(tranformFromCameraType) } - + if isAnimated { UIView.animate(withDuration: 0.3, animations: updateOrientation) } else { updateOrientation() } } - + // MARK: - Photo Capture - + var hasCaptureStarted = false - + private func captureReady() { self.hasCaptureStarted = true BenchEventComplete(eventId: "Show-Camera") } - + private func setupPhotoCapture() { photoCapture.delegate = self cameraCaptureControl.delegate = photoCapture if let sideBar = sideBar { sideBar.cameraCaptureControl.delegate = photoCapture } - + // If the session is already running, we're good to go. guard !photoCapture.session.isRunning else { return self.captureReady() } - + firstly { photoCapture.prepareVideoCapture() }.catch { [weak self] error in @@ -837,7 +665,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self.showFailureUI(error: error) } } - + private func pausePhotoCapture() { guard photoCapture.session.isRunning else { return } firstly { @@ -848,7 +676,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self?.showFailureUI(error: error) } } - + private func resumePhotoCapture() { guard !photoCapture.session.isRunning else { return } firstly { @@ -859,16 +687,16 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate self?.showFailureUI(error: error) } } - + private func showFailureUI(error: Error) { Logger.error("error: \(error)") - + OWSActionSheets.showActionSheet(title: nil, message: error.userErrorDescription, buttonTitle: CommonStrings.dismissButton, buttonAction: { [weak self] _ in self?.dismiss(animated: true) }) } - + private func updateFlashModeControl() { let image: UIImage? switch photoCapture.flashMode { @@ -892,27 +720,192 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } } +private struct ButtonImages { + static let close = UIImage(named: "media-composer-close") + static let switchCamera = UIImage(named: "media-composer-switch-camera-24") + + static let batchModeOn = UIImage(named: "media-composer-create-album-solid-24") + static let batchModeOff = UIImage(named: "media-composer-create-album-outline-24") + + static let flashOn = UIImage(named: "media-composer-flash-filled-24") + static let flashOff = UIImage(named: "media-composer-flash-outline-24") + static let flashAuto = UIImage(named: "media-composer-flash-auto-24") +} + +private class TopBar: UIView { + private(set) var closeButton: CameraOverlayButton! + + private var cameraControlsContainerView: UIView! + private(set) var flashModeButton: CameraOverlayButton! + private(set) var batchModeButton: CameraOverlayButton! + + private(set) var recordingTimerView: RecordingTimerView! + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4) + + closeButton = CameraOverlayButton(image: ButtonImages.close, userInterfaceStyleOverride: .dark) + addSubview(closeButton) + closeButton.autoPinHeightToSuperviewMargins() + closeButton.autoPinLeadingToSuperviewMargin() + + recordingTimerView = RecordingTimerView(frame: .zero) + addSubview(recordingTimerView) + recordingTimerView.autoPinHeightToSuperview(withMargin: 8) + recordingTimerView.autoHCenterInSuperview() + + flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) + batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) + let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton ]) + stackView.spacing = 16 + addSubview(stackView) + stackView.autoPinHeightToSuperviewMargins() + stackView.autoPinTrailingToSuperviewMargin() + cameraControlsContainerView = stackView + } + + // MARK: - Mode + + enum Mode { + case cameraControls, closeButton, videoRecording + } + + var mode: Mode = .cameraControls { + didSet { + switch mode { + case .cameraControls: + closeButton.isHidden = false + cameraControlsContainerView.isHidden = false + recordingTimerView?.isHidden = true + + case .closeButton: + closeButton.isHidden = false + cameraControlsContainerView.isHidden = true + recordingTimerView?.isHidden = true + + case .videoRecording: + closeButton.isHidden = true + cameraControlsContainerView.isHidden = true + recordingTimerView.isHidden = false + } + } + } +} + +private class BottomBar: UIView { + private(set) var photoLibraryButton: MediaPickerThumbnailButton! + private(set) var switchCameraButton: CameraOverlayButton! + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) + + photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) + addSubview(photoLibraryButton) + photoLibraryButton.autoVCenterInSuperview() + photoLibraryButton.autoPinLeadingToSuperviewMargin() + + switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) + addSubview(switchCameraButton) + switchCameraButton.autoPinHeightToSuperviewMargins() + switchCameraButton.autoPinTrailingToSuperviewMargin() + } +} + +private class SideBar: UIView { + var isRecordingVideo = false { + didSet { + cameraControlsContainerView.isHidden = isRecordingVideo + photoLibraryButton.isHidden = isRecordingVideo + } + } + + private var cameraControlsContainerView: UIView! + private(set) var flashModeButton: CameraOverlayButton! + private(set) var batchModeButton: CameraOverlayButton! + private(set) var switchCameraButton: CameraOverlayButton! + + private(set) var photoLibraryButton: MediaPickerThumbnailButton! + + private(set) var cameraCaptureControl = CameraCaptureControl(axis: .vertical) + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + layoutMargins = UIEdgeInsets(margin: 8) + + flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) + switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) + batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) + let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton, switchCameraButton ]) + stackView.spacing = 16 + stackView.axis = .vertical + addSubview(stackView) + stackView.autoPinWidthToSuperviewMargins() + stackView.autoPinTopToSuperviewMargin() + cameraControlsContainerView = stackView + + addSubview(cameraCaptureControl) + cameraCaptureControl.autoHCenterInSuperview() + cameraCaptureControl.shutterButtonLayoutGuide.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 36).isActive = true + + photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) + addSubview(photoLibraryButton) + photoLibraryButton.autoHCenterInSuperview() + photoLibraryButton.topAnchor.constraint(equalTo: cameraCaptureControl.shutterButtonLayoutGuide.bottomAnchor, constant: 36).isActive = true + photoLibraryButton.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor).isActive = true + + } +} + extension PhotoCaptureViewController: PhotoCaptureDelegate { - + // MARK: - Photo - + func photoCaptureDidStart(_ photoCapture: PhotoCapture) { let captureFeedbackView = UIView() 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() } } - + func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessing attachment: SignalAttachment) { dataSource?.addMedia(attachment: attachment) @@ -922,10 +915,10 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { delegate?.photoCaptureViewControllerDidFinish(self) } } - + func photoCapture(_ photoCapture: PhotoCapture, didFailProcessing error: Error) { isRecordingVideo = false - + if case PhotoCaptureError.invalidVideo = error { // Don't show an error if the user aborts recording before video // recording has begun. @@ -933,59 +926,59 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { } showFailureUI(error: error) } - + func photoCaptureCanCaptureMoreItems(_ photoCapture: PhotoCapture) -> Bool { return delegate?.photoCaptureViewControllerCanCaptureMoreItems(self) ?? false } - + func photoCaptureDidTryToCaptureTooMany(_ photoCapture: PhotoCapture) { delegate?.photoCaptureViewControllerDidTryToCaptureTooMany(self) } - + // MARK: - Video - + func photoCaptureDidBeginRecording(_ photoCapture: PhotoCapture) { isRecordingVideo = true } - + func photoCaptureDidFinishRecording(_ photoCapture: PhotoCapture) { isRecordingVideo = false } - + func photoCaptureDidCancelRecording(_ photoCapture: PhotoCapture) { isRecordingVideo = false } - + // MARK: - - + var zoomScaleReferenceDistance: CGFloat? { if isIPadUIInRegularMode { return view.bounds.width } return view.bounds.height } - + func beginCaptureButtonAnimation(_ duration: TimeInterval) { cameraCaptureControl.setState(.recording, animationDuration: duration) if let sideBar = sideBar { sideBar.cameraCaptureControl.setState(.recording, animationDuration: duration) } } - + func endCaptureButtonAnimation(_ duration: TimeInterval) { cameraCaptureControl.setState(.initial, animationDuration: duration) if let sideBar = sideBar { sideBar.cameraCaptureControl.setState(.initial, animationDuration: duration) } } - + func photoCapture(_ photoCapture: PhotoCapture, didChangeOrientation orientation: AVCaptureVideoOrientation) { updateIconOrientations(isAnimated: true, captureOrientation: orientation) if UIDevice.current.isIPad { photoCapture.updateVideoPreviewConnection(toOrientation: orientation) } } - + func photoCapture(_ photoCapture: PhotoCapture, didCompleteFocusing focusPoint: CGPoint) { completeFocusAnimation(forFocusPoint: focusPoint) } @@ -994,25 +987,25 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { // MARK: - Views private class MediaPickerThumbnailButton: UIButton { - + private static let visibleSize = CGSize(square: 36) - + func configure() { layer.cornerRadius = 10 layer.borderWidth = 1.5 layer.borderColor = UIColor.ows_whiteAlpha80.cgColor clipsToBounds = true - + let placeholderView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) insertSubview(placeholderView, at: 0) placeholderView.autoPinEdgesToSuperviewEdges() - + // Async Fetch last image DispatchQueue.global(qos: .userInteractive).async { let fetchOptions = PHFetchOptions() fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] fetchOptions.fetchLimit = 1 - + let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions) if fetchResult.count > 0, let asset = fetchResult.firstObject { let targetImageSize = MediaPickerThumbnailButton.visibleSize @@ -1025,41 +1018,29 @@ private class MediaPickerThumbnailButton: UIButton { } } } - + override var intrinsicContentSize: CGSize { return Self.visibleSize } } class CapturePreviewView: UIView { - + let previewLayer: AVCaptureVideoPreviewLayer - + override var bounds: CGRect { didSet { previewLayer.frame = bounds } } - + override var frame: CGRect { didSet { previewLayer.frame = bounds } } - + override var contentMode: UIView.ContentMode { - set { - switch newValue { - case .scaleAspectFill: - previewLayer.videoGravity = .resizeAspectFill - case .scaleAspectFit: - previewLayer.videoGravity = .resizeAspect - case .scaleToFill: - previewLayer.videoGravity = .resize - default: - owsFailDebug("Unexpected contentMode") - } - } get { switch previewLayer.videoGravity { case .resizeAspectFill: @@ -1073,8 +1054,20 @@ class CapturePreviewView: UIView { return .scaleToFill } } + set { + switch newValue { + case .scaleAspectFill: + previewLayer.videoGravity = .resizeAspectFill + case .scaleAspectFit: + previewLayer.videoGravity = .resizeAspect + case .scaleToFill: + previewLayer.videoGravity = .resize + default: + owsFailDebug("Unexpected contentMode") + } + } } - + init(session: AVCaptureSession) { previewLayer = AVCaptureVideoPreviewLayer(session: session) if Platform.isSimulator { @@ -1086,39 +1079,43 @@ class CapturePreviewView: UIView { previewLayer.frame = bounds layer.addSublayer(previewLayer) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } private class RecordingTimerView: PillView { - + override init(frame: CGRect) { super.init(frame: frame) - + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { layoutMargins = UIEdgeInsets(hMargin: 16, vMargin: 0) - + let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) addSubview(backgroundView) backgroundView.autoPinEdgesToSuperviewEdges() - + let stackView = UIStackView(arrangedSubviews: [icon, label]) stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = 5 addSubview(stackView) stackView.autoPinEdgesToSuperviewMargins() - + updateView() } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - + // MARK: - Subviews - + private lazy var label: UILabel = { let label = UILabel() label.font = UIFont.ows_monospacedDigitFont(withSize: 20) @@ -1126,7 +1123,7 @@ private class RecordingTimerView: PillView { label.textColor = UIColor.white return label }() - + private let icon: UIView = { let icon = CircleView() icon.backgroundColor = .red @@ -1134,11 +1131,11 @@ private class RecordingTimerView: PillView { icon.alpha = 0 return icon }() - + // MARK: - - + var recordingStartTime: TimeInterval? - + func startCounting() { recordingStartTime = CACurrentMediaTime() timer = Timer.weakScheduledTimer(withTimeInterval: 0.1, target: self, selector: #selector(updateView), userInfo: nil, repeats: true) @@ -1147,7 +1144,7 @@ private class RecordingTimerView: PillView { options: [.autoreverse, .repeat], animations: { self.icon.alpha = 1 }) } - + func stopCounting() { timer?.invalidate() timer = nil @@ -1157,28 +1154,28 @@ private class RecordingTimerView: PillView { } label.text = nil } - + // MARK: - - + private var timer: Timer? - + private lazy var timeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "mm:ss" formatter.timeZone = TimeZone(identifier: "UTC")! - + return formatter }() - + // This method should only be called when the call state is "connected". var recordingDuration: TimeInterval { guard let recordingStartTime = recordingStartTime else { return 0 } - + return CACurrentMediaTime() - recordingStartTime } - + @objc private func updateView() { let recordingDuration = self.recordingDuration @@ -1188,7 +1185,7 @@ private class RecordingTimerView: PillView { } private extension UIView { - + func embeddedInContainerView(layoutMargins: UIEdgeInsets = .zero) -> UIView { var containerViewFrame = bounds containerViewFrame.width += layoutMargins.leading + layoutMargins.trailing diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index cfdba70c6d..011dff6295 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -152,17 +152,17 @@ class SendMediaNavigationController: OWSNavigationController { // MARK: Child VC's fileprivate lazy var captureViewController: PhotoCaptureViewController = { - let vc = PhotoCaptureViewController() - vc.delegate = self - vc.dataSource = self - return vc + let viewController = PhotoCaptureViewController() + viewController.delegate = self + viewController.dataSource = self + return viewController }() private lazy var mediaLibraryViewController: ImagePickerGridController = { - let vc = ImagePickerGridController() - vc.delegate = self - vc.dataSource = self - return vc + let viewController = ImagePickerGridController() + viewController.delegate = self + viewController.dataSource = self + return viewController }() private func pushApprovalViewController(attachmentApprovalItems: [AttachmentApprovalItem], @@ -192,7 +192,8 @@ class SendMediaNavigationController: OWSNavigationController { } else { let alert = ActionSheetController(title: nil, message: nil) - let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken") + let confirmAbandonText = NSLocalizedString("SEND_MEDIA_CONFIRM_ABANDON_ALBUM", + comment: "alert action, confirming the user wants to exit the media flow and abandon any photos they've taken") let confirmAbandonAction = ActionSheetAction(title: confirmAbandonText, style: .destructive, handler: { [weak self] _ in @@ -589,7 +590,7 @@ private struct CameraCaptureAttachment: Hashable, Equatable { hasher.combine(signalAttachment) } - static func ==(lhs: CameraCaptureAttachment, rhs: CameraCaptureAttachment) -> Bool { + static func == (lhs: CameraCaptureAttachment, rhs: CameraCaptureAttachment) -> Bool { return lhs.signalAttachment == rhs.signalAttachment } } @@ -602,7 +603,7 @@ private struct MediaLibraryAttachment: Hashable, Equatable { hasher.combine(asset) } - static func ==(lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool { + static func == (lhs: MediaLibraryAttachment, rhs: MediaLibraryAttachment) -> Bool { return lhs.asset == rhs.asset } } From 4e1a48a785c171f60419937dd80d952334098650 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 22 Feb 2022 16:29:27 -0800 Subject: [PATCH 17/32] Improved in-app camera layout on devices with a physical home button. "Photo Library" and "Switch Camera" buttons are now vertically centered with camera shutter button. --- .../Photos/PhotoCaptureViewController.swift | 129 ++++++++++++------ 1 file changed, 88 insertions(+), 41 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 820ba5cf44..55df39d614 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -208,7 +208,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate topBar.mode = .videoRecording topBar.recordingTimerView.startCounting() - cameraCaptureControl.setState(.recording, animationDuration: 0.4) + bottomBar.captureControl.setState(.recording, animationDuration: 0.4) if let sideBar = sideBar { sideBar.cameraCaptureControl.setState(.recording, animationDuration: 0.4) } @@ -216,18 +216,18 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate topBar.mode = isIPadUIInRegularMode ? .closeButton : .cameraControls topBar.recordingTimerView.stopCounting() - cameraCaptureControl.setState(.initial, animationDuration: 0.2) + bottomBar.captureControl.setState(.initial, animationDuration: 0.2) if let sideBar = sideBar { sideBar.cameraCaptureControl.setState(.initial, animationDuration: 0.2) } } + bottomBar.isRecordingVideo = isRecordingVideo if let sideBar = sideBar { sideBar.isRecordingVideo = isRecordingVideo } doneButton.isHidden = isRecordingVideo || doneButton.badgeNumber == 0 - bottomBar.isHidden = isRecordingVideo } } @@ -245,13 +245,10 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate private var topBarOffsetFromTop: NSLayoutConstraint! private let bottomBar = BottomBar(frame: .zero) - private var bottomBarOffsetFromBottom: NSLayoutConstraint! + private var bottomBarVerticalPositionConstraint: NSLayoutConstraint! private var sideBar: SideBar? // Optional because most devices are iPhones and will never need this. - private let cameraCaptureControl = CameraCaptureControl(axis: .horizontal) - private var captureButtonVPositionConstraint: NSLayoutConstraint! - private lazy var tapToFocusView: AnimationView = { let view = AnimationView(name: "tap_to_focus") view.animationSpeed = 1 @@ -289,23 +286,30 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate topBarOffsetFromTop = topBar.autoPinEdge(toSuperviewEdge: .top) view.addSubview(bottomBar) + bottomBar.isCompactHeightLayout = !UIDevice.current.hasIPhoneXNotch bottomBar.switchCameraButton.addTarget(self, action: #selector(didTapSwitchCamera), for: .touchUpInside) bottomBar.photoLibraryButton.addTarget(self, action: #selector(didTapPhotoLibrary), for: .touchUpInside) bottomBar.autoPinWidthToSuperview() - bottomBarOffsetFromBottom = view.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor) - view.addConstraint(bottomBarOffsetFromBottom) + if bottomBar.isCompactHeightLayout { + // On devices with home button bar is simply pinned to the bottom of the screen + // with a margin that defines space under the shutter button. + view.bottomAnchor.constraint(equalTo: bottomBar.bottomAnchor, constant: 32).isActive = true + } else { + // On `notch` devices: + // i. Shutter button is placed 16 pts above the bottom edge of the preview view. + previewView.bottomAnchor.constraint(equalTo: bottomBar.shutterButtonLayoutGuide.bottomAnchor, constant: 16).isActive = true - view.addSubview(cameraCaptureControl) - captureButtonVPositionConstraint = view.bottomAnchor.constraint(equalTo: cameraCaptureControl.bottomAnchor) - view.addConstraint(captureButtonVPositionConstraint) - cameraCaptureControl.shutterButtonLayoutGuide.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true - cameraCaptureControl.autoPinTrailingToSuperviewMargin() + // ii. Other buttons are centered vertically in the black box between + // bottom of the preview view and top of bottom safe area. + bottomBarVerticalPositionConstraint = bottomBar.controlButtonsLayoutGuide.centerYAnchor.constraint(equalTo: previewView.bottomAnchor) + view.addConstraint(bottomBarVerticalPositionConstraint) + } view.addSubview(doneButton) doneButton.isHidden = true doneButton.translatesAutoresizingMaskIntoConstraints = false doneButtonIPhoneConstraints = [ doneButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), - doneButton.centerYAnchor.constraint(equalTo: cameraCaptureControl.centerYAnchor) ] + doneButton.centerYAnchor.constraint(equalTo: bottomBar.shutterButtonLayoutGuide.centerYAnchor) ] view.addConstraints(doneButtonIPhoneConstraints) doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) @@ -336,6 +340,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate guard sideBar == nil else { return } let sideBar = SideBar(frame: .zero) + sideBar.cameraCaptureControl.delegate = photoCapture sideBar.batchModeButton.addTarget(self, action: #selector(didTapBatchMode), for: .touchUpInside) sideBar.flashModeButton.addTarget(self, action: #selector(didTapFlashMode), for: .touchUpInside) sideBar.switchCameraButton.addTarget(self, action: #selector(didTapSwitchCamera), for: .touchUpInside) @@ -373,22 +378,11 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate previewView.frame = previewFrame previewView.previewLayer.cornerRadius = cornerRadius - // Bottom bar is pinned to the bottom of the screen, residing either directly above safe area / bottom margin - // or (for taller screens) floating in the center of the black area between the bottom of the capture view and safe area. - let bottomBarHeight = bottomBar.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, - withHorizontalFittingPriority: .fittingSizeLevel, - verticalFittingPriority: .fittingSizeLevel).height - let blackBarHeight = view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom - var bottomBarOffset = UIDevice.current.hasIPhoneXNotch ? view.safeAreaInsets.bottom : 16 - if blackBarHeight > bottomBarHeight { - bottomBarOffset += 0.5*(blackBarHeight - bottomBarHeight) + // See comment in `initializeUI`. + if !bottomBar.isCompactHeightLayout { + let blackBarHeight = view.bounds.maxY - previewFrame.maxY - view.safeAreaInsets.bottom + bottomBarVerticalPositionConstraint.constant = 0.5 * blackBarHeight } - bottomBarOffsetFromBottom.constant = bottomBarOffset - - // Bottom edge of the capture button is either 16pts above bottom edge of the camera capture view - // or directly adjacent to the top of the bottom bar, whatever is higher. - let captureButtonOffsetFromBottom = max(view.bounds.maxY - (previewFrame.maxY - 16), bottomBarOffset + bottomBarHeight) - captureButtonVPositionConstraint.constant = captureButtonOffsetFromBottom } private func updateIPadInterfaceLayout() { @@ -407,7 +401,6 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate if !isRecordingVideo { topBar.mode = isIPadUIInRegularMode ? .closeButton : .cameraControls } - cameraCaptureControl.isHidden = isIPadUIInRegularMode bottomBar.isHidden = isIPadUIInRegularMode sideBar?.isHidden = !isIPadUIInRegularMode } @@ -518,9 +511,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate @objc func didPinchZoom(pinchGesture: UIPinchGestureRecognizer) { switch pinchGesture.state { - case .began: - fallthrough - case .changed: + case .began, .changed: photoCapture.updateZoom(scaleFromPreviousZoomFactor: pinchGesture.scale) case .ended: photoCapture.completeZoom(scaleFromPreviousZoomFactor: pinchGesture.scale) @@ -648,7 +639,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate private func setupPhotoCapture() { photoCapture.delegate = self - cameraCaptureControl.delegate = photoCapture + bottomBar.captureControl.delegate = photoCapture if let sideBar = sideBar { sideBar.cameraCaptureControl.delegate = photoCapture } @@ -803,8 +794,30 @@ private class TopBar: UIView { } private class BottomBar: UIView { + private var compactHeightLayoutConstraints = [NSLayoutConstraint]() + private var regularHeightLayoutConstraints = [NSLayoutConstraint]() + var isCompactHeightLayout = false { + didSet { + guard oldValue != isCompactHeightLayout else { return } + updateCompactHeightLayoutConstraints() + } + } + + var isRecordingVideo = false { + didSet { + photoLibraryButton.isHidden = isRecordingVideo + switchCameraButton.isHidden = isRecordingVideo + } + } + private(set) var photoLibraryButton: MediaPickerThumbnailButton! private(set) var switchCameraButton: CameraOverlayButton! + let controlButtonsLayoutGuide = UILayoutGuide() // area encompassing Photo Library and Switch Camera buttons. + + private(set) var captureControl: CameraCaptureControl! + var shutterButtonLayoutGuide: UILayoutGuide { + captureControl.shutterButtonLayoutGuide + } override init(frame: CGRect) { super.init(frame: frame) @@ -819,16 +832,50 @@ private class BottomBar: UIView { private func commonInit() { layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) + addLayoutGuide(controlButtonsLayoutGuide) + addConstraints([ controlButtonsLayoutGuide.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + controlButtonsLayoutGuide.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) ]) + photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) + photoLibraryButton.translatesAutoresizingMaskIntoConstraints = false addSubview(photoLibraryButton) - photoLibraryButton.autoVCenterInSuperview() - photoLibraryButton.autoPinLeadingToSuperviewMargin() + addConstraints([ photoLibraryButton.leadingAnchor.constraint(equalTo: controlButtonsLayoutGuide.leadingAnchor), + photoLibraryButton.centerYAnchor.constraint(equalTo: controlButtonsLayoutGuide.centerYAnchor), + photoLibraryButton.topAnchor.constraint(greaterThanOrEqualTo: controlButtonsLayoutGuide.topAnchor) ]) switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) + switchCameraButton.translatesAutoresizingMaskIntoConstraints = false addSubview(switchCameraButton) - switchCameraButton.autoPinHeightToSuperviewMargins() - switchCameraButton.autoPinTrailingToSuperviewMargin() + addConstraints([ switchCameraButton.trailingAnchor.constraint(equalTo: controlButtonsLayoutGuide.trailingAnchor), + switchCameraButton.topAnchor.constraint(greaterThanOrEqualTo: controlButtonsLayoutGuide.topAnchor), + switchCameraButton.centerYAnchor.constraint(equalTo: controlButtonsLayoutGuide.centerYAnchor) ]) + + captureControl = CameraCaptureControl(axis: .horizontal) + captureControl.translatesAutoresizingMaskIntoConstraints = false + addSubview(captureControl) + captureControl.autoPinTopToSuperviewMargin() + captureControl.autoPinTrailingToSuperviewMargin() + addConstraint(captureControl.shutterButtonLayoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor)) + + compactHeightLayoutConstraints.append(contentsOf: [ controlButtonsLayoutGuide.centerYAnchor.constraint(equalTo: captureControl.shutterButtonLayoutGuide.centerYAnchor), + captureControl.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) ]) + + regularHeightLayoutConstraints.append(contentsOf: [ controlButtonsLayoutGuide.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), + captureControl.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor) ]) + + updateCompactHeightLayoutConstraints() } + + private func updateCompactHeightLayoutConstraints() { + if isCompactHeightLayout { + removeConstraints(regularHeightLayoutConstraints) + addConstraints(compactHeightLayoutConstraints) + } else { + removeConstraints(compactHeightLayoutConstraints) + addConstraints(regularHeightLayoutConstraints) + } + } + } private class SideBar: UIView { @@ -959,14 +1006,14 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { } func beginCaptureButtonAnimation(_ duration: TimeInterval) { - cameraCaptureControl.setState(.recording, animationDuration: duration) + bottomBar.captureControl.setState(.recording, animationDuration: duration) if let sideBar = sideBar { sideBar.cameraCaptureControl.setState(.recording, animationDuration: duration) } } func endCaptureButtonAnimation(_ duration: TimeInterval) { - cameraCaptureControl.setState(.initial, animationDuration: duration) + bottomBar.captureControl.setState(.initial, animationDuration: duration) if let sideBar = sideBar { sideBar.cameraCaptureControl.setState(.initial, animationDuration: duration) } From 29f75ed03c6d17c5aabec9bdeebdbe8e82bc2dd1 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Wed, 23 Feb 2022 17:29:26 -0800 Subject: [PATCH 18/32] Fix regression that caused crash when opening media library picker from chat. --- .../Photos/PhotoCaptureViewController.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 55df39d614..f281c4bed7 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -47,7 +47,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate weak var delegate: PhotoCaptureViewControllerDelegate? weak var dataSource: PhotoCaptureViewControllerDataSource? - private var interactiveDismiss: PhotoCaptureInteractiveDismiss! + private var interactiveDismiss: PhotoCaptureInteractiveDismiss? public lazy var photoCapture = PhotoCapture() @@ -88,9 +88,10 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate view.addGestureRecognizer(doubleTapToSwitchCameraGesture) if let navController = self.navigationController { - interactiveDismiss = PhotoCaptureInteractiveDismiss(viewController: navController) + let interactiveDismiss = PhotoCaptureInteractiveDismiss(viewController: navController) interactiveDismiss.interactiveDismissDelegate = self interactiveDismiss.addGestureRecognizer(to: view) + self.interactiveDismiss = interactiveDismiss } tapToFocusGesture.require(toFail: doubleTapToSwitchCameraGesture) @@ -360,7 +361,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - guard !interactiveDismiss.interactionInProgress else { return } + guard !(interactiveDismiss?.interactionInProgress ?? false) else { return } // Clamp capture view to 16:9 on iPhones. var previewFrame = view.bounds From 0f19830c6243e34cecb3d4349b905b76390d89d7 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Wed, 23 Feb 2022 17:30:52 -0800 Subject: [PATCH 19/32] Fix missing camera permissions check when presenting in-app camera. In the following flow: open chat, open media library picker, tap on Camera button. --- .../Photos/SendMediaNavigationController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 011dff6295..c08c29148f 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -357,7 +357,12 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { return } - fadeTo(viewControllers: [ captureViewController ], duration: 0.2) + self.ows_askForCameraPermissions { isGranted in + guard isGranted else { return } + + BenchEventStart(title: "Show-Camera", eventId: "Show-Camera") + self.fadeTo(viewControllers: [self.captureViewController], duration: 0.08) + } } func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) { From 9136113b8890f1805246c4e1087aa27a6515f376 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Wed, 23 Feb 2022 20:07:30 -0800 Subject: [PATCH 20/32] Uncheck "Preserve Vector Representation" for in-app camera assets. --- .../media-composer-camera-outline-28.imageset/Contents.json | 1 - .../media-composer-checkmark.imageset/Contents.json | 1 - .../Images.xcassets/media-composer-close.imageset/Contents.json | 1 - .../Contents.json | 1 - .../media-composer-create-album-solid-24.imageset/Contents.json | 1 - .../media-composer-flash-auto-24.imageset/Contents.json | 1 - .../media-composer-flash-filled-24.imageset/Contents.json | 1 - .../media-composer-flash-outline-24.imageset/Contents.json | 1 - .../media-composer-lock-outline-24.imageset/Contents.json | 1 - .../media-composer-navbar-chevron.imageset/Contents.json | 1 - .../media-composer-switch-camera-24.imageset/Contents.json | 1 - 11 files changed, 11 deletions(-) diff --git a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json index 7c5d5a832f..e5795367ff 100644 --- a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json b/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json index 3ce4894655..1732beebc1 100644 --- a/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-checkmark.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-close.imageset/Contents.json b/Signal/Images.xcassets/media-composer-close.imageset/Contents.json index 4c94f4f150..b16c2030f5 100644 --- a/Signal/Images.xcassets/media-composer-close.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-close.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json index 8899a572d9..4266319043 100644 --- a/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-create-album-outline-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json index 4ecc875445..5dfa74c324 100644 --- a/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-create-album-solid-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json index 99ae7bab96..05a001a88e 100644 --- a/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-flash-auto-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json index 53ade49241..c63870d317 100644 --- a/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-flash-filled-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json index 973a998219..93c457fea5 100644 --- a/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-flash-outline-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json index 2c04ad3cc9..c0b8216e57 100644 --- a/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-lock-outline-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json b/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json index 475ad9106b..6f0da23901 100644 --- a/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-navbar-chevron.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } diff --git a/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json b/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json index 5ae7a11ef1..06a95557b1 100644 --- a/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json +++ b/Signal/Images.xcassets/media-composer-switch-camera-24.imageset/Contents.json @@ -10,7 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } From 2e3efccc303e262df23989b0c5a58d98f126649a Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Fri, 25 Feb 2022 23:58:15 -0800 Subject: [PATCH 21/32] Transition camera UI to "recording video" state sooner. Transition as soon as a request to start video recording has been made, not waiting for recording to actually start (which sometimes takes a perceptible amount of time). --- Signal/src/ViewControllers/Photos/PhotoCapture.swift | 3 +++ .../ViewControllers/Photos/PhotoCaptureViewController.swift | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/Signal/src/ViewControllers/Photos/PhotoCapture.swift b/Signal/src/ViewControllers/Photos/PhotoCapture.swift index 59ca0d0046..1ca853ce0c 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCapture.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCapture.swift @@ -15,6 +15,7 @@ protocol PhotoCaptureDelegate: AnyObject { // MARK: Video + func photoCaptureWillBeginRecording(_ photoCapture: PhotoCapture) func photoCaptureDidBeginRecording(_ photoCapture: PhotoCapture) func photoCaptureDidFinishRecording(_ photoCapture: PhotoCapture) func photoCaptureDidCancelRecording(_ photoCapture: PhotoCapture) @@ -521,6 +522,8 @@ class PhotoCapture: NSObject { }.catch { error in self.handleMovieCaptureError(error) } + + delegate.photoCaptureWillBeginRecording(self) } private func completeMovieCapture() { diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index f281c4bed7..1d1c25f765 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -985,6 +985,10 @@ extension PhotoCaptureViewController: PhotoCaptureDelegate { // MARK: - Video + func photoCaptureWillBeginRecording(_ photoCapture: PhotoCapture) { + isRecordingVideo = true + } + func photoCaptureDidBeginRecording(_ photoCapture: PhotoCapture) { isRecordingVideo = true } From 48bef690b64b44186dcfd9b6343ada810b66fdfe Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 28 Feb 2022 15:17:37 -0800 Subject: [PATCH 22/32] Fix previously captured item not deleted when going back from media editor to camera. --- .../Photos/PhotoCaptureViewController.swift | 5 ++++- .../SendMediaNavigationController.swift | 19 +++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 1d1c25f765..53e4ac2336 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -13,6 +13,7 @@ protocol PhotoCaptureViewControllerDelegate: AnyObject { func photoCaptureViewControllerDidFinish(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewControllerDidTryToCaptureTooMany(_ photoCaptureViewController: PhotoCaptureViewController) + func photoCaptureViewControllerViewWillAppear(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewControllerCanCaptureMoreItems(_ photoCaptureViewController: PhotoCaptureViewController) -> Bool func photoCaptureViewControllerDidRequestPresentPhotoLibrary(_ photoCaptureViewController: PhotoCaptureViewController) func photoCaptureViewController(_ photoCaptureViewController: PhotoCaptureViewController, didRequestSwitchBatchMode batchMode: Bool) -> Bool @@ -107,6 +108,8 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + delegate?.photoCaptureViewControllerViewWillAppear(self) + isVisible = true let previewOrientation: AVCaptureVideoOrientation if UIDevice.current.isIPad { @@ -232,7 +235,7 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } } - private var isInBatchMode: Bool = false { + private(set) var isInBatchMode: Bool = false { didSet { let buttonImage = isInBatchMode ? ButtonImages.batchModeOn : ButtonImages.batchModeOff topBar.batchModeButton.setImage(buttonImage, for: .normal) diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index c08c29148f..3477e4ffbd 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -216,18 +216,6 @@ extension SendMediaNavigationController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { updateNavbarTheme(for: viewController, animated: animated) - - switch viewController { - case is PhotoCaptureViewController: - if attachmentCount == 1, navigationController.topViewController is AttachmentApprovalViewController { - // User is navigating "back" to the previous view, indicating - // they want to discard the previously captured item - discardDraft() - } - - default: - break - } } // In case back navigation was canceled, we re-apply whatever is showing. @@ -297,6 +285,13 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { didRequestExit(dontAbandonText: dontAbandonText) } + func photoCaptureViewControllerViewWillAppear(_ photoCaptureViewController: PhotoCaptureViewController) { + if !photoCaptureViewController.isInBatchMode, attachmentCount == 1, case .camera(_) = attachmentDraftCollection.attachmentDrafts.last { + // User is navigating back to the camera screen, indicating they want to discard the previously captured item. + discardDraft() + } + } + func photoCaptureViewControllerDidTryToCaptureTooMany(_ photoCaptureViewController: PhotoCaptureViewController) { showTooManySelectedToast() } From 4a8511f0a086bc2f331b2112d8214cc73f06aa9b Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 28 Feb 2022 16:57:46 -0800 Subject: [PATCH 23/32] In-app camera bug fixes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • fixed bug when photo/video capture using physical volume buttons wouldn't work sometimes. • do not show lock icon when recording video using physical volume buttons. • fixed an issue when lock icon could be briefly visible upon start of the video recording. --- .../Photos/MediaControls.swift | 27 ++++++++++--------- .../Photos/PhotoCaptureViewController.swift | 3 +++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index 17eb11314f..e63197255e 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -48,6 +48,7 @@ class CameraCaptureControl: UIView { private lazy var slidingCircleView: CircleView = { let view = CircleView(diameter: CameraCaptureControl.recordingLockControlSize) view.backgroundColor = .ows_white + view.isHidden = true return view }() private lazy var lockIconView = LockView(frame: CGRect(origin: .zero, size: .square(CameraCaptureControl.recordingLockControlSize))) @@ -60,7 +61,7 @@ class CameraCaptureControl: UIView { button.dimsWhenHighlighted = true button.layer.masksToBounds = true button.layer.cornerRadius = 4 - button.alpha = 0 + button.isHidden = true return button }() @@ -176,9 +177,9 @@ class CameraCaptureControl: UIView { case .initial: // element visibility if slidingCircleHPositionConstraint != nil { - stopButton.alpha = 0 - slidingCircleView.alpha = 0 - lockIconView.alpha = 0 + stopButton.isHidden = true + slidingCircleView.isHidden = true + lockIconView.isHidden = true lockIconView.state = .unlocked } shutterButtonInnerCircle.alpha = 1 @@ -189,24 +190,26 @@ class CameraCaptureControl: UIView { case .recording: prepareRecordingControlsIfNecessary() + let recordingWithLongPress = longPressGesture.state != .possible + let sliderProgress = recordingWithLongPress ? sliderTrackingProgress : 0 // element visibility - stopButton.alpha = sliderTrackingProgress > 0 ? 1 : 0 - slidingCircleView.alpha = sliderTrackingProgress > 0 ? 1 : 0 - lockIconView.alpha = 1 - lockIconView.setState(sliderTrackingProgress > 0.5 ? .locking : .unlocked, animated: true) + stopButton.isHidden = sliderProgress == 0 + slidingCircleView.isHidden = sliderProgress == 0 + lockIconView.isHidden = !recordingWithLongPress + lockIconView.setState(sliderProgress > 0.5 ? .locking : .unlocked, animated: true) shutterButtonInnerCircle.backgroundColor = .ows_white // element sizes outerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonRecordingSize // Inner (white) circle gets smaller as user drags the slider and reveals stop button when the slider is halfway to the lock icon. - let circleSizeOffset = 2 * sliderTrackingProgress * (CameraCaptureControl.shutterButtonDefaultSize - CameraCaptureControl.recordingLockControlSize) + let circleSizeOffset = 2 * sliderProgress * (CameraCaptureControl.shutterButtonDefaultSize - CameraCaptureControl.recordingLockControlSize) innerCircleSizeConstraint.constant = CameraCaptureControl.shutterButtonDefaultSize - circleSizeOffset case .recordingLocked: prepareRecordingControlsIfNecessary() // element visibility - stopButton.alpha = 1 - slidingCircleView.alpha = 1 - lockIconView.alpha = 1 + stopButton.isHidden = false + slidingCircleView.isHidden = false + lockIconView.isHidden = false lockIconView.setState(.locked, animated: true) shutterButtonInnerCircle.alpha = 0 shutterButtonInnerCircle.backgroundColor = .ows_white diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 53e4ac2336..7b82500983 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -639,6 +639,9 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate private func captureReady() { self.hasCaptureStarted = true BenchEventComplete(eventId: "Show-Camera") + if isVisible { + VolumeButtons.shared?.addObserver(observer: photoCapture) + } } private func setupPhotoCapture() { From b139e05fdb28ac775e12e4d71fcf3bfc7f60e415 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 28 Feb 2022 17:49:05 -0800 Subject: [PATCH 24/32] Use simpler cross-fade transition between camera and media library. --- .../Photos/SendMediaNavigationController.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 3477e4ffbd..2568541230 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -313,7 +313,7 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { guard isGranted else { return } BenchEventStart(title: "Show-Media-Library", eventId: "Show-Media-Library") - self.pushViewController(self.mediaLibraryViewController, animated: true) + self.fadeTo(viewControllers: [self.mediaLibraryViewController], duration: 0.3) } } @@ -347,16 +347,11 @@ extension SendMediaNavigationController: ImagePickerGridControllerDelegate { } func imagePickerDidRequestPresentCamera(_ imagePicker: ImagePickerGridController) { - if let cameraViewController = viewControllers.first as? PhotoCaptureViewController { - popToViewController(cameraViewController, animated: true) - return - } - self.ows_askForCameraPermissions { isGranted in guard isGranted else { return } BenchEventStart(title: "Show-Camera", eventId: "Show-Camera") - self.fadeTo(viewControllers: [self.captureViewController], duration: 0.08) + self.fadeTo(viewControllers: [self.captureViewController], duration: 0.3) } } From f8824905c982f9a5f0788bfbdda8175cb9c10b90 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Mon, 28 Feb 2022 19:58:02 -0800 Subject: [PATCH 25/32] Remove OWSNavigationController.ows_preferredStatusBarStyle. Turns out it is not really necessary: a subclassed UINavigationController is presented and `preferredStatusBarStyle` override works just fine. --- .../Photos/PhotoCaptureViewController.swift | 4 ---- .../Photos/SendMediaNavigationController.swift | 13 ++++--------- SignalUI/ViewControllers/OWSNavigationController.h | 3 --- SignalUI/ViewControllers/OWSNavigationController.m | 5 ----- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 7b82500983..7225bff84f 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -154,10 +154,6 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate return true } - override var preferredStatusBarStyle: UIStatusBarStyle { - return .lightContent - } - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 2568541230..eb18432154 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -54,16 +54,11 @@ class SendMediaNavigationController: OWSNavigationController { return true } - override var ows_preferredStatusBarStyle: UIStatusBarStyle { - get { - if topViewController is ConversationPickerViewController { - return .default - } - return .lightContent - } - set { - super.ows_preferredStatusBarStyle = newValue + override var preferredStatusBarStyle: UIStatusBarStyle { + if topViewController is ConversationPickerViewController { + return .default } + return .lightContent } override func viewDidLoad() { diff --git a/SignalUI/ViewControllers/OWSNavigationController.h b/SignalUI/ViewControllers/OWSNavigationController.h index c238c212aa..6ae3c43aaf 100644 --- a/SignalUI/ViewControllers/OWSNavigationController.h +++ b/SignalUI/ViewControllers/OWSNavigationController.h @@ -28,9 +28,6 @@ NS_ASSUME_NONNULL_BEGIN // regardless of which view is currently visible. @property (nonatomic, nullable) NSNumber *ows_prefersStatusBarHidden; -// Defaults to UIStatusBarStyleDefault. If set to a non-default value, this status bar style will be used. -@property (nonatomic, assign) UIStatusBarStyle ows_preferredStatusBarStyle; - - (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; - (instancetype)initWithNavigationBarClass:(nullable Class)navigationBarClass toolbarClass:(nullable Class)toolbarClass NS_UNAVAILABLE; diff --git a/SignalUI/ViewControllers/OWSNavigationController.m b/SignalUI/ViewControllers/OWSNavigationController.m index 71eddcadb6..92260054ff 100644 --- a/SignalUI/ViewControllers/OWSNavigationController.m +++ b/SignalUI/ViewControllers/OWSNavigationController.m @@ -35,7 +35,6 @@ NS_ASSUME_NONNULL_BEGIN selector:@selector(themeDidChange:) name:ThemeDidChangeNotification object:nil]; - self.ows_preferredStatusBarStyle = UIStatusBarStyleDefault; return self; } @@ -140,10 +139,6 @@ NS_ASSUME_NONNULL_BEGIN if (!CurrentAppContext().isMainApp) { return super.preferredStatusBarStyle; } else { - if (self.ows_preferredStatusBarStyle != UIStatusBarStyleDefault) { - return self.ows_preferredStatusBarStyle; - } - UIViewController *presentedViewController = self.presentedViewController; if (presentedViewController != nil && !presentedViewController.isBeingDismissed) { return presentedViewController.preferredStatusBarStyle; From 4b876aec2cf42f3f080cc1abd7ea1692c740ff31 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 1 Mar 2022 13:45:13 -0800 Subject: [PATCH 26/32] Improvements to user flow in in-app camera controllers. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • media library picker is now presented modally from in-app camera. • there's no more "Camera" button in media library picker. --- .../Contents.json | 15 -- .../media-composer-camera-outline-28.pdf | 129 ------------------ .../Photos/ImagePickerController.swift | 16 --- .../SendMediaNavigationController.swift | 29 ++-- 4 files changed, 19 insertions(+), 170 deletions(-) delete mode 100644 Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json delete mode 100644 Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf diff --git a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json deleted file mode 100644 index e5795367ff..0000000000 --- a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "media-composer-camera-outline-28.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf b/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf deleted file mode 100644 index ab69c4365b..0000000000 --- a/Signal/Images.xcassets/media-composer-camera-outline-28.imageset/media-composer-camera-outline-28.pdf +++ /dev/null @@ -1,129 +0,0 @@ -%PDF-1.7 - -1 0 obj - << >> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 1.965088 2.899902 cm -0.000000 0.000000 0.000000 scn -12.034847 16.100098 m -15.034847 16.100098 17.534847 13.600098 17.534847 10.600098 c -17.534847 7.600098 15.034847 5.100098 12.034847 5.100098 c -9.034847 5.100098 6.534847 7.600098 6.534847 10.600098 c -6.534847 13.600098 9.034847 16.100098 12.034847 16.100098 c -12.034847 16.100098 l -h -12.034847 17.600098 m -8.134847 17.600098 5.034847 14.500097 5.034847 10.600098 c -5.034847 6.700098 8.134847 3.600098 12.034847 3.600098 c -15.934847 3.600098 19.034847 6.700098 19.034847 10.600098 c -19.034847 14.500097 15.934847 17.600098 12.034847 17.600098 c -h -14.734847 21.100098 m -14.834847 21.100098 15.034846 21.000097 15.134847 20.900097 c -17.034847 19.000097 l -17.434847 18.600098 l -18.934847 18.600098 l -20.634848 18.600098 20.934847 18.400097 21.334846 18.200098 c -21.734846 18.000097 22.034845 17.700098 22.234846 17.300098 c -22.434847 17.000097 22.634848 16.600098 22.634848 14.900098 c -22.634848 5.200098 l -22.634848 3.500097 22.434847 3.200098 22.234846 2.800098 c -22.034845 2.400099 21.734846 2.100098 21.334846 1.900097 c -21.034847 1.700096 20.634848 1.500097 18.934847 1.500097 c -5.134847 1.500097 l -3.434847 1.500097 3.134847 1.700096 2.734847 1.900097 c -2.334847 2.100098 2.034847 2.400099 1.834847 2.800098 c -1.634847 3.100098 1.434847 3.500097 1.434847 5.200098 c -1.434847 14.900098 l -1.434847 16.600098 1.634847 17.000097 1.834847 17.300098 c -2.034847 17.700098 2.334847 18.000097 2.734847 18.200098 c -3.034847 18.400097 3.434847 18.600098 5.134847 18.600098 c -6.634847 18.600098 l -7.034847 19.000097 l -8.834846 20.800098 l -9.034847 21.000097 9.334847 21.100098 9.534847 21.100098 c -14.734847 21.100098 l -14.734847 21.100098 l -h -14.734847 22.600098 m -9.634847 22.600098 l -8.934847 22.600098 8.334846 22.300098 7.834847 21.900097 c -6.034847 20.100098 l -5.134847 20.100098 l -4.134847 20.200098 3.034847 20.000097 2.134847 19.600098 c -1.434847 19.200098 0.934847 18.700098 0.534847 18.000097 c -0.034847 17.000097 -0.065153 16.000097 0.034847 14.900098 c -0.034847 5.200098 l --0.065153 4.200098 0.134847 3.100098 0.534847 2.100098 c -0.834847 1.500097 1.434847 0.900097 2.034847 0.600098 c -3.034847 0.200098 4.034847 0.000097 5.134847 0.100098 c -18.834846 0.100098 l -19.934847 0.000097 21.034847 0.200098 21.934847 0.600098 c -22.534847 0.900097 23.034847 1.400097 23.434847 2.100098 c -23.934847 3.100098 24.034847 4.200098 23.934847 5.200098 c -23.934847 14.900098 l -24.034847 16.000097 23.834846 17.000097 23.434847 18.000097 c -23.134848 18.600098 22.634848 19.200098 21.934847 19.500097 c -20.934847 20.000097 19.834846 20.100098 18.834846 20.000097 c -18.034847 20.000097 l -16.134848 21.900097 l -15.734848 22.400097 15.234847 22.600098 14.734847 22.600098 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 2785 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 28.000000 28.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Pages 5 0 R - /Type /Catalog - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000002875 00000 n -0000002898 00000 n -0000003071 00000 n -0000003145 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -3204 -%%EOF \ No newline at end of file diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index 6b89a1402f..e851856b48 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -8,7 +8,6 @@ import UIKit protocol ImagePickerGridControllerDelegate: AnyObject { func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController) - func imagePickerDidRequestPresentCamera(_ imagePicker: ImagePickerGridController) func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) func imagePicker(_ imagePicker: ImagePickerGridController, didSelectAsset asset: PHAsset, attachmentPromise: Promise) @@ -37,7 +36,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat private var titleView: TitleView! private var bottomBar: UIView! - private var cameraButton: CameraOverlayButton! private var doneButton: MediaDoneButton! init() { @@ -98,15 +96,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat bottomBar.autoPinBottomToSuperviewMargin(withInset: UIDevice.current.hasIPhoneXNotch ? 0 : 8) bottomBar.autoPinHorizontalEdges(toEdgesOf: view) - cameraButton = CameraOverlayButton(image: UIImage(named: "media-composer-camera-outline-28"), userInterfaceStyleOverride: .light) - cameraButton.addTarget(self, action: #selector(didTapCameraButton), for: .touchUpInside) - bottomBar.addSubview(cameraButton) - cameraButton.contentInsets = .zero - cameraButton.autoSetDimensions(to: .square(40)) - cameraButton.autoPinLeadingToSuperviewMargin() - cameraButton.autoPinTopToSuperviewMargin() - cameraButton.autoPinBottomToSuperviewMargin() - doneButton = MediaDoneButton() doneButton.userInterfaceStyleOverride = .light doneButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside) @@ -337,11 +326,6 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat self.delegate?.imagePickerDidCancel(self) } - @objc - private func didTapCameraButton() { - delegate?.imagePickerDidRequestPresentCamera(self) - } - @objc private func didTapDoneButton() { delegate?.imagePickerDidRequestSendMedia(self) diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index eb18432154..3369190335 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -4,6 +4,7 @@ import Foundation import Photos +import SignalUI @objc protocol SendMediaNavDelegate: AnyObject { @@ -308,7 +309,11 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDelegate { guard isGranted else { return } BenchEventStart(title: "Show-Media-Library", eventId: "Show-Media-Library") - self.fadeTo(viewControllers: [self.mediaLibraryViewController], duration: 0.3) + let presentedViewController = OWSNavigationController(rootViewController: self.mediaLibraryViewController) + if let owsNavBar = presentedViewController.navigationBar as? OWSNavigationBar { + owsNavBar.switchToStyle(.alwaysDarkAndClear) + } + self.presentFullScreen(presentedViewController, animated: true) } } @@ -338,19 +343,23 @@ extension SendMediaNavigationController: PhotoCaptureViewControllerDataSource { extension SendMediaNavigationController: ImagePickerGridControllerDelegate { func imagePickerDidRequestSendMedia(_ imagePicker: ImagePickerGridController) { + if let navigationController = presentedViewController as? OWSNavigationController, + navigationController.viewControllers.contains(imagePicker) { + dismiss(animated: true) { + self.showApprovalAfterProcessingAnyMediaLibrarySelections() + } + return + } showApprovalAfterProcessingAnyMediaLibrarySelections() } - func imagePickerDidRequestPresentCamera(_ imagePicker: ImagePickerGridController) { - self.ows_askForCameraPermissions { isGranted in - guard isGranted else { return } - - BenchEventStart(title: "Show-Camera", eventId: "Show-Camera") - self.fadeTo(viewControllers: [self.captureViewController], duration: 0.3) - } - } - func imagePickerDidCancel(_ imagePicker: ImagePickerGridController) { + if let navigationController = presentedViewController as? OWSNavigationController, + navigationController.viewControllers.contains(imagePicker) { + dismiss(animated: true) + return + } + let dontAbandonText = NSLocalizedString("SEND_MEDIA_RETURN_TO_MEDIA_LIBRARY", comment: "alert action when the user decides not to cancel the media flow after all.") didRequestExit(dontAbandonText: dontAbandonText) } From 4b9f2face3bfca5297eef90f4214060d21b11a31 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 1 Mar 2022 13:47:54 -0800 Subject: [PATCH 27/32] Make sure that focusing frame only appears within the camera viewfinder. --- .../Photos/PhotoCaptureViewController.swift | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 7225bff84f..fd25f3558e 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -522,43 +522,19 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate @objc func didTapFocusExpose(tapGesture: UITapGestureRecognizer) { - let viewLocation = tapGesture.location(in: view) + guard previewView.bounds.contains(tapGesture.location(in: previewView)) else { + return + } + + let viewLocation = tapGesture.location(in: previewView) let devicePoint = previewView.previewLayer.captureDevicePointConverted(fromLayerPoint: viewLocation) - photoCapture.focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true) -// // If the user taps near the capture button, it's more likely a mis-tap than intentional. -// // Skip the focus animation in that case, since it looks bad. -// let captureButtonOrigin = captureButton.superview!.convert(captureButton.frame.origin, to: view) -// if UIDevice.current.isIPad { -// guard viewLocation.x < captureButtonOrigin.x else { -// Logger.verbose("Skipping animation for right edge on iPad") -// -// // Finish any outstanding focus animation, otherwise it will remain in an -// // uncompleted state. -// if let lastUserFocusTapPoint = lastUserFocusTapPoint { -// completeFocusAnimation(forFocusPoint: lastUserFocusTapPoint) -// } -// return -// } -// } else { -// guard viewLocation.y < captureButtonOrigin.y else { -// Logger.verbose("Skipping animation for bottom row on iPhone") -// -// // Finish any outstanding focus animation, otherwise it will remain in an -// // uncompleted state. -// if let lastUserFocusTapPoint = lastUserFocusTapPoint { -// completeFocusAnimation(forFocusPoint: lastUserFocusTapPoint) -// } -// return -// } -// } - lastUserFocusTapPoint = devicePoint do { - let convertedPoint = tapToFocusView.superview!.convert(viewLocation, from: view) - positionTapToFocusView(center: convertedPoint) - tapToFocusView.superview?.layoutIfNeeded() + let focusFrameSuperview = tapToFocusView.superview! + positionTapToFocusView(center: tapGesture.location(in: focusFrameSuperview)) + focusFrameSuperview.layoutIfNeeded() startFocusAnimation() } } From 96c7b802dece69ff2801f6305ea614f59c0279ca Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 1 Mar 2022 20:27:01 -0800 Subject: [PATCH 28/32] Slightly tweak position of photos/videos in media editor. In order to minimize amount of moving elements when transitioning from in-app camera to media editor. --- .../AttachmentApprovalViewController.swift | 7 ++ .../AttachmentPrepViewController.swift | 107 +++++++++++++++--- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/SignalUI/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift b/SignalUI/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift index 8dfe3dbd58..90cd5dc9df 100644 --- a/SignalUI/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift +++ b/SignalUI/ViewControllers/AttachmentApproval/AttachmentApprovalViewController.swift @@ -237,6 +237,13 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTouchInterceptorView(gesture:))) touchInterceptorView.addGestureRecognizer(tapGesture) + let bottomToolViewWidth = view.bounds.width + let bottomToolViewHeight = bottomToolView.systemLayoutSizeFitting(view.bounds.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height + bottomToolView.frame = CGRect(x: 0, y: view.bounds.maxY - bottomToolViewHeight, width: bottomToolViewWidth, height: bottomToolViewHeight) + UIView.performWithoutAnimation { + bottomToolView.setNeedsLayout() + bottomToolView.layoutIfNeeded() + } view.addSubview(bottomToolView) bottomToolView.autoPinWidthToSuperview() bottomToolViewBottomConstraint = bottomToolView.autoPinEdge(toSuperviewEdge: .bottom) diff --git a/SignalUI/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift b/SignalUI/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift index 979769a14f..f272052dfd 100644 --- a/SignalUI/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift +++ b/SignalUI/ViewControllers/AttachmentApproval/AttachmentPrepViewController.swift @@ -37,7 +37,13 @@ public class AttachmentPrepViewController: OWSViewController { private(set) var contentContainer: UIView! private var imageEditorView: ImageEditorView? + private var imageEditorViewConstraintsPortrait: [NSLayoutConstraint]? + private var imageEditorViewConstraintsLandscape: [NSLayoutConstraint]? + private var videoEditorView: VideoEditorView? + private var videoEditorViewConstraintsPortrait: [NSLayoutConstraint]? + private var videoEditorViewConstraintsLandscape: [NSLayoutConstraint]? + private var mediaMessageView: MediaMessageView? public var shouldHideControls: Bool { @@ -61,20 +67,20 @@ public class AttachmentPrepViewController: OWSViewController { // MARK: - View Lifecycle override public func loadView() { - self.view = UIView() + view = UIView(frame: UIScreen.main.bounds) + view.backgroundColor = .ows_black // Anything that should be shrunk when user pops keyboard lives in the contentContainer. - let contentContainer = UIView() - self.contentContainer = contentContainer + contentContainer = UIView(frame: view.bounds) view.addSubview(contentContainer) contentContainer.autoPinEdgesToSuperviewEdges() // Scroll View - used to zoom/pan on images and video - scrollView = UIScrollView() - contentContainer.addSubview(scrollView) + scrollView = UIScrollView(frame: contentContainer.bounds) scrollView.delegate = self scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false + contentContainer.addSubview(scrollView) // Panning should stop pretty soon after the user stops scrolling scrollView.decelerationRate = UIScrollView.DecelerationRate.fast @@ -85,26 +91,38 @@ public class AttachmentPrepViewController: OWSViewController { scrollView.autoPinEdgesToSuperviewEdges() - let backgroundColor = UIColor.black - self.view.backgroundColor = backgroundColor - // Create full screen container view so the scrollView // can compute an appropriate content size in which to center // our media view. let containerView = UIView.container() + containerView.frame = view.bounds scrollView.addSubview(containerView) containerView.autoPinEdgesToSuperviewEdges() - containerView.autoMatch(.height, to: .height, of: self.view) - containerView.autoMatch(.width, to: .width, of: self.view) + containerView.autoMatch(.height, to: .height, of: view) + containerView.autoMatch(.width, to: .width, of: view) + + let contentMarginTop = UIDevice.current.hasIPhoneXNotch ? CurrentAppContext().statusBarHeight : 0 if let imageEditorModel = attachmentApprovalItem.imageEditorModel { let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) + imageEditorView.frame = view.bounds imageEditorView.configureSubviews() - self.imageEditorView = imageEditorView - + imageEditorView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(imageEditorView) - imageEditorView.autoPinEdgesToSuperviewEdges() + + imageEditorViewConstraintsPortrait = [ + imageEditorView.heightAnchor.constraint(equalTo: imageEditorView.widthAnchor, multiplier: 16/9), + imageEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + imageEditorView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + imageEditorView.topAnchor.constraint(equalTo: view.topAnchor, constant: contentMarginTop) ] + imageEditorViewConstraintsLandscape = [ + imageEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + imageEditorView.topAnchor.constraint(equalTo: view.topAnchor), + imageEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + imageEditorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ] + + self.imageEditorView = imageEditorView imageEditorUpdateNavigationBar() } else if let videoEditorModel = attachmentApprovalItem.videoEditorModel { @@ -112,11 +130,23 @@ public class AttachmentPrepViewController: OWSViewController { let videoEditorView = VideoEditorView(model: videoEditorModel, attachmentApprovalItem: attachmentApprovalItem, delegate: self) + videoEditorView.frame = view.bounds videoEditorView.configureSubviews() - self.videoEditorView = videoEditorView - + videoEditorView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(videoEditorView) - videoEditorView.autoPinEdgesToSuperviewEdges() + + videoEditorViewConstraintsPortrait = [ + videoEditorView.heightAnchor.constraint(equalTo: videoEditorView.widthAnchor, multiplier: 16/9), + videoEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + videoEditorView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + videoEditorView.topAnchor.constraint(equalTo: view.topAnchor, constant: contentMarginTop) ] + videoEditorViewConstraintsLandscape = [ + videoEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + videoEditorView.topAnchor.constraint(equalTo: view.topAnchor), + videoEditorView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + videoEditorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ] + + self.videoEditorView = videoEditorView videoEditorUpdateNavigationBar() } else { @@ -127,6 +157,12 @@ public class AttachmentPrepViewController: OWSViewController { } } + public override func viewDidLoad() { + super.viewDidLoad() + + updateLayoutConstraints() + } + override public func viewWillAppear(_ animated: Bool) { Logger.debug("") @@ -159,6 +195,14 @@ public class AttachmentPrepViewController: OWSViewController { positionBlurTooltip() } + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass { + updateLayoutConstraints() + } + } + // MARK: - Navigation Bar public func navigationBarItems() -> [UIView] { @@ -236,6 +280,37 @@ public class AttachmentPrepViewController: OWSViewController { } } + private func updateLayoutConstraints() { + let isPortraitLayout = traitCollection.horizontalSizeClass == .compact + + var constraintsToRemove: [NSLayoutConstraint]? + var constraintsToAdd: [NSLayoutConstraint]? + + if imageEditorView != nil { + if isPortraitLayout { + constraintsToRemove = imageEditorViewConstraintsLandscape + constraintsToAdd = imageEditorViewConstraintsPortrait + } else { + constraintsToRemove = imageEditorViewConstraintsPortrait + constraintsToAdd = imageEditorViewConstraintsLandscape + } + } else if videoEditorView != nil { + if isPortraitLayout { + constraintsToRemove = videoEditorViewConstraintsLandscape + constraintsToAdd = videoEditorViewConstraintsPortrait + } else { + constraintsToRemove = videoEditorViewConstraintsPortrait + constraintsToAdd = videoEditorViewConstraintsLandscape + } + } + if let constraintsToRemove = constraintsToRemove { + view.removeConstraints(constraintsToRemove) + } + if let constraintsToAdd = constraintsToAdd { + view.addConstraints(constraintsToAdd) + } + } + // MARK: - Tooltip private var shouldShowBlurTooltip: Bool { From 09048dad0722037b77822a48dee20e46293f6855 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Tue, 1 Mar 2022 23:12:15 -0800 Subject: [PATCH 29/32] Fix not being able to add more items using in-app camera. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce: • Take photo/video using in-app camera • In media editor, tap + • Observe previous photo/video deleted. --- .../ViewControllers/Photos/PhotoCaptureViewController.swift | 4 ++++ .../Photos/SendMediaNavigationController.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index fd25f3558e..3d45bb3d2e 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -231,6 +231,10 @@ class PhotoCaptureViewController: OWSViewController, InteractiveDismissDelegate } } + func switchToBatchMode() { + isInBatchMode = true + } + private(set) var isInBatchMode: Bool = false { didSet { let buttonImage = isInBatchMode ? ButtonImages.batchModeOn : ButtonImages.batchModeOff diff --git a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift index 3369190335..30129c6d2e 100644 --- a/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift +++ b/Signal/src/ViewControllers/Photos/SendMediaNavigationController.swift @@ -466,6 +466,10 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat // Current design dicates we'll go "back" to the single thing before us. owsAssertDebug(viewControllers.count == 2) + if let cameraViewController = viewControllers.first as? PhotoCaptureViewController { + cameraViewController.switchToBatchMode() + } + popViewController(animated: true) } From 45db3a963eab405526745bb9ed6dfe26d7cd63ef Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Wed, 2 Mar 2022 13:31:10 -0800 Subject: [PATCH 30/32] Minimize use of implicitly unwrapped optionals in code. And mark `init(coder:)` as unavailable. --- .../Photos/ImagePickerController.swift | 21 +-- .../Photos/MediaControls.swift | 172 +++++++++--------- .../Photos/PhotoCaptureViewController.swift | 127 ++++++------- 3 files changed, 144 insertions(+), 176 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/ImagePickerController.swift b/Signal/src/ViewControllers/Photos/ImagePickerController.swift index e851856b48..1b5a201af1 100644 --- a/Signal/src/ViewControllers/Photos/ImagePickerController.swift +++ b/Signal/src/ViewControllers/Photos/ImagePickerController.swift @@ -562,7 +562,7 @@ private class TitleView: UIView { private let label = UILabel() private let iconView = UIImageView() - private var stackView: UIStackView! + private let stackView: UIStackView // Returns same font as UIBarButtonItem uses. private func titleLabelFont() -> UIFont { @@ -573,18 +573,10 @@ private class TitleView: UIView { // MARK: - Initializers override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { stackView = UIStackView(arrangedSubviews: [label, iconView]) + + super.init(frame: frame) + stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = 5 @@ -605,6 +597,11 @@ private class TitleView: UIView { addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(titleTapped))) } + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() + } + override func tintColorDidChange() { super.tintColorDidChange() label.textColor = tintColor diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index e63197255e..f53a633b11 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -40,8 +40,8 @@ class CameraCaptureControl: UIView { private static let shutterButtonDefaultSize: CGFloat = 72 private static let shutterButtonRecordingSize: CGFloat = 122 - private var outerCircleSizeConstraint: NSLayoutConstraint! - private var innerCircleSizeConstraint: NSLayoutConstraint! + private let outerCircleSizeConstraint: NSLayoutConstraint + private let innerCircleSizeConstraint: NSLayoutConstraint private var slidingCircleHPositionConstraint: NSLayoutConstraint! private var slidingCircleVPositionConstraint: NSLayoutConstraint! @@ -67,23 +67,13 @@ class CameraCaptureControl: UIView { weak var delegate: CameraCaptureControlDelegate? - convenience init(axis: NSLayoutConstraint.Axis) { - self.init(frame: CGRect(origin: .zero, size: CameraCaptureControl.intrinsicContentSize(forAxis: axis))) + required init(axis: NSLayoutConstraint.Axis) { + innerCircleSizeConstraint = shutterButtonInnerCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) + outerCircleSizeConstraint = shutterButtonOuterCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) + + super.init(frame: CGRect(origin: .zero, size: CameraCaptureControl.intrinsicContentSize(forAxis: axis))) + self.axis = axis - reactivateConstraintsForCurrentAxis() - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { // Round Shutter Button addLayoutGuide(shutterButtonLayoutGuide) @@ -104,11 +94,9 @@ class CameraCaptureControl: UIView { addSubview(shutterButtonOuterCircle) shutterButtonOuterCircle.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor).isActive = true shutterButtonOuterCircle.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor).isActive = true - outerCircleSizeConstraint = shutterButtonOuterCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) shutterButtonOuterCircle.autoPin(toAspectRatio: 1) addSubview(shutterButtonInnerCircle) - innerCircleSizeConstraint = shutterButtonInnerCircle.autoSetDimension(.width, toSize: CameraCaptureControl.shutterButtonDefaultSize) shutterButtonInnerCircle.autoPin(toAspectRatio: 1) shutterButtonInnerCircle.isUserInteractionEnabled = false shutterButtonInnerCircle.backgroundColor = .clear @@ -119,13 +107,18 @@ class CameraCaptureControl: UIView { // 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(handleLongPress)) + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) longPressGesture.minimumPressDuration = 0 shutterButtonOuterCircle.addGestureRecognizer(longPressGesture) reactivateConstraintsForCurrentAxis() } + @available(*, unavailable, message: "Use init(axis:) instead") + required init?(coder: NSCoder) { + notImplemented() + } + // MARK: - UI State enum State { @@ -154,12 +147,13 @@ class CameraCaptureControl: UIView { } } - func setState(_ state: State, animationDuration: TimeInterval = 0) { + func setState(_ state: State, isRecordingWithLongPress: Bool = false, animationDuration: TimeInterval = 0) { guard _internalState != state else { return } Logger.debug("New state: \(_internalState) -> \(state)") _internalState = state + self.isRecordingWithLongPress = isRecordingWithLongPress if animationDuration > 0 { UIView.animate(withDuration: animationDuration, delay: 0, @@ -190,12 +184,11 @@ class CameraCaptureControl: UIView { case .recording: prepareRecordingControlsIfNecessary() - let recordingWithLongPress = longPressGesture.state != .possible - let sliderProgress = recordingWithLongPress ? sliderTrackingProgress : 0 + let sliderProgress = isRecordingWithLongPress ? sliderTrackingProgress : 0 // element visibility stopButton.isHidden = sliderProgress == 0 slidingCircleView.isHidden = sliderProgress == 0 - lockIconView.isHidden = !recordingWithLongPress + lockIconView.isHidden = !isRecordingWithLongPress lockIconView.setState(sliderProgress > 0.5 ? .locking : .unlocked, animated: true) shutterButtonInnerCircle.backgroundColor = .ows_white // element sizes @@ -307,7 +300,7 @@ class CameraCaptureControl: UIView { // MARK: - Gestures - private var longPressGesture: UILongPressGestureRecognizer! + private var isRecordingWithLongPress = false private static let longPressDurationThreshold = 0.5 private static let minDistanceBeforeActivatingLockSlider: CGFloat = 30 private var initialTouchLocation: CGPoint? @@ -340,7 +333,7 @@ class CameraCaptureControl: UIView { ) { [weak self] _ in guard let self = self else { return } - self.setState(.recording, animationDuration: 0.4) + self.setState(.recording, isRecordingWithLongPress: true, animationDuration: 0.4) self.delegate?.cameraCaptureControlDidRequestStartVideoRecording(self) } @@ -471,10 +464,10 @@ class CameraCaptureControl: UIView { private class LockView: UIView { - private var imageViewLock = UIImageView(image: UIImage(named: "media-composer-lock-outline-24")) - private var blurBackgroundView = CircleBlurView(effect: UIBlurEffect(style: .dark)) - private var whiteBackgroundView = CircleView() - private var whiteCircleView = CircleView() + private let imageViewLock = UIImageView(image: UIImage(named: "media-composer-lock-outline-24")) + private let blurBackgroundView = CircleBlurView(effect: UIBlurEffect(style: .dark)) + private let whiteBackgroundView = CircleView() + private let whiteCircleView = CircleView() enum State { case unlocked @@ -531,15 +524,7 @@ private class LockView: UIView { override init(frame: CGRect) { super.init(frame: frame) - commonInit() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { isUserInteractionEnabled = false addSubview(blurBackgroundView) @@ -562,6 +547,11 @@ private class LockView: UIView { updateAppearance() } + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() + } + override var intrinsicContentSize: CGSize { CGSize(width: CameraCaptureControl.recordingLockControlSize, height: CameraCaptureControl.recordingLockControlSize) } @@ -621,7 +611,11 @@ class CameraOverlayButton: UIButton, UserInterfaceStyleOverride { } } - private var backgroundView: UIVisualEffectView! + private let backgroundView: CircleBlurView = { + let view = CircleBlurView(effect: UIBlurEffect(style: .regular)) + view.isUserInteractionEnabled = false + return view + }() private static let visibleButtonSize: CGFloat = 36 // both height and width private static let defaultInset: CGFloat = 4 @@ -632,32 +626,28 @@ class CameraOverlayButton: UIButton, UserInterfaceStyleOverride { } } - convenience init(image: UIImage?, userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified) { - self.init(frame: CGRect(origin: .zero, size: .square(Self.visibleButtonSize + 2*Self.defaultInset))) + required init(image: UIImage?, userInterfaceStyleOverride: UIUserInterfaceStyle = .unspecified) { + super.init(frame: CGRect(origin: .zero, size: .square(Self.visibleButtonSize + 2*Self.defaultInset))) + self.userInterfaceStyleOverride = userInterfaceStyleOverride + + layoutMargins = contentInsets + + addSubview(backgroundView) + backgroundView.autoPinEdgesToSuperviewMargins() + setImage(image, for: .normal) updateStyle() } - private override init(frame: CGRect) { - super.init(frame: frame) - commonInit() + @available(*, unavailable, message: "Use init(image:userInterfaceStyleOverride:) instead") + override init(frame: CGRect) { + notImplemented() } + @available(*, unavailable, message: "Use init(image:userInterfaceStyleOverride:) instead") required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { - layoutMargins = contentInsets - - backgroundView = CircleBlurView(effect: UIBlurEffect(style: CameraOverlayButton.blurEffectStyle(for: effectiveUserInterfaceStyle))) - backgroundView.isUserInteractionEnabled = false - addSubview(backgroundView) - backgroundView.autoPinEdgesToSuperviewMargins() - - updateStyle() + notImplemented() } override func layoutSubviews() { @@ -700,14 +690,8 @@ class MediaDoneButton: UIButton, UserInterfaceStyleOverride { } } - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() + private static var font: UIFont { + return UIFont.ows_dynamicTypeSubheadline.ows_monospaced } private let numberFormatter: NumberFormatter = { @@ -720,22 +704,38 @@ class MediaDoneButton: UIButton, UserInterfaceStyleOverride { let label = UILabel() label.textColor = .ows_white label.textAlignment = .center - label.font = .ows_dynamicTypeSubheadline.ows_monospaced + label.font = MediaDoneButton.font return label }() - private var pillView: PillView! - private var blurBackgroundView: UIVisualEffectView! - private var chevronImageView: UIImageView! - private var dimmerView: UIView! - - private func commonInit() { - pillView = PillView(frame: bounds) + private let pillView: PillView = { + let pillView = PillView(frame: .zero) pillView.isUserInteractionEnabled = false pillView.layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 7) + return pillView + }() + private let blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + private let chevronImageView: UIImageView = { + let image: UIImage? + if #available(iOS 13, *) { + image = CurrentAppContext().isRTL ? UIImage(systemName: "chevron.backward") : UIImage(systemName: "chevron.right") + } else { + image = CurrentAppContext().isRTL ? UIImage(named: "chevron-left-20") : UIImage(named: "chevron-right-20") + } + let chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate)) + chevronImageView.contentMode = .center + if #available(iOS 13, *) { + chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: MediaDoneButton.font.pointSize) + } + return chevronImageView + }() + private var dimmerView: UIView? + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(pillView) pillView.autoPinEdgesToSuperviewEdges() - blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: MediaDoneButton.blurEffectStyle(for: effectiveUserInterfaceStyle))) pillView.addSubview(blurBackgroundView) blurBackgroundView.autoPinEdgesToSuperviewEdges() @@ -745,18 +745,6 @@ class MediaDoneButton: UIButton, UserInterfaceStyleOverride { blueBadgeView.addSubview(textLabel) textLabel.autoPinEdgesToSuperviewMargins() - let image: UIImage? - if #available(iOS 13, *) { - image = CurrentAppContext().isRTL ? UIImage(systemName: "chevron.backward") : UIImage(systemName: "chevron.right") - } else { - image = CurrentAppContext().isRTL ? UIImage(named: "chevron-left-20") : UIImage(named: "chevron-right-20") - } - chevronImageView = UIImageView(image: image!.withRenderingMode(.alwaysTemplate)) - chevronImageView.contentMode = .center - if #available(iOS 13, *) { - chevronImageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: textLabel.font.pointSize) - } - let hStack = UIStackView(arrangedSubviews: [blueBadgeView, chevronImageView]) hStack.spacing = 6 pillView.addSubview(hStack) @@ -765,6 +753,11 @@ class MediaDoneButton: UIButton, UserInterfaceStyleOverride { updateStyle() } + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() + } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { textLabel.font = .ows_dynamicTypeSubheadline.ows_monospaced @@ -781,13 +774,14 @@ class MediaDoneButton: UIButton, UserInterfaceStyleOverride { didSet { if isHighlighted { if dimmerView == nil { - dimmerView = UIView(frame: bounds) + let dimmerView = UIView(frame: bounds) dimmerView.isUserInteractionEnabled = false dimmerView.backgroundColor = .ows_black pillView.addSubview(dimmerView) dimmerView.autoPinEdgesToSuperviewEdges() + self.dimmerView = dimmerView } - dimmerView.alpha = 0.5 + dimmerView?.alpha = 0.5 } else if let dimmerView = dimmerView { dimmerView.alpha = 0 } diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 3d45bb3d2e..6282ea81ae 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -707,45 +707,38 @@ private struct ButtonImages { } private class TopBar: UIView { - private(set) var closeButton: CameraOverlayButton! + let closeButton = CameraOverlayButton(image: ButtonImages.close, userInterfaceStyleOverride: .dark) - private var cameraControlsContainerView: UIView! - private(set) var flashModeButton: CameraOverlayButton! - private(set) var batchModeButton: CameraOverlayButton! + private let cameraControlsContainerView: UIStackView + let flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) + let batchModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) - private(set) var recordingTimerView: RecordingTimerView! + let recordingTimerView = RecordingTimerView(frame: .zero) override init(frame: CGRect) { + cameraControlsContainerView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton ]) + super.init(frame: frame) - commonInit() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { layoutMargins = UIEdgeInsets(hMargin: 8, vMargin: 4) - closeButton = CameraOverlayButton(image: ButtonImages.close, userInterfaceStyleOverride: .dark) addSubview(closeButton) closeButton.autoPinHeightToSuperviewMargins() closeButton.autoPinLeadingToSuperviewMargin() - recordingTimerView = RecordingTimerView(frame: .zero) addSubview(recordingTimerView) recordingTimerView.autoPinHeightToSuperview(withMargin: 8) recordingTimerView.autoHCenterInSuperview() - flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) - batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) - let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton ]) - stackView.spacing = 16 - addSubview(stackView) - stackView.autoPinHeightToSuperviewMargins() - stackView.autoPinTrailingToSuperviewMargin() - cameraControlsContainerView = stackView + cameraControlsContainerView.spacing = 16 + addSubview(cameraControlsContainerView) + cameraControlsContainerView.autoPinHeightToSuperviewMargins() + cameraControlsContainerView.autoPinTrailingToSuperviewMargin() + } + + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() } // MARK: - Mode @@ -760,12 +753,12 @@ private class TopBar: UIView { case .cameraControls: closeButton.isHidden = false cameraControlsContainerView.isHidden = false - recordingTimerView?.isHidden = true + recordingTimerView.isHidden = true case .closeButton: closeButton.isHidden = false cameraControlsContainerView.isHidden = true - recordingTimerView?.isHidden = true + recordingTimerView.isHidden = true case .videoRecording: closeButton.isHidden = true @@ -793,47 +786,36 @@ private class BottomBar: UIView { } } - private(set) var photoLibraryButton: MediaPickerThumbnailButton! - private(set) var switchCameraButton: CameraOverlayButton! + let photoLibraryButton = MediaPickerThumbnailButton(frame: .zero) + let switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) let controlButtonsLayoutGuide = UILayoutGuide() // area encompassing Photo Library and Switch Camera buttons. - private(set) var captureControl: CameraCaptureControl! + let captureControl = CameraCaptureControl(axis: .horizontal) var shutterButtonLayoutGuide: UILayoutGuide { captureControl.shutterButtonLayoutGuide } override init(frame: CGRect) { super.init(frame: frame) - commonInit() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { layoutMargins = UIEdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 14) addLayoutGuide(controlButtonsLayoutGuide) addConstraints([ controlButtonsLayoutGuide.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), controlButtonsLayoutGuide.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) ]) - photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) photoLibraryButton.translatesAutoresizingMaskIntoConstraints = false addSubview(photoLibraryButton) addConstraints([ photoLibraryButton.leadingAnchor.constraint(equalTo: controlButtonsLayoutGuide.leadingAnchor), photoLibraryButton.centerYAnchor.constraint(equalTo: controlButtonsLayoutGuide.centerYAnchor), photoLibraryButton.topAnchor.constraint(greaterThanOrEqualTo: controlButtonsLayoutGuide.topAnchor) ]) - switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) switchCameraButton.translatesAutoresizingMaskIntoConstraints = false addSubview(switchCameraButton) addConstraints([ switchCameraButton.trailingAnchor.constraint(equalTo: controlButtonsLayoutGuide.trailingAnchor), switchCameraButton.topAnchor.constraint(greaterThanOrEqualTo: controlButtonsLayoutGuide.topAnchor), switchCameraButton.centerYAnchor.constraint(equalTo: controlButtonsLayoutGuide.centerYAnchor) ]) - captureControl = CameraCaptureControl(axis: .horizontal) captureControl.translatesAutoresizingMaskIntoConstraints = false addSubview(captureControl) captureControl.autoPinTopToSuperviewMargin() @@ -849,6 +831,11 @@ private class BottomBar: UIView { updateCompactHeightLayoutConstraints() } + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() + } + private func updateCompactHeightLayoutConstraints() { if isCompactHeightLayout { removeConstraints(regularHeightLayoutConstraints) @@ -869,49 +856,41 @@ private class SideBar: UIView { } } - private var cameraControlsContainerView: UIView! - private(set) var flashModeButton: CameraOverlayButton! - private(set) var batchModeButton: CameraOverlayButton! - private(set) var switchCameraButton: CameraOverlayButton! + private let cameraControlsContainerView: UIStackView + let flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) + let batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) + let switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) - private(set) var photoLibraryButton: MediaPickerThumbnailButton! + let photoLibraryButton = MediaPickerThumbnailButton(frame: .zero) private(set) var cameraCaptureControl = CameraCaptureControl(axis: .vertical) override init(frame: CGRect) { + cameraControlsContainerView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton, switchCameraButton ]) + super.init(frame: frame) - commonInit() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { layoutMargins = UIEdgeInsets(margin: 8) - flashModeButton = CameraOverlayButton(image: ButtonImages.flashAuto, userInterfaceStyleOverride: .dark) - switchCameraButton = CameraOverlayButton(image: ButtonImages.switchCamera, userInterfaceStyleOverride: .dark) - batchModeButton = CameraOverlayButton(image: ButtonImages.batchModeOff, userInterfaceStyleOverride: .dark) - let stackView = UIStackView(arrangedSubviews: [ batchModeButton, flashModeButton, switchCameraButton ]) - stackView.spacing = 16 - stackView.axis = .vertical - addSubview(stackView) - stackView.autoPinWidthToSuperviewMargins() - stackView.autoPinTopToSuperviewMargin() - cameraControlsContainerView = stackView + cameraControlsContainerView.spacing = 16 + cameraControlsContainerView.axis = .vertical + addSubview(cameraControlsContainerView) + cameraControlsContainerView.autoPinWidthToSuperviewMargins() + cameraControlsContainerView.autoPinTopToSuperviewMargin() addSubview(cameraCaptureControl) cameraCaptureControl.autoHCenterInSuperview() - cameraCaptureControl.shutterButtonLayoutGuide.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 36).isActive = true + cameraCaptureControl.shutterButtonLayoutGuide.topAnchor.constraint(equalTo: cameraControlsContainerView.bottomAnchor, constant: 36).isActive = true - photoLibraryButton = MediaPickerThumbnailButton(frame: CGRect(origin: .zero, size: .square(bounds.height))) addSubview(photoLibraryButton) photoLibraryButton.autoHCenterInSuperview() photoLibraryButton.topAnchor.constraint(equalTo: cameraCaptureControl.shutterButtonLayoutGuide.bottomAnchor, constant: 36).isActive = true photoLibraryButton.bottomAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.bottomAnchor).isActive = true + } + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() } } @@ -1114,8 +1093,9 @@ class CapturePreviewView: UIView { layer.addSublayer(previewLayer) } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "Use init(session:) instead") + required init?(coder: NSCoder) { + notImplemented() } } @@ -1123,15 +1103,7 @@ private class RecordingTimerView: PillView { override init(frame: CGRect) { super.init(frame: frame) - commonInit() - } - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { layoutMargins = UIEdgeInsets(hMargin: 16, vMargin: 0) let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) @@ -1148,9 +1120,14 @@ private class RecordingTimerView: PillView { updateView() } + @available(*, unavailable, message: "Use init(frame:) instead") + required init?(coder: NSCoder) { + notImplemented() + } + // MARK: - Subviews - private lazy var label: UILabel = { + private let label: UILabel = { let label = UILabel() label.font = UIFont.ows_monospacedDigitFont(withSize: 20) label.textAlignment = .center From d5f12ac4558acd89cce7762688927ac4f9000db9 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Wed, 2 Mar 2022 13:34:11 -0800 Subject: [PATCH 31/32] Check Photo Library auth status before attempting to load latest photo. Latest photo is used in in-app camera as an image for a button that opens photo library picker. --- .../Photos/PhotoCaptureViewController.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift index 6282ea81ae..d284d7cfcd 100644 --- a/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift +++ b/Signal/src/ViewControllers/Photos/PhotoCaptureViewController.swift @@ -1013,6 +1013,14 @@ private class MediaPickerThumbnailButton: UIButton { insertSubview(placeholderView, at: 0) placeholderView.autoPinEdgesToSuperviewEdges() + var authorizationStatus: PHAuthorizationStatus + if #available(iOS 14, *) { + authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) + } else { + authorizationStatus = PHPhotoLibrary.authorizationStatus() + } + guard authorizationStatus == .authorized else { return } + // Async Fetch last image DispatchQueue.global(qos: .userInteractive).async { let fetchOptions = PHFetchOptions() From f16047a20286912f0c13d4993de9aa1f85884561 Mon Sep 17 00:00:00 2001 From: Igor Solomennikov Date: Wed, 2 Mar 2022 16:38:32 -0800 Subject: [PATCH 32/32] Fix one more place where optionals didn't need implicit unwrapping. --- .../Photos/MediaControls.swift | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Signal/src/ViewControllers/Photos/MediaControls.swift b/Signal/src/ViewControllers/Photos/MediaControls.swift index f53a633b11..3248046e0f 100644 --- a/Signal/src/ViewControllers/Photos/MediaControls.swift +++ b/Signal/src/ViewControllers/Photos/MediaControls.swift @@ -42,8 +42,8 @@ class CameraCaptureControl: UIView { private let outerCircleSizeConstraint: NSLayoutConstraint private let innerCircleSizeConstraint: NSLayoutConstraint - private var slidingCircleHPositionConstraint: NSLayoutConstraint! - private var slidingCircleVPositionConstraint: NSLayoutConstraint! + private var slidingCircleHPositionConstraint: NSLayoutConstraint? + private var slidingCircleVPositionConstraint: NSLayoutConstraint? private lazy var slidingCircleView: CircleView = { let view = CircleView(diameter: CameraCaptureControl.recordingLockControlSize) @@ -228,13 +228,15 @@ class CameraCaptureControl: UIView { // 2. Slider. insertSubview(slidingCircleView, belowSubview: shutterButtonInnerCircle) slidingCircleView.translatesAutoresizingMaskIntoConstraints = false - slidingCircleHPositionConstraint = slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor) - var horizontalConstraints: [NSLayoutConstraint] = [ slidingCircleHPositionConstraint ] + let circleHPositionConstraint = slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor) + var horizontalConstraints: [NSLayoutConstraint] = [ circleHPositionConstraint ] horizontalConstraints.append(slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor)) + slidingCircleHPositionConstraint = circleHPositionConstraint - slidingCircleVPositionConstraint = slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor) - var verticalConstraints: [NSLayoutConstraint] = [ slidingCircleVPositionConstraint ] + let circleVPositionConstraint = slidingCircleView.centerYAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerYAnchor) + var verticalConstraints: [NSLayoutConstraint] = [ circleVPositionConstraint ] verticalConstraints.append(slidingCircleView.centerXAnchor.constraint(equalTo: shutterButtonLayoutGuide.centerXAnchor)) + slidingCircleVPositionConstraint = circleVPositionConstraint // 3. Lock Icon addSubview(lockIconView) @@ -438,21 +440,25 @@ class CameraCaptureControl: UIView { private func updateHorizontalTracking(xOffset: CGFloat) { let effectiveDistance = xOffset - Self.minDistanceBeforeActivatingLockSlider let distanceToLock = abs(lockIconView.center.x - stopButton.center.x) - let trackingPosition = effectiveDistance.clamp(0, distanceToLock) - slidingCircleHPositionConstraint.constant = trackingPosition + if let positionConstraint = slidingCircleHPositionConstraint { + let trackingPosition = effectiveDistance.clamp(0, distanceToLock) + positionConstraint.constant = trackingPosition + } sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1) - Logger.verbose("xOffset: \(xOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), trackingPosition: \(trackingPosition), progress: \(sliderTrackingProgress)") + Logger.debug("xOffset: \(xOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), progress: \(sliderTrackingProgress)") } private func updateVerticalTracking(yOffset: CGFloat) { let effectiveDistance = yOffset - Self.minDistanceBeforeActivatingLockSlider let distanceToLock = abs(lockIconView.center.y - stopButton.center.y) - let trackingPosition = effectiveDistance.clamp(0, distanceToLock) - slidingCircleVPositionConstraint.constant = trackingPosition + if let positionConstraint = slidingCircleVPositionConstraint { + let trackingPosition = effectiveDistance.clamp(0, distanceToLock) + positionConstraint.constant = trackingPosition + } sliderTrackingProgress = (effectiveDistance / distanceToLock).clamp(0, 1) - Logger.verbose("yOffset: \(yOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), trackingPosition: \(trackingPosition), progress: \(sliderTrackingProgress)") + Logger.debug("yOffset: \(yOffset), effectiveDistance: \(effectiveDistance), distanceToLock: \(distanceToLock), progress: \(sliderTrackingProgress)") } // MARK: - Button Actions