From 87bfdbb72cba7913c85c0fd2c701f09a311f84a9 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 14 Nov 2018 20:01:21 -0600 Subject: [PATCH] Sender Rail --- Signal.xcodeproj/project.pbxproj | 10 +- .../ic_small_x.imageset/Contents.json | 23 ++ .../ic_small_x.imageset/x-shadow-12@1x.png | Bin 0 -> 299 bytes .../ic_small_x.imageset/x-shadow-12@2x.png | Bin 0 -> 545 bytes .../ic_small_x.imageset/x-shadow-12@3x.png | Bin 0 -> 879 bytes .../ConversationViewController.m | 2 +- .../MediaPageViewController.swift | 23 +- .../AttachmentApprovalViewController.swift | 280 ++++++++++++++++-- .../Views}/GalleryRailView.swift | 113 ++++--- SignalMessaging/Views/OWSButton.swift | 32 ++ SignalMessaging/appearance/Theme.h | 1 + SignalMessaging/appearance/Theme.m | 5 + 12 files changed, 398 insertions(+), 91 deletions(-) create mode 100644 Signal/Images.xcassets/ic_small_x.imageset/Contents.json create mode 100644 Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@1x.png create mode 100644 Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@2x.png create mode 100644 Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@3x.png rename {Signal/src/views => SignalMessaging/Views}/GalleryRailView.swift (77%) create mode 100644 SignalMessaging/Views/OWSButton.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c0a7a72a6..c95fc3221 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -446,14 +446,15 @@ 4C3EF802210918740007EBF7 /* SSKProtoEnvelopeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */; }; 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AEC4420EC343B0020E72B /* DismissableTextField.swift */; }; 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4BC6C22102D697004040C9 /* ContactDiscoveryOperationTest.swift */; }; + 4C618199219DF03A009BD6B5 /* OWSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C618198219DF03A009BD6B5 /* OWSButton.swift */; }; 4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.supp */; }; 4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */; }; 4C7537892193779700DF5E37 /* OWS113MultiAttachmentMediaMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7537882193779700DF5E37 /* OWS113MultiAttachmentMediaMessages.swift */; }; 4C858A52212DC5E1001B45D3 /* UIImage+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */; }; 4C948FF72146EB4800349F0D /* BlockListCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C948FF62146EB4800349F0D /* BlockListCache.swift */; }; 4C9CA25D217E676900607C63 /* ZXingObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C9CA25C217E676900607C63 /* ZXingObjC.framework */; }; - 4CA46F4A219C78050038ABDE /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F49219C78050038ABDE /* GalleryRailView.swift */; }; 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F4B219CCC630038ABDE /* CaptionView.swift */; }; + 4CA46F4D219CFDAA0038ABDE /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F49219C78050038ABDE /* GalleryRailView.swift */; }; 4CA5F793211E1F06008C2708 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5F792211E1F06008C2708 /* Toast.swift */; }; 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; }; 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; }; @@ -1158,6 +1159,7 @@ 4C3EF801210918740007EBF7 /* SSKProtoEnvelopeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKProtoEnvelopeTest.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 = ""; }; + 4C618198219DF03A009BD6B5 /* OWSButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSButton.swift; sourceTree = ""; }; 4C63CBFF210A620B003AE45C /* SignalTSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalTSan.supp; sourceTree = ""; }; 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */ = {isa = PBXFileReference; lastKnownFileType = text; path = SignalUBSan.supp; sourceTree = ""; }; 4C7537882193779700DF5E37 /* OWS113MultiAttachmentMediaMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWS113MultiAttachmentMediaMessages.swift; sourceTree = ""; }; @@ -1699,6 +1701,7 @@ 34AC09FC211B39E700997B47 /* ContactTableViewCell.h */, 34AC09FF211B39E700997B47 /* ContactTableViewCell.m */, 34AC0A00211B39E700997B47 /* DisappearingTimerConfigurationView.swift */, + 4CA46F49219C78050038ABDE /* GalleryRailView.swift */, 34AC0A08211B39E900997B47 /* GradientView.swift */, 34AC0A06211B39E900997B47 /* OWSAlerts.swift */, 34AC0A09211B39E900997B47 /* OWSFlatButton.swift */, @@ -1715,6 +1718,7 @@ 34AC0A0D211B39EA00997B47 /* ThreadViewHelper.h */, 34AC0A0B211B39EA00997B47 /* ThreadViewHelper.m */, 34AC0A04211B39E800997B47 /* VideoPlayerView.swift */, + 4C618198219DF03A009BD6B5 /* OWSButton.swift */, ); path = Views; sourceTree = ""; @@ -2286,7 +2290,6 @@ 4CA5F792211E1F06008C2708 /* Toast.swift */, 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */, 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */, - 4CA46F49219C78050038ABDE /* GalleryRailView.swift */, 4CA46F4B219CCC630038ABDE /* CaptionView.swift */, ); name = Views; @@ -3247,6 +3250,7 @@ 342950822124C9750000B063 /* OWSTextField.m in Sources */, 34AC0A13211B39EA00997B47 /* DisappearingTimerConfigurationView.swift in Sources */, 3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */, + 4CA46F4D219CFDAA0038ABDE /* GalleryRailView.swift in Sources */, 34480B621FD0A98800BC14EF /* UIColor+OWS.m in Sources */, 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */, 34480B641FD0A98800BC14EF /* UIView+OWS.m in Sources */, @@ -3313,6 +3317,7 @@ 34B6D27520F664C900765BE2 /* OWSUnreadIndicator.m in Sources */, 346129A61FD1F09100532771 /* OWSContactsManager.m in Sources */, 4541B71D209D3B7A0008608F /* ContactShareViewModel.swift in Sources */, + 4C618199219DF03A009BD6B5 /* OWSButton.swift in Sources */, 4598198F204E2F28009414F2 /* OWS108CallLoggingPreference.m in Sources */, 34AC09F3211B39B100997B47 /* NewNonContactConversationViewController.m in Sources */, 34AC09FA211B39B100997B47 /* SharingThreadPickerViewController.m in Sources */, @@ -3469,7 +3474,6 @@ 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, 45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */, 45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */, - 4CA46F4A219C78050038ABDE /* GalleryRailView.swift in Sources */, 34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */, 45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */, 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */, diff --git a/Signal/Images.xcassets/ic_small_x.imageset/Contents.json b/Signal/Images.xcassets/ic_small_x.imageset/Contents.json new file mode 100644 index 000000000..dd24d1625 --- /dev/null +++ b/Signal/Images.xcassets/ic_small_x.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "x-shadow-12@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "x-shadow-12@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "x-shadow-12@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@1x.png b/Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..7cd37f5fe264719badef40acba6bbb12897fd69f GIT binary patch literal 299 zcmV+`0o4A9P)Px#WrBTnZvF`z+#Kt9YMAQNH}Ow-5Bn>R-TO|AwS@DZD0unwpuq1xKo=C5DB ze)<0W`=>*P4ut^4F-(SOAc+QfkZ327>}l4vwzeWr2%&ipY%9?7l_0s>w{N?_+zg61 xCt$>R0R>Pz2;_q_fg&yz7IAP*Kn77s0|4Z_e6321w0!^o002ovPDHLkV1jNxblU&` literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@2x.png b/Signal/Images.xcassets/ic_small_x.imageset/x-shadow-12@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a2af037b89792b63de80ebce249031d8fbe11c GIT binary patch literal 545 zcmV++0^a?JP)Px$+et)0R7efARohO&KoCXo8i`WTXjBsQ$xkpwDCpno;cjwH^?(A+X7O^aN_!sd_oBA6VI~r=H(2s~aGryqeKPDN4 zgUdit*Yz~?mSc-?m=i@1CyK@55oV=;b-?G#$V$Cle;5vj&j@k@`v6#zC&Km$$(I17 zTrOYGw^pk?!af9609WFekRFXjPh*H>S&dvSr(twj5HI;vzu$kQ?_e-^$z(ET@MUtY zu$)A-T0Q3kPVDu1&3rzu<9|>Pm%LOese+t7g+k#LKDz*y+$StGiAvmTP)Se_A2|}T zGoO)=9cYqcOR9vKDa>4uPe{a62%g^Px&DM>^@R9Fesm|JTTK@`WU=B`*Pl~}RR77+^8D@w&G3ZnSpm&r?>@?yS%AowXn zti6FCiV#Q$BFjH=OSzng(jdWXdD`WQV11BXq$b}CT%C7 z?2CXhQ0vMEMc9Bav)OF+X1m?a$K&yN+@H1UWrVm5By7il5n?VDi=|=vRW6sif%`L1 z0Yh;hBqdBblgT{q^?E<{A!zs>2G&r25qcV`PA6`Cg)lMbEOaRvjoxiGn;&Sq-|v6V z=kt&3{|Kdt04>z(^|x#wLIZ%hj;Dh{nK0{>N@ZK@^4jfoU&HPk)D?&$nWskpwW&4) z)Mh{^H<%sOW~bA6mq;X5&?iGeVim=Ku#$7S2-n^-M-u?b7smGbHS~1>%1~h&hp>{a zlli8LwNb&)M;}kQ!R)G@u?Hq`$ra}rm!m?l-Ck+%np$v_K?w^L0@1Id{R9_FZwKkK~Din8kF0VluD&9#bWX6Bw&PF zg<1jBhUpCzrqO8ZVk&Q;@d9*|El2wGyb_ALH8hB)1L+3<6k#aT)d4Hdn%Uo{3ywxHqw#V<4ti103>T?!cfY%%l%! zQ+0%8YM-h@{#3pOP1TDcgIWT55wFruk!RjuN56vlG&IZ+LRKGl{>wP9tA literal 0 HcmV?d00001 diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 5f29c6e77..68df293ab 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -90,7 +90,7 @@ @import Photos; -//#define FEATURE_FLAG_ALBUM_SEND_ENABLED; +#define FEATURE_FLAG_ALBUM_SEND_ENABLED NS_ASSUME_NONNULL_BEGIN diff --git a/Signal/src/ViewControllers/MediaPageViewController.swift b/Signal/src/ViewControllers/MediaPageViewController.swift index f386a6589..cfb192caa 100644 --- a/Signal/src/ViewControllers/MediaPageViewController.swift +++ b/Signal/src/ViewControllers/MediaPageViewController.swift @@ -331,7 +331,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou return } - galleryRailView.configure(itemProvider: currentItem.album, focusedItem: currentItem) + galleryRailView.configureCellViews(itemProvider: currentItem.album, focusedItem: currentItem) } // MARK: Actions @@ -764,6 +764,27 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } } +extension MediaGalleryItem: GalleryRailItem { + public var aspectRatio: CGFloat { + return self.imageSize.aspectRatio + } + + public func getRailImage() -> Promise { + let (guarantee, fulfill) = Guarantee.pending() + if let image = self.thumbnailImage(async: { fulfill($0) }) { + fulfill(image) + } + + return Promise(guarantee) + } +} + +extension MediaGalleryAlbum: GalleryRailItemProvider { + var railItems: [GalleryRailItem] { + return self.items + } +} + extension MediaPageViewController: GalleryRailViewDelegate { func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) { guard let targetItem = imageRailItem as? MediaGalleryItem else { diff --git a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift index f802ea5ce..d41c5e00d 100644 --- a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift @@ -5,6 +5,7 @@ import Foundation import AVFoundation import MediaPlayer +import PromiseKit @objc public protocol AttachmentApprovalViewControllerDelegate: class { @@ -12,8 +13,82 @@ public protocol AttachmentApprovalViewControllerDelegate: class { func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) } -struct SignalAttachmentItem: Hashable { +class AttachmentItemCollection { + private (set) var attachmentItems: [SignalAttachmentItem] + init(attachmentItems: [SignalAttachmentItem]) { + self.attachmentItems = attachmentItems + } + + func itemAfter(item: SignalAttachmentItem) -> SignalAttachmentItem? { + guard let currentIndex = attachmentItems.index(of: item) else { + owsFailDebug("currentIndex was unexpectedly nil") + return nil + } + + let nextIndex = attachmentItems.index(after: currentIndex) + + return attachmentItems[safe: nextIndex] + } + + func itemBefore(item: SignalAttachmentItem) -> SignalAttachmentItem? { + guard let currentIndex = attachmentItems.index(of: item) else { + owsFailDebug("currentIndex was unexpectedly nil") + return nil + } + + let prevIndex = attachmentItems.index(before: currentIndex) + + return attachmentItems[safe: prevIndex] + } + + func remove(item: SignalAttachmentItem) { + attachmentItems = attachmentItems.filter { $0 != item } + } +} + +class SignalAttachmentItem: Hashable { + + enum SignalAttachmentItemError: Error { + case noThumbnail + } + let attachment: SignalAttachment + + init(attachment: SignalAttachment) { + self.attachment = attachment + } + + // MARK: + + var imageSize: CGSize = .zero + + func getThumbnailImage() -> Promise { + return DispatchQueue.global().async(.promise) { () -> UIImage in + guard let image = self.attachment.image() else { + throw SignalAttachmentItemError.noThumbnail + } + return image + }.tap { result in + switch result { + case .fulfilled(let image): + self.imageSize = image.size + default: break + } + + } + } + + // MARK: Hashable + + public var hashValue: Int { + return attachment.hashValue + } + + // MARK: Equatable + + static func == (lhs: SignalAttachmentItem, rhs: SignalAttachmentItem) -> Bool { + return lhs.attachment == rhs.attachment + } } @objc @@ -37,7 +112,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC @objc required public init(attachments: [SignalAttachment]) { assert(attachments.count > 0) - self.attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} + let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} + self.attachmentItemCollection = AttachmentItemCollection(attachmentItems: attachmentItems) super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [UIPageViewControllerOptionInterPageSpacingKey: kSpacingBetweenItems]) @@ -62,6 +138,9 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: View Lifecycle + let galleryRailView = GalleryRailView() + let railContainerView = UIView() + override public func viewDidLoad() { super.viewDidLoad() @@ -69,6 +148,27 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC disablePagingIfNecessary() + railContainerView.backgroundColor = UIColor.black.withAlphaComponent(0.6) + view.addSubview(railContainerView) + railContainerView.preservesSuperviewLayoutMargins = true + railContainerView.layoutMargins.bottom = 50 + railContainerView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + + let footerGradientView = GradientView(from: .clear, to: .black) + railContainerView.addSubview(footerGradientView) + footerGradientView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) + footerGradientView.autoSetDimension(.height, toSize: ScaleFromIPhone5(100)) + + railContainerView.addSubview(galleryRailView) + galleryRailView.delegate = self + galleryRailView.scrollFocusMode = .keepWithinBounds + + galleryRailView.autoPinEdge(toSuperviewEdge: .leading) + galleryRailView.autoPinEdge(toSuperviewEdge: .trailing) + galleryRailView.autoPinEdge(toSuperviewMargin: .top) + galleryRailView.autoPinEdge(toSuperviewMargin: .bottom) + galleryRailView.autoSetDimension(.height, toSize: 72) + // Bottom Toolbar let captioningToolbar = CaptioningToolbar() @@ -124,6 +224,62 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - View Helpers + func remove(attachmentItem: SignalAttachmentItem) { + if attachmentItem == currentItem { + if let nextItem = attachmentItemCollection.itemAfter(item: attachmentItem) { + setCurrentItem(nextItem, direction: .forward, animated: true) + } else if let prevItem = attachmentItemCollection.itemBefore(item: attachmentItem) { + setCurrentItem(prevItem, direction: .reverse, animated: true) + } else { + owsFailDebug("removing last item shouldn't be possible because rail should not be visible") + return + } + } + + guard let cell = galleryRailView.cellViews.first(where: { $0.item === attachmentItem }) else { + owsFailDebug("cell was unexpectedly nil") + return + } + + UIView.animate(withDuration: 0.2, + animations: { + // shrink stack view item until it disappears + cell.isHidden = true + + // simultaneously fade out + cell.alpha = 0 + }, + completion: { _ in + self.attachmentItemCollection.remove(item: attachmentItem) + self.updateMediaRail() + }) + } + + func addDeleteIcon(cellViews: [GalleryRailCellView]) { + for cellView in cellViews { + guard let attachmentItem = cellView.item as? SignalAttachmentItem else { + owsFailDebug("attachmentItem was unexpectedly nil") + return + } + + let button = OWSButton { [weak self] in + guard let self = self else { return } + self.remove(attachmentItem: attachmentItem) + } + button.setImage(#imageLiteral(resourceName: "ic_small_x"), for: .normal) + + let kInsetDistance: CGFloat = 5 + button.imageEdgeInsets = UIEdgeInsets(top: kInsetDistance, left: kInsetDistance, bottom: kInsetDistance, right: kInsetDistance) + + cellView.addSubview(button) + + let kButtonWidth: CGFloat = 9 + kInsetDistance * 2 + button.autoSetDimensions(to: CGSize(width: kButtonWidth, height: kButtonWidth)) + button.autoPinEdge(toSuperviewMargin: .top) + button.autoPinEdge(toSuperviewMargin: .trailing) + } + } + var pagerScrollView: UIScrollView? // This is kind of a hack. Since we don't have first class access to the superview's `scrollView` // we traverse the view hierarchy until we find it, then disable scrolling if there's only one @@ -195,6 +351,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC }, completion: nil) previousPage.zoomOut(animated: false) + updateMediaRail() } } } @@ -274,10 +431,28 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } self.setViewControllers([page], direction: direction, animated: isAnimated, completion: nil) - // TODO update rail + updateMediaRail() + } + + func updateMediaRail() { + guard let currentItem = self.currentItem else { + owsFailDebug("currentItem was unexpectedly nil") + return + } + + galleryRailView.configureCellViews(itemProvider: attachmentItemCollection, focusedItem: currentItem) + addDeleteIcon(cellViews: galleryRailView.cellViews) + + railContainerView.isHidden = attachmentItemCollection.attachmentItems.count < 2 + captioningToolbar.alwaysShowGradient = railContainerView.isHidden + } + + let attachmentItemCollection: AttachmentItemCollection + + var attachmentItems: [SignalAttachmentItem] { + return attachmentItemCollection.attachmentItems } - let attachmentItems: [SignalAttachmentItem] var attachments: [SignalAttachment] { return attachmentItems.map { $0.attachment } } @@ -348,6 +523,49 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } } +// MARK: GalleryRail + +extension SignalAttachmentItem: GalleryRailItem { + var aspectRatio: CGFloat { + return self.imageSize.aspectRatio + } + + func getRailImage() -> Promise { + return self.getThumbnailImage() + } +} + +extension AttachmentItemCollection: GalleryRailItemProvider { + var railItems: [GalleryRailItem] { + return self.attachmentItems + } +} + +extension AttachmentApprovalViewController: GalleryRailViewDelegate { + public func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) { + guard let targetItem = imageRailItem as? SignalAttachmentItem else { + owsFailDebug("unexpected imageRailItem: \(imageRailItem)") + return + } + + guard let currentIndex = attachmentItems.index(of: currentItem) else { + owsFailDebug("currentIndex was unexpectedly nil") + return + } + + guard let targetIndex = attachmentItems.index(of: targetItem) else { + owsFailDebug("targetIndex was unexpectedly nil") + return + } + + let direction: NavigationDirection = currentIndex < targetIndex ? .forward : .reverse + + self.setCurrentItem(targetItem, direction: direction, animated: true) + } +} + +// MARK: - Individual Page + public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate { // We sometimes shrink the attachment view so that it remains somewhat visible // when the keyboard is presented. @@ -734,8 +952,8 @@ class CaptioningToolbar: UIView, UITextViewDelegate { set { self.textView.text = newValue } } - private let bottomGradient: GradientView - private let lengthLimitLabel: UILabel + private let bottomGradient: GradientView = GradientView(from: .clear, to: .black) + private let lengthLimitLabel: UILabel = UILabel() // Layout Constants @@ -770,10 +988,8 @@ class CaptioningToolbar: UIView, UITextViewDelegate { init() { self.sendButton = UIButton(type: .system) - self.bottomGradient = GradientView(from: UIColor.clear, to: UIColor.black) self.textView = MessageTextView() self.textViewHeight = kMinTextViewHeight - self.lengthLimitLabel = UILabel() super.init(frame: CGRect.zero) @@ -785,15 +1001,13 @@ class CaptioningToolbar: UIView, UITextViewDelegate { textView.delegate = self textView.keyboardAppearance = Theme.keyboardAppearance - textView.backgroundColor = (Theme.isDarkThemeEnabled ? UIColor.ows_gray90 : UIColor.ows_gray02) - textView.layer.borderColor = (Theme.isDarkThemeEnabled - ? Theme.primaryColor.withAlphaComponent(0.06).cgColor - : Theme.primaryColor.withAlphaComponent(0.12).cgColor) + textView.backgroundColor = Theme.darkThemeBackgroundColor + textView.layer.borderColor = Theme.darkThemePrimaryColor.cgColor textView.layer.borderWidth = 0.5 textView.layer.cornerRadius = kMinTextViewHeight / 2 textView.font = UIFont.ows_dynamicTypeBody - textView.textColor = Theme.primaryColor + textView.textColor = Theme.darkThemePrimaryColor textView.returnKeyType = .done textView.textContainerInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7) textView.scrollIndicatorInsets = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 3) @@ -804,17 +1018,7 @@ class CaptioningToolbar: UIView, UITextViewDelegate { sendButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: 16) sendButton.titleLabel?.textAlignment = .center - sendButton.tintColor = UIColor.white - sendButton.backgroundColor = UIColor.ows_systemPrimaryButton - sendButton.layer.cornerRadius = 4 - - // Send Button Shadow - without this the send button bottom doesn't feel aligned with the toolbar. - let kSendButtonShadowOffset: CGFloat = 1 - sendButton.layer.shadowColor = UIColor.darkGray.cgColor - sendButton.layer.shadowOffset = CGSize(width: 0, height: kSendButtonShadowOffset) - sendButton.layer.shadowOpacity = 0.8 - sendButton.layer.shadowRadius = 0.0 - sendButton.layer.masksToBounds = false + sendButton.tintColor = Theme.galleryHighlightColor // Increase hit area of send button sendButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8) @@ -831,13 +1035,14 @@ class CaptioningToolbar: UIView, UITextViewDelegate { self.lengthLimitLabel.isHidden = true let contentView = UIView() - addSubview(contentView) - contentView.autoPinEdgesToSuperviewEdges() contentView.addSubview(bottomGradient) contentView.addSubview(sendButton) contentView.addSubview(textView) contentView.addSubview(lengthLimitLabel) + addSubview(contentView) + contentView.autoPinEdgesToSuperviewEdges() + // Layout let kToolbarMargin: CGFloat = 8 @@ -860,9 +1065,7 @@ class CaptioningToolbar: UIView, UITextViewDelegate { textView.autoPinEdge(toSuperviewMargin: .bottom) sendButton.autoPinEdge(.left, to: .right, of: textView, withOffset: kToolbarMargin) - - // Because the textview has a border, the sendButton feels unaligned without this shadow and offset - sendButton.autoPinEdge(.bottom, to: .bottom, of: textView, withOffset: -kSendButtonShadowOffset) + sendButton.autoPinEdge(.bottom, to: .bottom, of: textView, withOffset: -3) sendButton.autoPinEdge(toSuperviewMargin: .right) sendButton.setContentHuggingHigh() @@ -874,6 +1077,7 @@ class CaptioningToolbar: UIView, UITextViewDelegate { lengthLimitLabel.setContentHuggingHigh() lengthLimitLabel.setCompressionResistanceHigh() + bottomGradient.isHidden = true let bottomGradientHeight = ScaleFromIPhone5(100) bottomGradient.autoSetDimension(.height, toSize: bottomGradientHeight) bottomGradient.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top) @@ -931,12 +1135,30 @@ class CaptioningToolbar: UIView, UITextViewDelegate { } } + var alwaysShowGradient: Bool = false { + didSet { + if alwaysShowGradient { + bottomGradient.isHidden = false + } + } + } + public func textViewDidBeginEditing(_ textView: UITextView) { self.captioningToolbarDelegate?.captioningToolbarDidBeginEditing(self) + if !alwaysShowGradient { + UIView.animate(withDuration: 0.2) { + self.bottomGradient.isHidden = false + } + } } public func textViewDidEndEditing(_ textView: UITextView) { self.captioningToolbarDelegate?.captioningToolbarDidEndEditing(self) + if !alwaysShowGradient { + UIView.animate(withDuration: 0.2) { + self.bottomGradient.isHidden = true + } + } } // MARK: - Helpers diff --git a/Signal/src/views/GalleryRailView.swift b/SignalMessaging/Views/GalleryRailView.swift similarity index 77% rename from Signal/src/views/GalleryRailView.swift rename to SignalMessaging/Views/GalleryRailView.swift index e43358dd0..85c1bcc86 100644 --- a/Signal/src/views/GalleryRailView.swift +++ b/SignalMessaging/Views/GalleryRailView.swift @@ -4,51 +4,20 @@ import PromiseKit -protocol GalleryRailItemProvider: class { +public protocol GalleryRailItemProvider: class { var railItems: [GalleryRailItem] { get } } -protocol GalleryRailItem: class { - func getRailImage() -> Guarantee +public protocol GalleryRailItem: class { + func getRailImage() -> Promise var aspectRatio: CGFloat { get } } -extension CGSize { - var aspectRatio: CGFloat { - guard self.height > 0 else { - return 0 - } - - return self.width / self.height - } -} - -extension MediaGalleryItem: GalleryRailItem { - var aspectRatio: CGFloat { - return self.imageSize.aspectRatio - } - - func getRailImage() -> Guarantee { - let (guarantee, fulfill) = Guarantee.pending() - if let image = self.thumbnailImage(async: { fulfill($0) }) { - fulfill(image) - } - - return guarantee - } -} - -extension MediaGalleryAlbum: GalleryRailItemProvider { - var railItems: [GalleryRailItem] { - return self.items - } -} - protocol GalleryRailCellViewDelegate: class { func didTapGalleryRailCellView(_ galleryRailCellView: GalleryRailCellView) } -class GalleryRailCellView: UIView { +public class GalleryRailCellView: UIView { weak var delegate: GalleryRailCellViewDelegate? @@ -98,7 +67,7 @@ class GalleryRailCellView: UIView { self.isSelected = isSelected if isSelected { layoutMargins = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6) - imageView.layer.borderColor = UIColor(rgbHex: 0x1f8fe8).cgColor + imageView.layer.borderColor = Theme.galleryHighlightColor.cgColor imageView.layer.borderWidth = 2 imageView.layer.cornerRadius = 2 } else { @@ -120,13 +89,19 @@ class GalleryRailCellView: UIView { }() } -protocol GalleryRailViewDelegate: class { +public protocol GalleryRailViewDelegate: class { func galleryRailView(_ galleryRailView: GalleryRailView, didTapItem imageRailItem: GalleryRailItem) } -class GalleryRailView: UIView, GalleryRailCellViewDelegate { +public class GalleryRailView: UIView, GalleryRailCellViewDelegate { - weak var delegate: GalleryRailViewDelegate? + public weak var delegate: GalleryRailViewDelegate? + + public var cellViews: [GalleryRailCellView] = [] + + var cellViewItems: [GalleryRailItem] { + get { return cellViews.compactMap { $0.item } } + } // MARK: Initializers @@ -143,13 +118,14 @@ class GalleryRailView: UIView, GalleryRailCellViewDelegate { // MARK: Public - public func configure(itemProvider: GalleryRailItemProvider?, focusedItem: GalleryRailItem?) { + public func configureCellViews(itemProvider: GalleryRailItemProvider?, focusedItem: GalleryRailItem?) { let animationDuration: TimeInterval = 0.2 guard let itemProvider = itemProvider else { UIView.animate(withDuration: animationDuration) { self.isHidden = true } + self.cellViews = [] return } @@ -183,6 +159,7 @@ class GalleryRailView: UIView, GalleryRailCellViewDelegate { self.isHidden = true }, completion: { _ in cellViews.forEach { $0.removeFromSuperview() } }) + self.cellViews = [] return } @@ -234,10 +211,10 @@ class GalleryRailView: UIView, GalleryRailCellViewDelegate { } } - var cellViews: [GalleryRailCellView] = [] - var cellViewItems: [GalleryRailItem] { - get { return cellViews.compactMap { $0.item } } + enum ScrollFocusMode { + case keepCentered, keepWithinBounds } + var scrollFocusMode: ScrollFocusMode = .keepCentered func updateFocusedItem(_ focusedItem: GalleryRailItem?) { var selectedCellView: GalleryRailCellView? cellViews.forEach { cellView in @@ -251,20 +228,42 @@ class GalleryRailView: UIView, GalleryRailCellViewDelegate { } self.layoutIfNeeded() - guard let selectedCell = selectedCellView else { - owsFailDebug("selectedCell was unexpectedly nil") - return + switch scrollFocusMode { + case .keepCentered: + guard let selectedCell = selectedCellView else { + owsFailDebug("selectedCell was unexpectedly nil") + return + } + + let cellViewCenter = selectedCell.superview!.convert(selectedCell.center, to: scrollView) + let additionalInset = scrollView.center.x - cellViewCenter.x + + var inset = scrollView.contentInset + inset.left = additionalInset + scrollView.contentInset = inset + + var offset = scrollView.contentOffset + offset.x = -additionalInset + scrollView.contentOffset = offset + case .keepWithinBounds: + guard let selectedCell = selectedCellView else { + owsFailDebug("selectedCell was unexpectedly nil") + return + } + + let cellFrame = selectedCell.superview!.convert(selectedCell.frame, to: scrollView) + + scrollView.scrollRectToVisible(cellFrame, animated: true) } - - let cellViewCenter = selectedCell.superview!.convert(selectedCell.center, to: scrollView) - let additionalInset = scrollView.center.x - cellViewCenter.x - - var inset = scrollView.contentInset - inset.left = additionalInset - scrollView.contentInset = inset - - var offset = scrollView.contentOffset - offset.x = -additionalInset - scrollView.contentOffset = offset + } +} + +public extension CGSize { + var aspectRatio: CGFloat { + guard self.height > 0 else { + return 0 + } + + return self.width / self.height } } diff --git a/SignalMessaging/Views/OWSButton.swift b/SignalMessaging/Views/OWSButton.swift new file mode 100644 index 000000000..4ecae71bd --- /dev/null +++ b/SignalMessaging/Views/OWSButton.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import UIKit + +@objc +public class OWSButton: UIButton { + + @objc + var block: () -> Void = { } + + // MARK: - + + @objc + init(block: @escaping () -> Void = { }) { + super.init(frame: .zero) + self.block = block + self.addTarget(self, action: #selector(didTap), for: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - + + @objc + func didTap() { + block() + } +} diff --git a/SignalMessaging/appearance/Theme.h b/SignalMessaging/appearance/Theme.h index 580825752..23dabdc6c 100644 --- a/SignalMessaging/appearance/Theme.h +++ b/SignalMessaging/appearance/Theme.h @@ -41,6 +41,7 @@ extern NSString *const ThemeDidChangeNotification; @property (class, readonly, nonatomic) UIColor *darkThemeBackgroundColor; @property (class, readonly, nonatomic) UIColor *darkThemePrimaryColor; +@property (class, readonly, nonatomic) UIColor *galleryHighlightColor; #pragma mark - diff --git a/SignalMessaging/appearance/Theme.m b/SignalMessaging/appearance/Theme.m index f9e523c83..517efec5c 100644 --- a/SignalMessaging/appearance/Theme.m +++ b/SignalMessaging/appearance/Theme.m @@ -128,6 +128,11 @@ NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled"; return UIColor.ows_gray05Color; } ++ (UIColor *)galleryHighlightColor +{ + return [UIColor colorWithRGBHex:0x1f8fe8]; +} + + (UIColor *)conversationButtonBackgroundColor { return (Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.35f alpha:1.f] : UIColor.ows_gray02Color);