mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Custom photo picker, respects theme/call banner
- share GridViewCell - Multiple image selection, with feature flag, cant currently approve multiple
This commit is contained in:
parent
c8ac66ff8f
commit
4c5d46e8f8
|
@ -430,6 +430,8 @@
|
||||||
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
|
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
|
||||||
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; };
|
4C11AA5020FD59C700351FBD /* MessageStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C11AA4F20FD59C700351FBD /* MessageStatusView.swift */; };
|
||||||
4C13C9F620E57BA30089A98B /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C13C9F520E57BA30089A98B /* ColorPickerViewController.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 */; };
|
4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadViewModel.swift */; };
|
||||||
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; };
|
4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C20B2B820CA10DE001BAC90 /* ConversationSearchViewController.swift */; };
|
||||||
4C23A5F2215C4ADE00534937 /* SheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C23A5F1215C4ADE00534937 /* SheetViewController.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
4C2F454E214C00E1004871FF /* AvatarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1762,6 +1766,7 @@
|
||||||
34D1F0BE1F8EC1760066283D /* Utils */,
|
34D1F0BE1F8EC1760066283D /* Utils */,
|
||||||
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
|
452B998F20A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift */,
|
||||||
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */,
|
4C13C9F520E57BA30089A98B /* ColorPickerViewController.swift */,
|
||||||
|
4C1885CF218D0EA800B67051 /* ImagePickerController.swift */,
|
||||||
);
|
);
|
||||||
path = ViewControllers;
|
path = ViewControllers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2248,6 +2253,7 @@
|
||||||
450D19121F85236600970622 /* RemoteVideoView.m */,
|
450D19121F85236600970622 /* RemoteVideoView.m */,
|
||||||
4CA5F792211E1F06008C2708 /* Toast.swift */,
|
4CA5F792211E1F06008C2708 /* Toast.swift */,
|
||||||
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */,
|
||||||
|
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
|
||||||
);
|
);
|
||||||
name = Views;
|
name = Views;
|
||||||
path = views;
|
path = views;
|
||||||
|
@ -3306,6 +3312,7 @@
|
||||||
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
|
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
|
||||||
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
|
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
|
||||||
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
|
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
|
||||||
|
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
|
||||||
45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */,
|
45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */,
|
||||||
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
|
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
|
||||||
34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */,
|
34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */,
|
||||||
|
@ -3423,6 +3430,7 @@
|
||||||
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
|
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
|
||||||
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
|
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
|
||||||
340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */,
|
340FC8C0204DB7D2007AEB0F /* OWSBackupExportJob.m in Sources */,
|
||||||
|
4C1885D0218D0EA800B67051 /* ImagePickerController.swift in Sources */,
|
||||||
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
|
||||||
340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
|
340FC8A7204DAC8D007AEB0F /* RegistrationViewController.m in Sources */,
|
||||||
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
|
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
|
||||||
|
|
|
@ -125,6 +125,7 @@ typedef enum : NSUInteger {
|
||||||
UIDocumentMenuDelegate,
|
UIDocumentMenuDelegate,
|
||||||
UIDocumentPickerDelegate,
|
UIDocumentPickerDelegate,
|
||||||
UIImagePickerControllerDelegate,
|
UIImagePickerControllerDelegate,
|
||||||
|
OWSImagePickerControllerDelegate,
|
||||||
UINavigationControllerDelegate,
|
UINavigationControllerDelegate,
|
||||||
UITextViewDelegate,
|
UITextViewDelegate,
|
||||||
ConversationCollectionViewDelegate,
|
ConversationCollectionViewDelegate,
|
||||||
|
@ -2650,14 +2651,14 @@ typedef enum : NSUInteger {
|
||||||
OWSLogWarn(@"Media Library permission denied.");
|
OWSLogWarn(@"Media Library permission denied.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UIImagePickerController *picker = [UIImagePickerController new];
|
OWSImagePickerGridController *picker = [OWSImagePickerGridController new];
|
||||||
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
|
||||||
picker.delegate = self;
|
picker.delegate = self;
|
||||||
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage, (__bridge NSString *)kUTTypeMovie ];
|
|
||||||
|
OWSNavigationController *pickerModal = [[OWSNavigationController alloc] initWithRootViewController:picker];
|
||||||
|
|
||||||
[self dismissKeyBoard];
|
[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;
|
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
|
* Fetching data from UIImagePickerController
|
||||||
*/
|
*/
|
||||||
|
@ -2718,7 +2727,7 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
NSString *mediaType = info[UIImagePickerControllerMediaType];
|
NSString *mediaType = info[UIImagePickerControllerMediaType];
|
||||||
if ([mediaType isEqualToString:(__bridge NSString *)kUTTypeMovie]) {
|
if ([mediaType isEqualToString:(__bridge NSString *)kUTTypeMovie]) {
|
||||||
// Video picked from library or captured with camera
|
// Video captured with camera
|
||||||
|
|
||||||
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
|
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
|
||||||
[self dismissViewControllerAnimated:YES
|
[self dismissViewControllerAnimated:YES
|
||||||
|
@ -2756,57 +2765,8 @@ typedef enum : NSUInteger {
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
} else {
|
} else {
|
||||||
// Non-Video image picked from library
|
OWSFailDebug(
|
||||||
|
@"Only use UIImagePicker for camera/video capture. Picking media from UIImagePicker is not supported. ");
|
||||||
// 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];
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2878,9 +2838,8 @@ typedef enum : NSUInteger {
|
||||||
VideoCompressionResult *compressionResult =
|
VideoCompressionResult *compressionResult =
|
||||||
[SignalAttachment compressVideoAsMp4WithDataSource:dataSource
|
[SignalAttachment compressVideoAsMp4WithDataSource:dataSource
|
||||||
dataUTI:(NSString *)kUTTypeMPEG4];
|
dataUTI:(NSString *)kUTTypeMPEG4];
|
||||||
[compressionResult.attachmentPromise retainUntilComplete];
|
|
||||||
|
|
||||||
compressionResult.attachmentPromise.then(^(SignalAttachment *attachment) {
|
[compressionResult.attachmentPromise.then(^(SignalAttachment *attachment) {
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
OWSAssertDebug([attachment isKindOfClass:[SignalAttachment class]]);
|
OWSAssertDebug([attachment isKindOfClass:[SignalAttachment class]]);
|
||||||
|
|
||||||
|
@ -2897,7 +2856,7 @@ typedef enum : NSUInteger {
|
||||||
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog];
|
[self tryToSendAttachmentIfApproved:attachment skipApprovalDialog:skipApprovalDialog];
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
});
|
}) retainUntilComplete];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
433
Signal/src/ViewControllers/ImagePickerController.swift
Normal file
433
Signal/src/ViewControllers/ImagePickerController.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,7 +68,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
|
||||||
|
|
||||||
collectionView.backgroundColor = Theme.backgroundColor
|
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(MediaGallerySectionHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: MediaGallerySectionHeader.reuseIdentifier)
|
||||||
collectionView.register(MediaGalleryStaticHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: MediaGalleryStaticHeader.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) {
|
override public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
Logger.debug("")
|
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")
|
owsFailDebug("galleryCell was unexpectedly nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let galleryItem = galleryCell.item else {
|
guard let galleryItem = (gridCell.item as? GalleryGridCellItem)?.galleryItem else {
|
||||||
owsFailDebug("galleryItem was unexpectedly nil")
|
owsFailDebug("galleryItem was unexpectedly nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -223,7 +223,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
|
||||||
updateDeleteButton()
|
updateDeleteButton()
|
||||||
} else {
|
} else {
|
||||||
collectionView.deselectItem(at: indexPath, animated: true)
|
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
|
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)")
|
owsFailDebug("unexpected cell for indexPath: \(indexPath)")
|
||||||
return defaultCell
|
return defaultCell
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.configure(item: galleryItem)
|
let gridCellItem = GalleryGridCellItem(galleryItem: galleryItem)
|
||||||
|
cell.configure(item: gridCellItem)
|
||||||
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
@ -878,130 +879,24 @@ private class MediaGalleryStaticHeader: UICollectionViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MediaGalleryCell: UICollectionViewCell {
|
class GalleryGridCellItem: PhotoGridItem {
|
||||||
|
let galleryItem: MediaGalleryItem
|
||||||
|
|
||||||
static let reuseIdentifier = "MediaGalleryCell"
|
init(galleryItem: MediaGalleryItem) {
|
||||||
|
self.galleryItem = galleryItem
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override var isHighlighted: Bool {
|
var type: PhotoGridItemType {
|
||||||
didSet {
|
if galleryItem.isVideo {
|
||||||
self.highlightedView.isHidden = !self.isHighlighted
|
return .video
|
||||||
}
|
} else if galleryItem.isAnimated {
|
||||||
}
|
return .animated
|
||||||
|
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: Show a placeholder?
|
return .photo
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func prepareForReuse() {
|
func asyncThumbnail(completion: @escaping (UIImage?) -> Void) -> UIImage? {
|
||||||
super.prepareForReuse()
|
return galleryItem.thumbnailImage(async: completion)
|
||||||
|
|
||||||
self.item = nil
|
|
||||||
self.imageView.image = nil
|
|
||||||
self.contentTypeBadgeView.isHidden = true
|
|
||||||
self.highlightedView.isHidden = true
|
|
||||||
self.selectedView.isHidden = true
|
|
||||||
self.selectedBadgeView.isHidden = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
152
Signal/src/views/PhotoGridViewCell.swift
Normal file
152
Signal/src/views/PhotoGridViewCell.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1502,6 +1502,9 @@
|
||||||
/* Label for 'Work FAX' phone numbers. */
|
/* Label for 'Work FAX' phone numbers. */
|
||||||
"PHONE_NUMBER_TYPE_WORK_FAX" = "Work Fax";
|
"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 */
|
/* Accessibility label for button to start media playback */
|
||||||
"PLAY_BUTTON_ACCESSABILITY_LABEL" = "Play Media";
|
"PLAY_BUTTON_ACCESSABILITY_LABEL" = "Play Media";
|
||||||
|
|
||||||
|
|
|
@ -243,7 +243,7 @@ public class SignalAttachment: NSObject {
|
||||||
public var outgoingAttachmentInfo: OutgoingAttachmentInfo {
|
public var outgoingAttachmentInfo: OutgoingAttachmentInfo {
|
||||||
return OutgoingAttachmentInfo(dataSource: dataSource, contentType: mimeType, sourceFilename: filenameOrDefault)
|
return OutgoingAttachmentInfo(dataSource: dataSource, contentType: mimeType, sourceFilename: filenameOrDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public func image() -> UIImage? {
|
public func image() -> UIImage? {
|
||||||
if let cachedImage = cachedImage {
|
if let cachedImage = cachedImage {
|
||||||
|
@ -1106,7 +1106,7 @@ public class SignalAttachment: NSObject {
|
||||||
|
|
||||||
// MARK: Attachments
|
// 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.
|
// NOTE: The attachment returned by this method may not be valid.
|
||||||
// Check the attachment's error property.
|
// Check the attachment's error property.
|
||||||
|
|
Loading…
Reference in a new issue