Custom photo picker, respects theme/call banner

- share GridViewCell
- Multiple image selection, with feature flag, cant currently approve multiple
This commit is contained in:
Michael Kirk 2018-11-02 18:08:19 -06:00
parent c8ac66ff8f
commit 4c5d46e8f8
7 changed files with 637 additions and 187 deletions

View File

@ -430,6 +430,8 @@
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; };
4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */; };
4C1885D0218D0EA800B67051 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885CF218D0EA800B67051 /* ImagePickerController.swift */; };
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */; };
4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; };
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; };
4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23A5F1215C4ADE00534937 /* SheetViewController.swift */; };
@ -1125,6 +1127,8 @@
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HapticFeedback.swift; path = UserInterface/HapticFeedback.swift; sourceTree = "<group>"; };
4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatusView.swift; sourceTree = "<group>"; };
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = "<group>"; };
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = "<group>"; };
4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearchViewController.swift; sourceTree = "<group>"; };
4C23A5F1215C4ADE00534937 /* SheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetViewController.swift; sourceTree = "<group>"; };
4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = "<group>"; };
@ -1762,6 +1766,7 @@
34D1F0BE1F8EC1760066283D /* Utils */,
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */,
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -2248,6 +2253,7 @@
450D19121F85236600970622 /* RemoteVideoView.m */,
4CA5F792211E1F06008C2708 /* Toast.swift */,
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
);
name = Views;
path = views;
@ -3306,6 +3312,7 @@
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */,
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */,
@ -3423,6 +3430,7 @@
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */,
4C1885D0218D0EA800B67051 /* ImagePickerController.swift in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,

View File

@ -125,6 +125,7 @@ typedef enum : NSUInteger {
UIDocumentMenuDelegate,
UIDocumentPickerDelegate,
UIImagePickerControllerDelegate,
OWSImagePickerControllerDelegate,
UINavigationControllerDelegate,
UITextViewDelegate,
ConversationCollectionViewDelegate,
@ -2650,14 +2651,14 @@ typedef enum : NSUInteger {
OWSLogWarn(@"Media Library permission denied.");
return;
}
UIImagePickerController *picker = [UIImagePickerController new];
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
OWSImagePickerGridController *picker = [OWSImagePickerGridController new];
picker.delegate = self;
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
OWSNavigationController *pickerModal = [[OWSNavigationController alloc] initWithRootViewController:picker];
[self dismissKeyBoard];
[self presentViewController:picker animated:YES completion:nil];
[self presentViewController:pickerModal animated:YES completion:nil];
}];
}
@ -2677,6 +2678,14 @@ typedef enum : NSUInteger {
self.view.frame = frame;
}
- (void)imagePicker:(OWSImagePickerGridController *)imagePicker
didPickImageAttachments:(NSArray<SignalAttachment *> *)attachments
{
// TODO support approving multiple attachments.
SignalAttachment *firstAttachment = attachments.firstObject;
[self tryToSendAttachmentIfApproved:firstAttachment];
}
/*
* Fetching data from UIImagePickerController
*/
@ -2718,7 +2727,7 @@ typedef enum : NSUInteger {
NSString *mediaType = info[UIImagePickerControllerMediaType];
if ([mediaType isEqualToString:(__bridge NSString *)kUTTypeMovie]) {
// Video picked from library or captured with camera
// Video captured with camera
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
[self dismissViewControllerAnimated:YES
@ -2756,57 +2765,8 @@ typedef enum : NSUInteger {
}
}];
} else {
// Non-Video image picked from library
// To avoid re-encoding GIF and PNG's as JPEG we have to get the raw data of
// the selected item vs. using the UIImagePickerControllerOriginalImage
NSURL *assetURL = info[UIImagePickerControllerReferenceURL];
PHAsset *asset = [[PHAsset fetchAssetsWithALAssetURLs:@[ assetURL ] options:nil] lastObject];
if (!asset) {
return failedToPickAttachment(nil);
}
// Images chosen from the "attach document" UI should be sent as originals;
// images chosen from the "attach media" UI should be resized to "medium" size;
TSImageQuality imageQuality = (self.isPickingMediaAsDocument ? TSImageQualityOriginal : TSImageQualityMedium);
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = YES; // We're only fetching one asset.
options.networkAccessAllowed = YES; // iCloud OK
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; // Don't need quick/dirty version
[[PHImageManager defaultManager]
requestImageDataForAsset:asset
options:options
resultHandler:^(NSData *_Nullable imageData,
NSString *_Nullable dataUTI,
UIImageOrientation orientation,
NSDictionary *_Nullable assetInfo) {
NSError *assetFetchingError = assetInfo[PHImageErrorKey];
if (assetFetchingError || !imageData) {
return failedToPickAttachment(assetFetchingError);
}
OWSAssertIsOnMainThread();
DataSource *_Nullable dataSource =
[DataSourceValue dataSourceWithData:imageData utiType:dataUTI];
[dataSource setSourceFilename:filename];
SignalAttachment *attachment = [SignalAttachment attachmentWithDataSource:dataSource
dataUTI:dataUTI
imageQuality:imageQuality];
[self dismissViewControllerAnimated:YES
completion:^{
OWSAssertIsOnMainThread();
if (!attachment || [attachment hasError]) {
OWSLogWarn(@"Invalid attachment: %@.",
attachment ? [attachment errorName] : @"Missing data");
[self showErrorAlertForAttachment:attachment];
failedToPickAttachment(nil);
} else {
[self tryToSendAttachmentIfApproved:attachment];
}
}];
}];
OWSFailDebug(
@"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not supported. ");
}
}
@ -2878,9 +2838,8 @@ typedef enum : NSUInteger {
VideoCompressionResult *compressionResult =
[SignalAttachment compressVideoAsMp4WithDataSource:dataSource
dataUTI:(NSString *)kUTTypeMPEG4];
[compressionResult.attachmentPromise retainUntilComplete];
compressionResult.attachmentPromise.then(^(SignalAttachment *attachment) {
[compressionResult.attachmentPromise.then(^(SignalAttachment *attachment) {
OWSAssertIsOnMainThread();
OWSAssertDebug([attachment isKindOfClass:[SignalAttachment class]]);
@ -2897,7 +2856,7 @@ typedef enum : NSUInteger {
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog];
}
}];
});
}) retainUntilComplete];
}];
}

View File

@ -0,0 +1,433 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import Photos
import PromiseKit
@objc(OWSImagePickerControllerDelegate)
protocol ImagePickerControllerDelegate {
func imagePicker(_ imagePicker: ImagePickerGridController, didPickImageAttachments attachments: [SignalAttachment])
}
@objc(OWSImagePickerGridController)
class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegate {
@objc
weak var delegate: ImagePickerControllerDelegate?
private let library: PhotoLibrary = PhotoLibrary()
private let libraryAlbum: PhotoLibraryAlbum
var availableWidth: CGFloat = 0
var collectionViewFlowLayout: UICollectionViewFlowLayout
init() {
collectionViewFlowLayout = type(of: self).buildLayout()
libraryAlbum = library.albumForAllPhotos()
super.init(collectionViewLayout: collectionViewFlowLayout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.title = libraryAlbum.localizedTitle
library.delegate = self
guard let collectionView = collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier)
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(didPressCancel))
let featureFlag_isMultiselectEnabled = true
if featureFlag_isMultiselectEnabled {
updateSelectButton()
}
collectionView.backgroundColor = Theme.backgroundColor
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateLayout()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Determine the size of the thumbnails to request
let scale = UIScreen.main.scale
let cellSize = collectionViewFlowLayout.itemSize
libraryAlbum.thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
}
// MARK: Actions
@objc
func didPressCancel(sender: UIBarButtonItem) {
self.dismiss(animated: true)
}
// MARK: Layout
static let kInterItemSpacing: CGFloat = 2
private class func buildLayout() -> UICollectionViewFlowLayout {
let layout = UICollectionViewFlowLayout()
if #available(iOS 11, *) {
layout.sectionInsetReference = .fromSafeArea
}
layout.minimumInteritemSpacing = kInterItemSpacing
layout.minimumLineSpacing = kInterItemSpacing
layout.sectionHeadersPinToVisibleBounds = true
return layout
}
func updateLayout() {
let containerWidth: CGFloat
if #available(iOS 11.0, *) {
containerWidth = self.view.safeAreaLayoutGuide.layoutFrame.size.width
} else {
containerWidth = self.view.frame.size.width
}
let kItemsPerPortraitRow = 4
let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
let approxItemWidth = screenWidth / CGFloat(kItemsPerPortraitRow)
let itemCount = round(containerWidth / approxItemWidth)
let spaceWidth = (itemCount + 1) * type(of: self).kInterItemSpacing
let availableWidth = containerWidth - spaceWidth
let itemWidth = floor(availableWidth / CGFloat(itemCount))
let newItemSize = CGSize(width: itemWidth, height: itemWidth)
if (newItemSize != collectionViewFlowLayout.itemSize) {
collectionViewFlowLayout.itemSize = newItemSize
collectionViewFlowLayout.invalidateLayout()
}
}
// MARK: Batch Selection
lazy var doneButton: UIBarButtonItem = {
return UIBarButtonItem(barButtonSystemItem: .done,
target: self,
action: #selector(didPressDone))
}()
lazy var selectButton: UIBarButtonItem = {
return UIBarButtonItem(title: NSLocalizedString("BUTTON_SELECT", comment: "Button text to enable batch selection mode"),
style: .plain,
target: self,
action: #selector(didTapSelect))
}()
var isInBatchSelectMode = false {
didSet {
collectionView!.allowsMultipleSelection = isInBatchSelectMode
updateSelectButton()
updateDoneButton()
}
}
@objc
func didPressDone(_ sender: Any) {
Logger.debug("")
guard let collectionView = self.collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
guard let indexPaths = collectionView.indexPathsForSelectedItems else {
owsFailDebug("indexPaths was unexpectedly nil")
return
}
let assets: [PHAsset] = indexPaths.compactMap { return self.libraryAlbum.asset(at: $0.row) }
let promises = assets.map { return libraryAlbum.outgoingAttachment(for: $0) }
when(fulfilled: promises).map { attachments in
self.dismiss(animated: true) {
self.delegate?.imagePicker(self, didPickImageAttachments: attachments)
}
}.retainUntilComplete()
}
func updateDoneButton() {
guard let collectionView = self.collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
if let count = collectionView.indexPathsForSelectedItems?.count, count > 0 {
self.doneButton.isEnabled = true
} else {
self.doneButton.isEnabled = false
}
}
func updateSelectButton() {
navigationItem.rightBarButtonItem = isInBatchSelectMode ? doneButton : selectButton
}
@objc
func didTapSelect(_ sender: Any) {
isInBatchSelectMode = true
// disabled until at least one item is selected
self.doneButton.isEnabled = false
}
@objc
func didCancelSelect(_ sender: Any) {
endSelectMode()
}
func endSelectMode() {
isInBatchSelectMode = false
guard let collectionView = self.collectionView else {
owsFailDebug("collectionView was unexpectedly nil")
return
}
// deselect any selected
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)}
}
// MARK: PhotoLibraryDelegate
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
collectionView?.reloadData()
}
// MARK: UICollectionView
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if isInBatchSelectMode {
updateDoneButton()
} else {
let asset = libraryAlbum.asset(at: indexPath.row)
firstly {
libraryAlbum.outgoingAttachment(for: asset)
}.map { attachment in
self.dismiss(animated: true) {
self.delegate?.imagePicker(self, didPickImageAttachments: [attachment])
}
}.retainUntilComplete()
}
}
public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
Logger.debug("")
if isInBatchSelectMode {
updateDoneButton()
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return libraryAlbum.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoGridViewCell.reuseIdentifier, for: indexPath) as? PhotoGridViewCell else {
owsFail("cell was unexpectedly nil")
}
let mediaItem = libraryAlbum.mediaItem(at: indexPath.item)
cell.configure(item: mediaItem)
return cell
}
}
protocol PhotoLibraryDelegate: class {
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary)
}
class ImagePickerGridItem: PhotoGridItem {
let asset: PHAsset
let album: PhotoLibraryAlbum
init(asset: PHAsset, album: PhotoLibraryAlbum) {
self.asset = asset
self.album = album
}
// MARK: PhotoGridItem
var type: PhotoGridItemType {
if asset.mediaType == .video {
return .video
}
// TODO show GIF badge?
return .photo
}
func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? {
album.requestThumbnail(for: self.asset) { image, _ in
completion(image)
}
return nil
}
}
class PhotoLibraryAlbum {
let fetchResult: PHFetchResult<PHAsset>
let localizedTitle: String?
var thumbnailSize: CGSize = .zero
enum PhotoLibraryError: Error {
case assertionError(_ description: String)
case unsupportedMediaType
}
init(fetchResult: PHFetchResult<PHAsset>, localizedTitle: String?) {
self.fetchResult = fetchResult
self.localizedTitle = localizedTitle
}
var count: Int {
return fetchResult.count
}
private let imageManager = PHCachingImageManager()
func asset(at index: Int) -> PHAsset {
return fetchResult.object(at: index)
}
func mediaItem(at index: Int) -> ImagePickerGridItem {
let mediaAsset = asset(at: index)
return ImagePickerGridItem(asset: mediaAsset, album: self)
}
// MARK: ImageManager
func requestThumbnail(for asset: PHAsset, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> Void) {
_ = imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: resultHandler)
}
private func requestImageDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { resolver in
_ = imageManager.requestImageData(for: asset, options: nil) { imageData, dataUTI, orientation, info in
guard let imageData = imageData else {
resolver.reject(PhotoLibraryError.assertionError("imageData was unexpectedly nil"))
return
}
guard let dataUTI = dataUTI else {
resolver.reject(PhotoLibraryError.assertionError("dataUTI was unexpectedly nil"))
return
}
guard let dataSource = DataSourceValue.dataSource(with: imageData, utiType: dataUTI) else {
resolver.reject(PhotoLibraryError.assertionError("dataSource was unexpectedly nil"))
return
}
resolver.fulfill((dataSource: dataSource, dataUTI: dataUTI))
}
}
}
private func requestVideoDataSource(for asset: PHAsset) -> Promise<(dataSource: DataSource, dataUTI: String)> {
return Promise { resolver in
_ = imageManager.requestExportSession(forVideo: asset, options: nil, exportPreset: AVAssetExportPresetMediumQuality) { exportSession, info in
guard let exportSession = exportSession else {
resolver.reject(PhotoLibraryError.assertionError("exportSession was unexpectedly nil"))
return
}
exportSession.outputFileType = AVFileType.mp4
exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing()
let exportPath = OWSFileSystem.temporaryFilePath(withFileExtension: "mp4")
let exportURL = URL(fileURLWithPath: exportPath)
exportSession.outputURL = exportURL
Logger.debug("starting video export")
exportSession.exportAsynchronously {
Logger.debug("Completed video export")
guard let dataSource = DataSourcePath.dataSource(with: exportURL, shouldDeleteOnDeallocation: true) else {
resolver.reject(PhotoLibraryError.assertionError("Failed to build data source for exported video URL"))
return
}
resolver.fulfill((dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String))
}
}
}
}
func outgoingAttachment(for asset: PHAsset) -> Promise<SignalAttachment> {
switch asset.mediaType {
case .image:
return requestImageDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .medium)
}
case .video:
return requestVideoDataSource(for: asset).map { (dataSource: DataSource, dataUTI: String) in
return SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI)
}
default:
return Promise(error: PhotoLibraryError.unsupportedMediaType)
}
}
}
class PhotoLibrary: NSObject, PHPhotoLibraryChangeObserver {
weak var delegate: PhotoLibraryDelegate?
var assetCollection: PHAssetCollection!
var availableWidth: CGFloat = 0
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
self.delegate?.photoLibraryDidChange(self)
}
}
override init() {
super.init()
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func albumForAllPhotos() -> PhotoLibraryAlbum {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let fetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
let title = NSLocalizedString("PHOTO_PICKER_DEFAULT_ALBUM", comment: "navbar title when viewing the default photo album, which includes all photos")
return PhotoLibraryAlbum(fetchResult: fetchResult, localizedTitle: title)
}
}

View File

@ -68,7 +68,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
collectionView.backgroundColor = Theme.backgroundColor
collectionView.register(MediaGalleryCell.self, forCellWithReuseIdentifier: MediaGalleryCell.reuseIdentifier)
collectionView.register(PhotoGridViewCell.self, forCellWithReuseIdentifier: PhotoGridViewCell.reuseIdentifier)
collectionView.register(MediaGallerySectionHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: MediaGallerySectionHeader.reuseIdentifier)
collectionView.register(MediaGalleryStaticHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: MediaGalleryStaticHeader.reuseIdentifier)
@ -209,12 +209,12 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
override public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
Logger.debug("")
guard let galleryCell = self.collectionView(collectionView, cellForItemAt: indexPath) as? MediaGalleryCell else {
guard let gridCell = self.collectionView(collectionView, cellForItemAt: indexPath) as? PhotoGridViewCell else {
owsFailDebug("galleryCell was unexpectedly nil")
return
}
guard let galleryItem = galleryCell.item else {
guard let galleryItem = (gridCell.item as? GalleryGridCellItem)?.galleryItem else {
owsFailDebug("galleryItem was unexpectedly nil")
return
}
@ -223,7 +223,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
updateDeleteButton()
} else {
collectionView.deselectItem(at: indexPath, animated: true)
self.delegate?.mediaTileViewController(self, didTapView: galleryCell.imageView, mediaGalleryItem: galleryItem)
self.delegate?.mediaTileViewController(self, didTapView: gridCell.imageView, mediaGalleryItem: galleryItem)
}
}
@ -359,12 +359,13 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
return defaultCell
}
guard let cell = self.collectionView?.dequeueReusableCell(withReuseIdentifier: MediaGalleryCell.reuseIdentifier, for: indexPath) as? MediaGalleryCell else {
guard let cell = self.collectionView?.dequeueReusableCell(withReuseIdentifier: PhotoGridViewCell.reuseIdentifier, for: indexPath) as? PhotoGridViewCell else {
owsFailDebug("unexpected cell for indexPath: \(indexPath)")
return defaultCell
}
cell.configure(item: galleryItem)
let gridCellItem = GalleryGridCellItem(galleryItem: galleryItem)
cell.configure(item: gridCellItem)
return cell
}
@ -878,130 +879,24 @@ private class MediaGalleryStaticHeader: UICollectionViewCell {
}
}
private class MediaGalleryCell: UICollectionViewCell {
class GalleryGridCellItem: PhotoGridItem {
let galleryItem: MediaGalleryItem
static let reuseIdentifier = "MediaGalleryCell"
public let imageView: UIImageView
private let contentTypeBadgeView: UIImageView
private let selectedBadgeView: UIImageView
private let highlightedView: UIView
private let selectedView: UIView
fileprivate var item: MediaGalleryItem?
static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video")
static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif")
static let selectedBadgeImage = #imageLiteral(resourceName: "selected_blue_circle")
override var isSelected: Bool {
didSet {
self.selectedBadgeView.isHidden = !self.isSelected
self.selectedView.isHidden = !self.isSelected
}
init(galleryItem: MediaGalleryItem) {
self.galleryItem = galleryItem
}
override var isHighlighted: Bool {
didSet {
self.highlightedView.isHidden = !self.isHighlighted
}
}
override init(frame: CGRect) {
self.imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
self.contentTypeBadgeView = UIImageView()
contentTypeBadgeView.isHidden = true
self.selectedBadgeView = UIImageView()
selectedBadgeView.image = MediaGalleryCell.selectedBadgeImage
selectedBadgeView.isHidden = true
self.highlightedView = UIView()
highlightedView.alpha = 0.2
highlightedView.backgroundColor = Theme.primaryColor
highlightedView.isHidden = true
self.selectedView = UIView()
selectedView.alpha = 0.3
selectedView.backgroundColor = Theme.backgroundColor
selectedView.isHidden = true
super.init(frame: frame)
self.clipsToBounds = true
self.contentView.addSubview(imageView)
self.contentView.addSubview(contentTypeBadgeView)
self.contentView.addSubview(highlightedView)
self.contentView.addSubview(selectedView)
self.contentView.addSubview(selectedBadgeView)
imageView.autoPinEdgesToSuperviewEdges()
highlightedView.autoPinEdgesToSuperviewEdges()
selectedView.autoPinEdgesToSuperviewEdges()
// 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(width: 18, height: 12)
contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3)
contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3)
contentTypeBadgeView.autoSetDimensions(to: kContentTypeBadgeSize)
let kSelectedBadgeSize = CGSize(width: 31, height: 31)
selectedBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
selectedBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0)
selectedBadgeView.autoSetDimensions(to: kSelectedBadgeSize)
}
@available(*, unavailable, message: "Unimplemented")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
public func configure(item: MediaGalleryItem) {
self.item = item
if let image = item.thumbnailImage(async: {
[weak self] (image) in
guard let strongSelf = self else {
return
}
guard strongSelf.item == item else {
return
}
strongSelf.imageView.image = image
strongSelf.imageView.backgroundColor = UIColor.clear
}) {
self.imageView.image = image
self.imageView.backgroundColor = UIColor.clear
var type: PhotoGridItemType {
if galleryItem.isVideo {
return .video
} else if galleryItem.isAnimated {
return .animated
} else {
// TODO: Show a placeholder?
self.imageView.backgroundColor = Theme.offBackgroundColor
}
if item.isVideo {
self.contentTypeBadgeView.isHidden = false
self.contentTypeBadgeView.image = MediaGalleryCell.videoBadgeImage
} else if item.isAnimated {
self.contentTypeBadgeView.isHidden = false
self.contentTypeBadgeView.image = MediaGalleryCell.animatedBadgeImage
} else {
assert(item.isImage)
self.contentTypeBadgeView.isHidden = true
return .photo
}
}
override public func prepareForReuse() {
super.prepareForReuse()
self.item = nil
self.imageView.image = nil
self.contentTypeBadgeView.isHidden = true
self.highlightedView.isHidden = true
self.selectedView.isHidden = true
self.selectedBadgeView.isHidden = true
func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? {
return galleryItem.thumbnailImage(async: completion)
}
}

View File

@ -0,0 +1,152 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
public enum PhotoGridItemType {
case photo, animated, video
}
public protocol PhotoGridItem: class {
var type: PhotoGridItemType { get }
func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage?
}
public class PhotoGridViewCell: UICollectionViewCell {
static let reuseIdentifier = "PhotoGridViewCell"
public let imageView: UIImageView
private let contentTypeBadgeView: UIImageView
private let selectedBadgeView: UIImageView
private let highlightedView: UIView
private let selectedView: UIView
var item: PhotoGridItem?
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: "selected_blue_circle")
override public var isSelected: Bool {
didSet {
self.selectedBadgeView.isHidden = !self.isSelected
self.selectedView.isHidden = !self.isSelected
}
}
override public var isHighlighted: Bool {
didSet {
self.highlightedView.isHidden = !self.isHighlighted
}
}
override init(frame: CGRect) {
self.imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
self.contentTypeBadgeView = UIImageView()
contentTypeBadgeView.isHidden = true
self.selectedBadgeView = UIImageView()
selectedBadgeView.image = PhotoGridViewCell.selectedBadgeImage
selectedBadgeView.isHidden = true
self.highlightedView = UIView()
highlightedView.alpha = 0.2
highlightedView.backgroundColor = Theme.primaryColor
highlightedView.isHidden = true
self.selectedView = UIView()
selectedView.alpha = 0.3
selectedView.backgroundColor = Theme.backgroundColor
selectedView.isHidden = true
super.init(frame: frame)
self.clipsToBounds = true
self.contentView.addSubview(imageView)
self.contentView.addSubview(contentTypeBadgeView)
self.contentView.addSubview(highlightedView)
self.contentView.addSubview(selectedView)
self.contentView.addSubview(selectedBadgeView)
imageView.autoPinEdgesToSuperviewEdges()
highlightedView.autoPinEdgesToSuperviewEdges()
selectedView.autoPinEdgesToSuperviewEdges()
// 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(width: 18, height: 12)
contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3)
contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3)
contentTypeBadgeView.autoSetDimensions(to: kContentTypeBadgeSize)
let kSelectedBadgeSize = CGSize(width: 31, height: 31)
selectedBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
selectedBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0)
selectedBadgeView.autoSetDimensions(to: kSelectedBadgeSize)
}
@available(*, unavailable, message: "Unimplemented")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
var image: UIImage? {
get { return imageView.image }
set {
imageView.image = newValue
imageView.backgroundColor = newValue == nil ? Theme.offBackgroundColor : .clear
}
}
var contentTypeBadgeImage: UIImage? {
get { return contentTypeBadgeView.image }
set {
contentTypeBadgeView.image = newValue
contentTypeBadgeView.isHidden = newValue == nil
}
}
public func configure(item: PhotoGridItem) {
self.item = item
self.image = item.asyncThumbnail { image in
guard let currentItem = self.item else {
return
}
guard currentItem === item else {
return
}
if image == nil {
Logger.debug("image == nil")
}
self.image = image
}
switch item.type {
case .video:
self.contentTypeBadgeImage = PhotoGridViewCell.videoBadgeImage
case .animated:
self.contentTypeBadgeImage = PhotoGridViewCell.animatedBadgeImage
case .photo:
self.contentTypeBadgeImage = nil
}
}
override public func prepareForReuse() {
super.prepareForReuse()
self.item = nil
self.imageView.image = nil
self.contentTypeBadgeView.isHidden = true
self.highlightedView.isHidden = true
self.selectedView.isHidden = true
self.selectedBadgeView.isHidden = true
}
}

View File

@ -1502,6 +1502,9 @@
/* Label for 'Work FAX' phone numbers. */
"PHONE_NUMBER_TYPE_WORK_FAX" = "Work Fax";
/* navbar title when viewing the default photo album, which includes all photos */
"PHOTO_PICKER_DEFAULT_ALBUM" = "All Photos";
/* Accessibility label for button to start media playback */
"PLAY_BUTTON_ACCESSABILITY_LABEL" = "Play Media";

View File

@ -243,7 +243,7 @@ public class SignalAttachment: NSObject {
public var outgoingAttachmentInfo: OutgoingAttachmentInfo {
return OutgoingAttachmentInfo(dataSource: dataSource, contentType: mimeType, sourceFilename: filenameOrDefault)
}
@objc
public func image() -> UIImage? {
if let cachedImage = cachedImage {
@ -1106,7 +1106,7 @@ public class SignalAttachment: NSObject {
// MARK: Attachments
// Factory method for attachments of any kind.
// Factory method for non-image Attachments.
//
// NOTE: The attachment returned by this method may not be valid.
// Check the attachment's error property.