From 8b24fba095df7e39c79e29f410e654885e8ad466 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 15 Nov 2018 10:25:48 -0500 Subject: [PATCH] Add "add more" button to image picker. Provide caption editing continuity. --- .../album_add_more.imageset/Contents.json | 23 +++ .../album_add_more@1x.png | Bin 0 -> 1487 bytes .../album_add_more@2x.png | Bin 0 -> 1836 bytes .../album_add_more@3x.png | Bin 0 -> 2448 bytes .../ConversationViewController.m | 2 +- .../PhotoLibrary/ImagePickerController.swift | 169 ++++++++++++++---- .../PhotoCollectionPickerController.swift | 32 +++- .../PhotoLibrary/PhotoLibrary.swift | 8 +- Signal/src/views/PhotoGridViewCell.swift | 4 +- .../AttachmentApprovalViewController.swift | 111 +++++++----- .../ViewControllers/OWSTableViewController.h | 2 + SignalMessaging/Views/GalleryRailView.swift | 4 +- SignalMessaging/Views/OWSButton.swift | 2 +- .../attachments/SignalAttachment.swift | 4 + 14 files changed, 277 insertions(+), 84 deletions(-) create mode 100644 Signal/Images.xcassets/album_add_more.imageset/Contents.json create mode 100644 Signal/Images.xcassets/album_add_more.imageset/album_add_more@1x.png create mode 100644 Signal/Images.xcassets/album_add_more.imageset/album_add_more@2x.png create mode 100644 Signal/Images.xcassets/album_add_more.imageset/album_add_more@3x.png diff --git a/Signal/Images.xcassets/album_add_more.imageset/Contents.json b/Signal/Images.xcassets/album_add_more.imageset/Contents.json new file mode 100644 index 000000000..b4dcc1357 --- /dev/null +++ b/Signal/Images.xcassets/album_add_more.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "album_add_more@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "album_add_more@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "album_add_more@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/album_add_more.imageset/album_add_more@1x.png b/Signal/Images.xcassets/album_add_more.imageset/album_add_more@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..bc89e5a4fafa18030dc9add6e0eaf086e4bb1abd GIT binary patch literal 1487 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fjKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z81_llpi<;HsXMd|v6mX?-X(Gs7c7{+3kj2o|M`E)8SrADBDCn&MGAmMZB3v?o0Sfko5ztjwCD1iG=jY@X z1s5bHr-B>?)`BF2t{QAjBra=^B#<cpt8_^$NwqUFFtpG$G}JXT4Kc8^GBmU@GPTi1Q-kCJkc@LtYGO%#QAmD% zjvd$+xgf5Bv7WgeR4=j$sAd~|P(DJ+SCC8#76s;7J1(HDuoA$Ii?@5~Hei9`;_2cT zVj;M7s$t$C0|~d}kkHf>0&^uK7)tf_vVP&1)5z_Rf56j%fw`bHPggi7l#|gZw##ag zYVCZML!x}0oHGKou^&MGja&w+#Zs&Yk>!qIgS$E4) z?y03#uh|&#kaxQ9MwNAqF<%R%wLY@F%blowv-sHksK+~X&#wHV_t8Xs=GvJt@A(hi z`fG6IfSg`(<%V)L#@V4Q{JBxbEmdKI;Vst0Qo@hBme*a literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/album_add_more.imageset/album_add_more@2x.png b/Signal/Images.xcassets/album_add_more.imageset/album_add_more@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b5102c6741cd9837af969c342c8088a72c3d832e GIT binary patch literal 1836 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!1|;QLq8Nb`V{wqX6T`Z5GB1G~&H|6fVg?3o zVGw3ym^DX&fq_LOGbExU!q>+tIX_n~F(p4KRj(qq0H~UQ!KT6r$jnVGNmQuF&B-ga zs<2f8tFQvHLBje<3ScEA*|tg%z5xo(`9-M;rg|oN21<5Z3JMA~MJZ`kK`w4k?LeNb zQbtKhft9{~d3m{Bxv^e;QM$gNrKP35fswwEkuFe$ZgFK^Nn(X=Ua>O75STeGsl~}f znFS@8`FRQ;a}$&DOG|8(lt3220mPjpnP~`{@`|C}0(wv%B%^PrXP}QwTWUon4s9SA zoZ3>7;l3&;Ey@A=DJ5AyH77MUHLs)?sLv3qb-=KNYeaEmMPdQOGH@V5{AL4kxm8eV zaehuICu1<^J5 z7i9u{nh0{2ogvf$WHEI0k=QIi7DUnj3VN%6%!<^U2$xJ?fP#HtVq&9@RRUe3bAC>K zQE)+Gaw^DSU@b^O=&HfiMB=grNdie@O0rdPX;M~datTsw0pkpu_MH;b^^t^a^s%b8 z0j2~i-~5!!v`Ux6l2kh*149d4LqlCd(+~qoD?>vo17jO~G&M*r0LeHPr6!i-7lq{K z=h%ULkqhD~80(qqnL_j;tAJ{@(Ff%tqV%a5c3e-w_5#ZbriGp^ zjv*18S7+M$i#SRgi%ja_^in>;&k!N;NIpwxYKG?mhAoY5_U*@>>}%khvh7x8rXX_W*S&P1^c=l8(@Ug{I76*_62$ieN2tUV8#nloRl>YjTc_pV>r-IdZA8#X7ozVAO#-Ohe?-^u&=d{Ixi zQezWZOISN!{$Ei3HQ=j{e8_4x{mAo&zGvQ@zQCaWyM+JAR+V^L1rygleR?lj`Z)|#TE24gu534*|COyf ze1*W{B@L!O`&5^)*vN6OjbODA6Z|lvp^T$n`tKyeS^JH&%qOOOTdI8EeZlW|6`>aw ze{3*(|`7;)D-_u8*D2jq(17fy$WWKxn7jbXVu60_D{sZ?edF0-!iJ0az6Hl#Dl;u ze63uS{bFoyt_obX-12l{)a|(9y`L&A{h97gxFu54>o+lbMei-KTM;?c1v~1LCS`Sp zdB>Jd6ieB1?BdSS>I0U!ck3MGFSyr5Z(SO#@sFXTa^n4;HOZ-&t;ucLK6VG C9idGC literal 0 HcmV?d00001 diff --git a/Signal/Images.xcassets/album_add_more.imageset/album_add_more@3x.png b/Signal/Images.xcassets/album_add_more.imageset/album_add_more@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..87e6bf6f2af7ebc42c45b5270fa6894491b01b5f GIT binary patch literal 2448 zcmZ`*2~<gL<|N(l1B+RF;xNxMcRV3 z=m(KP)u0rsfJI~yq!a`y^lL3BA|hg-Qh#Wv{3M{}v+%9;hI`K5cb|RkyKlYRkRU%D zO=C?M45qWg9}JV;y<{gsO?n-t!k$ZSO8hWCFIa7#>6CP!k>nrEhrzTAWTz7BOc@FW zQ?X=GqoAlj3W32%z|dnk`gAoM;>0<&DqN4=~>_k36Kmz0lg7jGy#sX+L z1Uf_lq5?zE-W(ni?SgT}I09r%G#X9h#U3Dpfxatr>4*dzgrFn>7Mqfif=O}0aCmW8 zoV&X_))9}z;~k_32YzZIL>D+D@-3Dpc{dNpmv27#ZvKc2~e6jn>R zX7YF6iyntn2oteuqKVQsfe^@vWgSQb=@65Qcf`3l;BXE&7pkK>0f!^Fx)ZUhFbWmO zGTuBU9pdn)91fc-cab0)qMb0?FfPknMZ^l3h?VUlg?+WWY2{Jrt|ep- z-27|)m`BJbdGp`1q|KMjqU_TcGLr$HJoFiD<+*a}UxK7@~C!cC_5+H>HtQ>=+=?fF^j2 zCKoaSFo+qqw{B^r8SBf90LjRz`&PQa@ApSPR~wTAR_(-H#g7H#X3Y)Y9$f zJe*^;&W@w)}x!aS<}(d}*k}4y9FWyA#(`(A_}olY9?)w6O{Q zRtqi(FHUCwkA1rDy?GQttX8w6ly_||g7lv@T$h+X{$euzNq~x(p`E%X+&}-?c(*#O zuOUcBC@9sv*-Wr%pzUlFQBFApnFk2dv*TD7s^ZG(xdA=f$NUUpiMI1y3GwVIPg~0v z$|*zR4Kc?yG>s7cH1bgyxSFmO=NZ~uKeQCFLF=<7)b@Q*x{g;&JpRJFJZj zHyV}WLN5-VtxNt)?bo2!3}^1xbX%6^tZ7QXq%8L$u*=yqBUw82M zeBoUGUp^8m>Vgf5p__fm*A&T{16-p<0Be9&ji>6jp&xXi3RFDnA8Z6@uLtZzfm=rp z#ejc{hp(kgwO_G{uN2;k53Tm{Ws+M%wg9vqTctZQUEN#tko>^F6GPU>fzx#5lhaNh z*lfpi6B!{5@RW3J^Q%x^1j(NNO1I!yz+gPYeRkzpPjVTs9~1;LvW0IQy9@LNf;-I4 zT0r8UZ}RdkoN1gQ+E6Pm0pseQJcr=6vrpfK>&>ax zyijI0#NLVVt`fC7zwDki#pU$|76(l@Ss(VvJaXNdWCzm!YM6b&f z35#A+%WUjYGrYRO>e{Lx>CSTD?rEdHV=XEV~bV^lJ7Tawnx$~8mUbI16_7CqH_ zT#bs+`|<&=^HLuIuVzkk}gs9oW z>fik^Zfsj^HI<&tNbLKPqBA74!{;ul>iuJczfTeIgb9 QQudp)!zT!=^@_>-AKHkiZU6uP literal 0 HcmV?d00001 diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 68df293ab..70e0248c8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2676,7 +2676,7 @@ typedef enum : NSUInteger { - (void)imagePicker:(OWSImagePickerGridController *)imagePicker didPickImageAttachments:(NSArray *)attachments { - [self tryToSendAttachmentsIfApproved:attachments]; + [self tryToSendAttachmentsIfApproved:attachments skipApprovalDialog:YES]; } /* diff --git a/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift b/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift index 1ed56ff00..c753b452c 100644 --- a/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift +++ b/Signal/src/ViewControllers/PhotoLibrary/ImagePickerController.swift @@ -12,7 +12,7 @@ protocol ImagePickerControllerDelegate { } @objc(OWSImagePickerGridController) -class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate { +class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate, PhotoCollectionPickerDelegate, AttachmentApprovalViewControllerDelegate { @objc weak var delegate: ImagePickerControllerDelegate? @@ -26,6 +26,11 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat private let titleLabel = UILabel() + private var selectedIds = Set() + + // This variable should only be accessed on the main thread. + private var assetIdToCommentMap = [String: String]() + init() { collectionViewFlowLayout = type(of: self).buildLayout() photoCollection = library.defaultPhotoCollection() @@ -38,7 +43,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat fatalError("init(coder:) has not been implemented") } - // MARK: View Lifecycle + // MARK: - View Lifecycle override func viewDidLoad() { super.viewDidLoad() @@ -52,17 +57,27 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier) - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, - target: self, - action: #selector(didPressCancel)) + view.backgroundColor = .ows_gray95 + + if let navBar = self.navigationController?.navigationBar as? OWSNavigationBar { + navBar.makeClear() + } else { + owsFailDebug("Invalid nav bar.") + } + + let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(didPressCancel)) + cancelButton.tintColor = .ows_gray05 + navigationItem.leftBarButtonItem = cancelButton if #available(iOS 11, *) { titleLabel.text = photoCollection.localizedTitle() - titleLabel.textColor = Theme.primaryColor + titleLabel.textColor = .ows_gray05 titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight() let titleIconView = UIImageView() - titleIconView.tintColor = Theme.primaryColor + titleIconView.tintColor = .ows_gray05 titleIconView.image = UIImage(named: "navbar_disclosure_down")?.withRenderingMode(.alwaysTemplate) let titleView = UIStackView(arrangedSubviews: [titleLabel, titleIconView]) @@ -81,7 +96,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat updateSelectButton() } - collectionView.backgroundColor = Theme.backgroundColor + collectionView.backgroundColor = .ows_gray95 } override func viewWillLayoutSubviews() { @@ -96,16 +111,38 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let scale = UIScreen.main.scale let cellSize = collectionViewFlowLayout.itemSize photoMediaSize.thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale) + + reloadDataAndRestoreSelection() } - // MARK: Actions + private func reloadDataAndRestoreSelection() { + guard let collectionView = collectionView else { + owsFailDebug("Missing collectionView.") + return + } + + collectionView.reloadData() + collectionView.layoutIfNeeded() + + let count = photoCollectionContents.count + for index in 0.. UICollectionViewFlowLayout { @@ -146,7 +183,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } } - // MARK: Batch Selection + // MARK: - Batch Selection lazy var doneButton: UIBarButtonItem = { return UIBarButtonItem(barButtonSystemItem: .done, @@ -184,12 +221,39 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } let assets: [PHAsset] = indexPaths.compactMap { return photoCollectionContents.asset(at: $0.row) } - let promises = assets.map { return photoCollectionContents.outgoingAttachment(for: $0) } - when(fulfilled: promises).map { attachments in - self.dismiss(animated: true) { - self.delegate?.imagePicker(self, didPickImageAttachments: attachments) + complete(withAssets: assets) + } + + func complete(withAssets assets: [PHAsset]) { + let attachmentPromises: [Promise] = assets.map({ + return photoCollectionContents.outgoingAttachment(for: $0) + }) + when(fulfilled: attachmentPromises) + .map { attachments in + self.didComplete(withAttachments: attachments) + }.retainUntilComplete() + } + + private func didComplete(withAttachments attachments: [SignalAttachment]) { + AssertIsOnMainThread() + + // If we re-enter image picking, do so in batch mode. + isInBatchSelectMode = true + + for attachment in attachments { + guard let assetId = attachment.sourceId else { + owsFailDebug("Attachment is missing asset id.") + continue } - }.retainUntilComplete() + // Link the attachment with its asset to ensure caption continuity. + attachment.sourceId = assetId + // Restore any existing caption for this attachment. + attachment.captionText = assetIdToCommentMap[assetId] + } + + let vc = AttachmentApprovalViewController(mode: .sharedNavigation, attachments: attachments) + vc.approvalDelegate = self + navigationController?.pushViewController(vc, animated: true) } func updateDoneButton() { @@ -206,7 +270,9 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat } func updateSelectButton() { - navigationItem.rightBarButtonItem = isInBatchSelectMode ? doneButton : selectButton + let button = isInBatchSelectMode ? doneButton : selectButton + button.tintColor = .ows_gray05 + navigationItem.rightBarButtonItem = button } @objc @@ -234,13 +300,17 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)} } - // MARK: PhotoLibraryDelegate + // MARK: - PhotoLibraryDelegate func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) { - collectionView?.reloadData() + // We only want to let users select assets + // from a single collection. + selectedIds.removeAll() + + reloadDataAndRestoreSelection() } - // MARK: PhotoCollectionPickerDelegate + // MARK: - PhotoCollectionPickerDelegate func photoCollectionPicker(_ photoCollectionPicker: PhotoCollectionPickerController, didPickCollection collection: PhotoCollection) { photoCollection = collection @@ -252,7 +322,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat navigationItem.title = photoCollection.localizedTitle() } - collectionView?.reloadData() + reloadDataAndRestoreSelection() } // MARK: - Event Handlers @@ -264,30 +334,33 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat let view = PhotoCollectionPickerController(library: library, previousPhotoCollection: photoCollection, collectionDelegate: self) - let nav = UINavigationController(rootViewController: view) + let nav = OWSNavigationController(rootViewController: view) self.present(nav, animated: true, completion: nil) } - // MARK: UICollectionView + // MARK: - UICollectionView override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + let asset = photoCollectionContents.asset(at: indexPath.item) + let assetId = asset.localIdentifier + selectedIds.insert(assetId) + if isInBatchSelectMode { updateDoneButton() } else { let asset = photoCollectionContents.asset(at: indexPath.row) - firstly { - photoCollectionContents.outgoingAttachment(for: asset) - }.map { attachment in - self.dismiss(animated: true) { - self.delegate?.imagePicker(self, didPickImageAttachments: [attachment]) - } - }.retainUntilComplete() + complete(withAssets: [asset]) } } public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { Logger.debug("") + let asset = photoCollectionContents.asset(at: indexPath.item) + let assetId = asset.localIdentifier + selectedIds.remove(assetId) + if isInBatchSelectMode { updateDoneButton() } @@ -301,10 +374,44 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoGridViewCell.reuseIdentifier, for: indexPath) as? PhotoGridViewCell else { owsFail("cell was unexpectedly nil") } - + cell.loadingColor = UIColor(white: 0.2, alpha: 1) let assetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) cell.configure(item: assetItem) + + let assetId = assetItem.asset.localIdentifier + let isSelected = selectedIds.contains(assetId) + cell.isSelected = isSelected + return cell } + // MARK: - AttachmentApprovalViewControllerDelegate + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment]) { + self.dismiss(animated: true) { + self.delegate?.imagePicker(self, didPickImageAttachments: attachments) + } + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) { + navigationController?.popToViewController(self, animated: true) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, addMoreToAttachments attachments: [SignalAttachment]) { + navigationController?.popToViewController(self, animated: true) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) { + AssertIsOnMainThread() + + guard let assetId = attachment.sourceId else { + owsFailDebug("Attachment missing source id.") + return + } + guard let captionText = attachment.captionText, captionText.count > 0 else { + assetIdToCommentMap.removeValue(forKey: assetId) + return + } + assetIdToCommentMap[assetId] = captionText + } } diff --git a/Signal/src/ViewControllers/PhotoLibrary/PhotoCollectionPickerController.swift b/Signal/src/ViewControllers/PhotoLibrary/PhotoCollectionPickerController.swift index 4c5c4a981..6f2748122 100644 --- a/Signal/src/ViewControllers/PhotoLibrary/PhotoCollectionPickerController.swift +++ b/Signal/src/ViewControllers/PhotoLibrary/PhotoCollectionPickerController.swift @@ -37,14 +37,22 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .ows_gray95 + + if let navBar = self.navigationController?.navigationBar as? OWSNavigationBar { + navBar.makeClear() + } else { + owsFailDebug("Invalid nav bar.") + } + if #available(iOS 11, *) { let titleLabel = UILabel() titleLabel.text = previousPhotoCollection.localizedTitle() - titleLabel.textColor = Theme.primaryColor + titleLabel.textColor = .ows_gray05 titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight() let titleIconView = UIImageView() - titleIconView.tintColor = Theme.primaryColor + titleIconView.tintColor = .ows_gray05 titleIconView.image = UIImage(named: "navbar_disclosure_up")?.withRenderingMode(.alwaysTemplate) let titleView = UIStackView(arrangedSubviews: [titleLabel, titleIconView]) @@ -60,9 +68,11 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg library.add(delegate: self) - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, - target: self, - action: #selector(didPressCancel)) + let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, + target: self, + action: #selector(didPressCancel)) + cancelButton.tintColor = .ows_gray05 + navigationItem.leftBarButtonItem = cancelButton updateContents() } @@ -75,6 +85,10 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg section.add(OWSTableItem.init(customCellBlock: { () -> UITableViewCell in let cell = OWSTableItem.newCell() + cell.backgroundColor = .ows_gray95 + cell.contentView.backgroundColor = .ows_gray95 + cell.selectedBackgroundView?.backgroundColor = UIColor(white: 0.2, alpha: 1) + let imageView = UIImageView() let kImageSize = 50 imageView.autoSetDimensions(to: CGSize(width: kImageSize, height: kImageSize)) @@ -97,7 +111,7 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg let titleLabel = UILabel() titleLabel.text = collection.localizedTitle() titleLabel.font = UIFont.ows_dynamicTypeBody - titleLabel.textColor = Theme.primaryColor + titleLabel.textColor = .ows_gray05 let stackView = UIStackView(arrangedSubviews: [imageView, titleLabel]) stackView.axis = .horizontal @@ -120,6 +134,12 @@ class PhotoCollectionPickerController: OWSTableViewController, PhotoLibraryDeleg self.contents = contents } + @objc + public override func applyTheme() { + view.backgroundColor = .ows_gray95 + tableView.backgroundColor = .ows_gray95 + } + // MARK: Actions @objc diff --git a/Signal/src/ViewControllers/PhotoLibrary/PhotoLibrary.swift b/Signal/src/ViewControllers/PhotoLibrary/PhotoLibrary.swift index c9b62fc3b..9fd41b896 100644 --- a/Signal/src/ViewControllers/PhotoLibrary/PhotoLibrary.swift +++ b/Signal/src/ViewControllers/PhotoLibrary/PhotoLibrary.swift @@ -150,11 +150,15 @@ class PhotoCollectionContents { switch asset.mediaType { case .image: return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in - return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium) + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium) + attachment.sourceId = asset.localIdentifier + return attachment } case .video: return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in - return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI) + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI) + attachment.sourceId = asset.localIdentifier + return attachment } default: return Promise(error: PhotoLibraryError.unsupportedMediaType) diff --git a/Signal/src/views/PhotoGridViewCell.swift b/Signal/src/views/PhotoGridViewCell.swift index 2114a94dc..fdc486914 100644 --- a/Signal/src/views/PhotoGridViewCell.swift +++ b/Signal/src/views/PhotoGridViewCell.swift @@ -29,6 +29,8 @@ public class PhotoGridViewCell: UICollectionViewCell { private static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif") private static let selectedBadgeImage = #imageLiteral(resourceName: "selected_blue_circle") + public var loadingColor = Theme.offBackgroundColor + override public var isSelected: Bool { didSet { self.selectedBadgeView.isHidden = !self.isSelected @@ -99,7 +101,7 @@ public class PhotoGridViewCell: UICollectionViewCell { get { return imageView.image } set { imageView.image = newValue - imageView.backgroundColor = newValue == nil ? Theme.offBackgroundColor : .clear + imageView.backgroundColor = newValue == nil ? loadingColor : .clear } } diff --git a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift index d41c5e00d..b748ebeb8 100644 --- a/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift +++ b/SignalMessaging/ViewControllers/AttachmentApprovalViewController.swift @@ -11,6 +11,8 @@ import PromiseKit public protocol AttachmentApprovalViewControllerDelegate: class { func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment]) func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didCancelAttachments attachments: [SignalAttachment]) + @objc optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, addMoreToAttachments attachments: [SignalAttachment]) + @objc optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, changedCaptionOfAttachment attachment: SignalAttachment) } class AttachmentItemCollection { @@ -91,16 +93,24 @@ class SignalAttachmentItem: Hashable { } } +@objc +public enum AttachmentApprovalViewControllerMode: UInt { + case modal + case sharedNavigation +} + @objc public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, CaptioningToolbarDelegate { - // MARK: Properties + // MARK: - Properties - weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? + private let mode: AttachmentApprovalViewControllerMode + + public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? private(set) var captioningToolbar: CaptioningToolbar! - // MARK: Initializers + // MARK: - Initializers @available(*, unavailable, message:"use attachment: constructor instead.") required public init?(coder aDecoder: NSCoder) { @@ -110,8 +120,10 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC let kSpacingBetweenItems: CGFloat = 20 @objc - required public init(attachments: [SignalAttachment]) { + required public init(mode: AttachmentApprovalViewControllerMode, + attachments: [SignalAttachment]) { assert(attachments.count > 0) + self.mode = mode let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} self.attachmentItemCollection = AttachmentItemCollection(attachmentItems: attachmentItems) super.init(transitionStyle: .scroll, @@ -123,7 +135,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC @objc public class func wrappedInNavController(attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate) -> OWSNavigationController { - let vc = AttachmentApprovalViewController(attachments: attachments) + let vc = AttachmentApprovalViewController(mode: .modal, attachments: attachments) vc.approvalDelegate = approvalDelegate let navController = OWSNavigationController(rootViewController: vc) @@ -136,7 +148,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return navController } - // MARK: View Lifecycle + // MARK: - View Lifecycle let galleryRailView = GalleryRailView() let railContainerView = UIView() @@ -171,7 +183,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // Bottom Toolbar - let captioningToolbar = CaptioningToolbar() + let isAddMoreVisible = mode == .sharedNavigation + let captioningToolbar = CaptioningToolbar(isAddMoreVisible: isAddMoreVisible) captioningToolbar.captioningToolbarDelegate = self self.captioningToolbar = captioningToolbar @@ -179,9 +192,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC self.navigationItem.title = nil - let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(cancelPressed)) - cancelButton.tintColor = .white - self.navigationItem.leftBarButtonItem = cancelButton + if mode != .sharedNavigation { + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, + target: self, action: #selector(cancelPressed)) + cancelButton.tintColor = .white + self.navigationItem.leftBarButtonItem = cancelButton + } guard let firstItem = attachmentItems.first else { owsFailDebug("firstItem was unexpectedly nil") @@ -189,6 +205,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } self.setCurrentItem(firstItem, direction: .forward, animated: false) + + captioningToolbar.captionText = currentViewController.attachment.captionText } override public func viewWillAppear(_ animated: Bool) { @@ -263,8 +281,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } let button = OWSButton { [weak self] in - guard let self = self else { return } - self.remove(attachmentItem: attachmentItem) + guard let strongSelf = self else { return } + strongSelf.remove(attachmentItem: attachmentItem) } button.setImage(#imageLiteral(resourceName: "ic_small_x"), for: .normal) @@ -300,21 +318,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC pagerScrollView.isScrollEnabled = attachmentItems.count > 1 } - private func makeClearToolbar() -> UIToolbar { - let toolbar = UIToolbar() - - toolbar.backgroundColor = UIColor.clear - - // Making a toolbar transparent requires setting an empty uiimage - toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) - - // hide 1px top-border - toolbar.clipsToBounds = true - - return toolbar - } - - // MARK: UIPageViewControllerDelegate + // MARK: - UIPageViewControllerDelegate public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { Logger.debug("") @@ -356,7 +360,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } } - // MARK: UIPageViewControllerDataSource + // MARK: - UIPageViewControllerDataSource public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let currentViewController = viewController as? AttachmentPrepViewController else { @@ -493,7 +497,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC self.approvalDelegate?.attachmentApproval(self, didCancelAttachments: attachments) } - // MARK: CaptioningToolbarDelegate + // MARK: - CaptioningToolbarDelegate var currentPageController: AttachmentPrepViewController { return viewControllers!.first as! AttachmentPrepViewController @@ -520,6 +524,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC func captioningToolbar(_ captioningToolbar: CaptioningToolbar, textViewDidChange textView: UITextView) { currentItem.attachment.captionText = textView.text + + self.approvalDelegate?.attachmentApproval?(self, changedCaptionOfAttachment: currentItem.attachment) + } + + func captioningToolbarDidAddMore(_ captioningToolbar: CaptioningToolbar) { + self.approvalDelegate?.attachmentApproval?(self, addMoreToAttachments: attachments) } } @@ -558,7 +568,7 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate { return } - let direction: NavigationDirection = currentIndex < targetIndex ? .forward : .reverse + let direction: UIPageViewControllerNavigationDirection = currentIndex < targetIndex ? .forward : .reverse self.setCurrentItem(targetItem, direction: direction, animated: true) } @@ -573,7 +583,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD case fullsize, compact } - // MARK: Properties + // MARK: - Properties let attachmentItem: SignalAttachmentItem var attachment: SignalAttachment { @@ -587,7 +597,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD private(set) var contentContainer: UIView! private(set) var playVideoButton: UIView? - // MARK: Initializers + // MARK: - Initializers init(attachmentItem: SignalAttachmentItem) { self.attachmentItem = attachmentItem @@ -599,7 +609,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD fatalError("init(coder:) has not been implemented") } - // MARK: View Lifecycle + // MARK: - View Lifecycle override public func loadView() { self.view = UIView() @@ -713,7 +723,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD ensureAttachmentViewScale(animated: false) } - // MARK: + // MARK: - @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { assert(self.videoPlayer != nil) @@ -727,7 +737,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD self.playVideo() } - // MARK: Video + // MARK: - Video private func playVideo() { Logger.info("") @@ -804,7 +814,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } } - // MARK: Helpers + // MARK: - Helpers var isZoomable: Bool { return attachment.isImage || attachment.isVideo @@ -939,11 +949,13 @@ protocol CaptioningToolbarDelegate: class { func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) func captioningToolbar(_ captioningToolbar: CaptioningToolbar, textViewDidChange: UITextView) + func captioningToolbarDidAddMore(_ captioningToolbar: CaptioningToolbar) } class CaptioningToolbar: UIView, UITextViewDelegate { weak var captioningToolbarDelegate: CaptioningToolbarDelegate? + private let addMoreButton: UIButton private let sendButton: UIButton private let textView: UITextView @@ -984,9 +996,10 @@ class CaptioningToolbar: UIView, UITextViewDelegate { } } - // MARK: Initializers + // MARK: - Initializers - init() { + init(isAddMoreVisible: Bool) { + self.addMoreButton = UIButton(type: .custom) self.sendButton = UIButton(type: .system) self.textView = MessageTextView() self.textViewHeight = kMinTextViewHeight @@ -1012,6 +1025,9 @@ class CaptioningToolbar: UIView, UITextViewDelegate { textView.textContainerInset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7) textView.scrollIndicatorInsets = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 3) + addMoreButton.setImage(UIImage(named: "album_add_more"), for: .normal) + addMoreButton.addTarget(self, action: #selector(didTapAddMore), for: .touchUpInside) + let sendTitle = NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON", comment: "Label for 'send' button in the 'attachment approval' dialog.") sendButton.setTitle(sendTitle, for: .normal) sendButton.addTarget(self, action: #selector(didTapSend), for: .touchUpInside) @@ -1039,6 +1055,9 @@ class CaptioningToolbar: UIView, UITextViewDelegate { contentView.addSubview(sendButton) contentView.addSubview(textView) contentView.addSubview(lengthLimitLabel) + if isAddMoreVisible { + contentView.addSubview(addMoreButton) + } addSubview(contentView) contentView.autoPinEdgesToSuperviewEdges() @@ -1060,9 +1079,17 @@ class CaptioningToolbar: UIView, UITextViewDelegate { // So it doesn't work as expected with RTL layouts when we explicitly want something // to be on the right side for both RTL and LTR layouts, like with the send button. // I believe this is a bug in PureLayout. Filed here: https://github.com/PureLayout/PureLayout/issues/209 - textView.autoPinEdge(toSuperviewMargin: .left) textView.autoPinEdge(toSuperviewMargin: .top) textView.autoPinEdge(toSuperviewMargin: .bottom) + if isAddMoreVisible { + addMoreButton.autoPinEdge(toSuperviewMargin: .left) + textView.autoPinEdge(.left, to: .right, of: addMoreButton, withOffset: kToolbarMargin) + addMoreButton.autoAlignAxis(.horizontal, toSameAxisOf: sendButton) + addMoreButton.setContentHuggingHigh() + addMoreButton.setCompressionResistanceHigh() + } else { + textView.autoPinEdge(toSuperviewMargin: .left) + } sendButton.autoPinEdge(.left, to: .right, of: textView, withOffset: kToolbarMargin) sendButton.autoPinEdge(.bottom, to: .bottom, of: textView, withOffset: -3) @@ -1087,12 +1114,16 @@ class CaptioningToolbar: UIView, UITextViewDelegate { notImplemented() } - // MARK: + // MARK: - @objc func didTapSend() { self.captioningToolbarDelegate?.captioningToolbarDidTapSend(self) } + @objc func didTapAddMore() { + self.captioningToolbarDelegate?.captioningToolbarDidAddMore(self) + } + // MARK: - UITextViewDelegate public func textViewDidChange(_ textView: UITextView) { diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.h b/SignalMessaging/ViewControllers/OWSTableViewController.h index 471b6c4eb..0185f88b2 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.h +++ b/SignalMessaging/ViewControllers/OWSTableViewController.h @@ -139,6 +139,8 @@ typedef UITableViewCell *_Nonnull (^OWSTableCustomCellBlock)(void); - (void)presentFromViewController:(UIViewController *)fromViewController; +- (void)applyTheme; + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/Views/GalleryRailView.swift b/SignalMessaging/Views/GalleryRailView.swift index 85c1bcc86..42c72c854 100644 --- a/SignalMessaging/Views/GalleryRailView.swift +++ b/SignalMessaging/Views/GalleryRailView.swift @@ -33,7 +33,7 @@ public class GalleryRailCellView: UIView { addGestureRecognizer(tapGesture) } - required init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -112,7 +112,7 @@ public class GalleryRailView: UIView, GalleryRailCellViewDelegate { scrollView.autoPinEdgesToSuperviewMargins() } - required init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/SignalMessaging/Views/OWSButton.swift b/SignalMessaging/Views/OWSButton.swift index 4ecae71bd..9d51bf99d 100644 --- a/SignalMessaging/Views/OWSButton.swift +++ b/SignalMessaging/Views/OWSButton.swift @@ -19,7 +19,7 @@ public class OWSButton: UIButton { self.addTarget(self, action: #selector(didTap), for: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index d4ad2c049..972962f03 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -160,6 +160,10 @@ public class SignalAttachment: NSObject { @objc public let dataUTI: String + // Can be used by views to link this SignalAttachment with an arbitrary source. + @objc + public var sourceId: String? + var error: SignalAttachmentError? { didSet { AssertIsOnMainThread()