diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index 66627d459b..26d033e9cb 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -639,6 +639,12 @@ 7CE2DC9A28587DE100AF00AE /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B8BB58F234F36C000991ACB /* Colors.xcassets */; }; 7CE2DE102858B52000AF00AE /* WallpaperImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE2DE0F2858B52000AF00AE /* WallpaperImageView.swift */; }; 7CE3A25C2771A8AB006BE765 /* DeleteAccountVerifyCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE3A25B2771A8AB006BE765 /* DeleteAccountVerifyCodeViewController.swift */; }; + 7CE4BA1F283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CE4BA1D283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib */; }; + 7CE4BA20283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA1E283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift */; }; + 7CE4BA23283CD297001C87D5 /* MediaPreviewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CE4BA21283CD297001C87D5 /* MediaPreviewCell.xib */; }; + 7CE4BA24283CD297001C87D5 /* MediaPreviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA22283CD297001C87D5 /* MediaPreviewCell.swift */; }; + 7CE4BA26283CD2B4001C87D5 /* SelectedPhotoInputItemsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA25283CD2B3001C87D5 /* SelectedPhotoInputItemsViewController.swift */; }; + 7CE4BA28283CD2C9001C87D5 /* SelectedMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE4BA27283CD2C9001C87D5 /* SelectedMediaCell.swift */; }; 7CE5E7A8269BDA29000B7904 /* HomeAppsPinTipsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE5E7A6269BDA29000B7904 /* HomeAppsPinTipsViewController.swift */; }; 7CE5E7A9269BDA29000B7904 /* HomeAppsPinTipsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CE5E7A7269BDA29000B7904 /* HomeAppsPinTipsView.xib */; }; 7CF2FEA626AA89BA00D3A5B3 /* StickersAlbumPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CF2FEA526AA89BA00D3A5B3 /* StickersAlbumPreviewViewController.swift */; }; @@ -1658,6 +1664,12 @@ 7CDBA58D28F7B6CB00AC3777 /* TransferSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSearchViewController.swift; sourceTree = ""; }; 7CE2DE0F2858B52000AF00AE /* WallpaperImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperImageView.swift; sourceTree = ""; }; 7CE3A25B2771A8AB006BE765 /* DeleteAccountVerifyCodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountVerifyCodeViewController.swift; sourceTree = ""; }; + 7CE4BA1D283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SelectedPhotoInputItemsPreviewWindow.xib; sourceTree = ""; }; + 7CE4BA1E283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedPhotoInputItemsPreviewWindow.swift; sourceTree = ""; }; + 7CE4BA21283CD297001C87D5 /* MediaPreviewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MediaPreviewCell.xib; sourceTree = ""; }; + 7CE4BA22283CD297001C87D5 /* MediaPreviewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPreviewCell.swift; sourceTree = ""; }; + 7CE4BA25283CD2B3001C87D5 /* SelectedPhotoInputItemsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedPhotoInputItemsViewController.swift; sourceTree = ""; }; + 7CE4BA27283CD2C9001C87D5 /* SelectedMediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedMediaCell.swift; sourceTree = ""; }; 7CE5E7A6269BDA29000B7904 /* HomeAppsPinTipsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAppsPinTipsViewController.swift; sourceTree = ""; }; 7CE5E7A7269BDA29000B7904 /* HomeAppsPinTipsView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HomeAppsPinTipsView.xib; sourceTree = ""; }; 7CF2FEA526AA89BA00D3A5B3 /* StickersAlbumPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersAlbumPreviewViewController.swift; sourceTree = ""; }; @@ -2861,6 +2873,7 @@ 7B915F73215FB0C100A562C6 /* GiphySearchViewController.swift */, 7B05CFDC22293B72006DA9E3 /* PhotoInputViewController.swift */, 7B93CAA4222963120053AE90 /* PhotoInputGridViewController.swift */, + 7CE4BA25283CD2B3001C87D5 /* SelectedPhotoInputItemsViewController.swift */, 7B6A4045228400AF0037C7E5 /* MessageReceiverViewController.swift */, 7BFD3457228589ED00524EA0 /* ContactSelectorViewController.swift */, 7BEBCE2C228185130037BF18 /* MediaPreviewViewController.swift */, @@ -2976,6 +2989,8 @@ 7C0FAAC827E07A0A008D4021 /* ExpiredMessageTimePickerWindow.xib */, 7C47352828571CC900ECD293 /* AccessPhoneContactHintWindow.swift */, 7C47352A28571D0300ECD293 /* AccessPhoneContactHintWindow.xib */, + 7CE4BA1E283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift */, + 7CE4BA1D283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib */, ); path = Windows; sourceTree = ""; @@ -3039,6 +3054,8 @@ DF53BB6E202362E5002BF028 /* AuthorizationScopeCell.xib */, 7C53049828FE753400567CF6 /* AuthorizationScopeGroupCell.swift */, 7C53049928FE753400567CF6 /* AuthorizationScopeGroupCell.xib */, + 7CE4BA22283CD297001C87D5 /* MediaPreviewCell.swift */, + 7CE4BA21283CD297001C87D5 /* MediaPreviewCell.xib */, ); path = Cells; sourceTree = ""; @@ -3610,6 +3627,7 @@ 7B04142A240FA09F00BE8D73 /* LocationCell.swift */, 7BA1768D244ACE2E007D50FD /* PickerCell.swift */, 7B3CDA6324FFDD1D003A3E80 /* FavoriteStickerCell.swift */, + 7CE4BA27283CD2C9001C87D5 /* SelectedMediaCell.swift */, ); path = Cells; sourceTree = ""; @@ -3997,6 +4015,7 @@ 7B600D282453186B001D8146 /* DesktopTableHeaderView.xib in Resources */, 7BB7872B24514ACC0057B4ED /* PhoneContactsSettingTableHeaderView.xib in Resources */, 7B7B5DB5230EBEBA00D0F463 /* TransferTypeCell.xib in Resources */, + 7CE4BA23283CD297001C87D5 /* MediaPreviewCell.xib in Resources */, E0BEB85D236C1C49001FE534 /* ProfileMenuItemView.xib in Resources */, 7BACA8D523602BF8007E3381 /* RecorderLongPressHintView.xib in Resources */, 94C6AA0B280D36940011AB02 /* AttachmentDiagnosticView.xib in Resources */, @@ -4039,6 +4058,7 @@ 7B81BF2922893F8B00266A77 /* GroupParticipantCell.xib in Resources */, 7B36920A233B3650007321A7 /* MediaTypeOverlayView.xib in Resources */, 944ED8D0264640E200C97215 /* WebLoadingFailureView.xib in Resources */, + 7CE4BA1F283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.xib in Resources */, 7BB788B2216C5A4A00EDE7B4 /* LoadingIndicatorFooterView.xib in Resources */, DFDD89E922C4B8E600128991 /* DepositChooseNetworkWindow.xib in Resources */, DFA5B5911FB04C9C00549728 /* Wallet.storyboard in Resources */, @@ -4426,6 +4446,7 @@ files = ( 7BF49DD320C3DBAC00A8510E /* CaptchaManager.swift in Sources */, DF8CECE11FC3054700E40064 /* TransferTypeCell.swift in Sources */, + 7CE4BA26283CD2B4001C87D5 /* SelectedPhotoInputItemsViewController.swift in Sources */, 7BCB8C8422BB56B8002A13CC /* DataAndStorageSettingsViewController.swift in Sources */, 9BB351671FB19ECB00EDDD2C /* ConversationDateHeaderView.swift in Sources */, DF2819752014669E001EE5FA /* RefreshAccountJob.swift in Sources */, @@ -4819,6 +4840,7 @@ 7B51DDB2223A408A008ACDBB /* LoginMobileNumberViewController.swift in Sources */, 7B21782122C4E70B00C08106 /* OggOpusPlayer.swift in Sources */, 7B3CDA6424FFDD1D003A3E80 /* FavoriteStickerCell.swift in Sources */, + 7CE4BA20283CD249001C87D5 /* SelectedPhotoInputItemsPreviewWindow.swift in Sources */, 7BC3559F2265B7C30073C7BF /* DragDownIndicator.swift in Sources */, 7BA398CD242B539900DB5154 /* UIColor+Assets.swift in Sources */, 7B8BB588234F160C00991ACB /* SharedMediaCategorizer.swift in Sources */, @@ -4890,6 +4912,7 @@ 7B9D825A22F1BFEA0099381E /* NormalNetworkOperationIconSet.swift in Sources */, 7B6E5503223F69D90060E6FC /* KeyboardBasedLayoutViewController.swift in Sources */, 9B748DCD1FA71CEF00BC009B /* CameraViewController.swift in Sources */, + 7CE4BA24283CD297001C87D5 /* MediaPreviewCell.swift in Sources */, 7B54F95B22B24A5600908A9D /* CreateEmergencyContactVerificationCodeViewController.swift in Sources */, 7BB0F9512434DDD400BEDA97 /* CircleMemberSearchResult.swift in Sources */, 7B81D91423E93ECA0031E945 /* QuotedMessageView.swift in Sources */, @@ -4968,6 +4991,7 @@ 7BD7534C2182CD7A00BAC172 /* CallMessageViewModel.swift in Sources */, 7B2D174F22B11A8600AE3DD8 /* LoginInfoInputViewController.swift in Sources */, DF1ED8D920BBECFF003E10E8 /* AlbumViewController.swift in Sources */, + 7CE4BA28283CD2C9001C87D5 /* SelectedMediaCell.swift in Sources */, 7BEE5351222D0E5C008D3911 /* ConversationExtensionCell.swift in Sources */, DF8CECF21FC4256D00E40064 /* BlockUserCell.swift in Sources */, 7CC730502745F95D002780F5 /* StickerStore.swift in Sources */, diff --git a/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json new file mode 100644 index 0000000000..89f4a91024 --- /dev/null +++ b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_video_bold@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_video_bold@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@2x.png b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@2x.png new file mode 100644 index 0000000000..d40d895c1e Binary files /dev/null and b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@2x.png differ diff --git a/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@3x.png b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@3x.png new file mode 100644 index 0000000000..3179d4d2fb Binary files /dev/null and b/Mixin/Assets.xcassets/Conversation/ic_video_bold.imageset/ic_video_bold@3x.png differ diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json b/Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json new file mode 100644 index 0000000000..2803457d58 --- /dev/null +++ b/Mixin/Assets.xcassets/ic_media_close.imageset/Contents.json @@ -0,0 +1,54 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_media_close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_media_close_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_media_close@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_media_close_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@2x.png b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@2x.png new file mode 100644 index 0000000000..f59d49cfa6 Binary files /dev/null and b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@2x.png differ diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@3x.png b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@3x.png new file mode 100644 index 0000000000..c4153f188a Binary files /dev/null and b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close@3x.png differ diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@2x.png b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@2x.png new file mode 100644 index 0000000000..e80af0b687 Binary files /dev/null and b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@2x.png differ diff --git a/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@3x.png b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@3x.png new file mode 100644 index 0000000000..b363c2662e Binary files /dev/null and b/Mixin/Assets.xcassets/ic_media_close.imageset/ic_media_close_dark@3x.png differ diff --git a/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/Contents.json b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/Contents.json new file mode 100644 index 0000000000..74cc4c6c8d --- /dev/null +++ b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_photo_checkmark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_photo_checkmark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@2x.png b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@2x.png new file mode 100644 index 0000000000..755af11ffd Binary files /dev/null and b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@2x.png differ diff --git a/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@3x.png b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@3x.png new file mode 100644 index 0000000000..9f774b1b68 Binary files /dev/null and b/Mixin/Assets.xcassets/ic_photo_checkmark.imageset/ic_photo_checkmark@3x.png differ diff --git a/Mixin/Assets.xcassets/ic_photo_unselected.imageset/Contents.json b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/Contents.json new file mode 100644 index 0000000000..7139e5d701 --- /dev/null +++ b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_photo_unselected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_photo_unselected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@2x.png b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@2x.png new file mode 100644 index 0000000000..5f4f402c92 Binary files /dev/null and b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@2x.png differ diff --git a/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@3x.png b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@3x.png new file mode 100644 index 0000000000..d3fd566e73 Binary files /dev/null and b/Mixin/Assets.xcassets/ic_photo_unselected.imageset/ic_photo_unselected@3x.png differ diff --git a/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json new file mode 100644 index 0000000000..cfe7a5145c --- /dev/null +++ b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_photo_unselected_narrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_photo_unselected_narrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@2x.png b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@2x.png new file mode 100644 index 0000000000..bd6e1a4d2a Binary files /dev/null and b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@2x.png differ diff --git a/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@3x.png b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@3x.png new file mode 100644 index 0000000000..2eed5be70a Binary files /dev/null and b/Mixin/Assets.xcassets/ic_photo_unselected_narrow.imageset/ic_photo_unselected_narrow@3x.png differ diff --git a/Mixin/UserInterface/Controllers/Chat/Cells/PhotoInputGridCell.swift b/Mixin/UserInterface/Controllers/Chat/Cells/PhotoInputGridCell.swift index f7fc5e9819..4db203c1ef 100644 --- a/Mixin/UserInterface/Controllers/Chat/Cells/PhotoInputGridCell.swift +++ b/Mixin/UserInterface/Controllers/Chat/Cells/PhotoInputGridCell.swift @@ -5,6 +5,9 @@ class PhotoInputGridCell: UICollectionViewCell { @IBOutlet weak var imageWrapperView: UIView! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var mediaTypeView: MediaTypeOverlayView! + @IBOutlet weak var statusImageView: UIImageView! + @IBOutlet weak var indexLabel: UILabel! + @IBOutlet weak var overlayView: UIView! let cornerRadius: CGFloat = 8 @@ -30,6 +33,20 @@ class PhotoInputGridCell: UICollectionViewCell { imageView.image = nil } + func updateBadge(with index: Int?) { + if let index = index { + indexLabel.text = "\(index + 1)" + statusImageView.backgroundColor = .theme + statusImageView.image = nil + overlayView.isHidden = false + } else { + indexLabel.text = nil + statusImageView.backgroundColor = .clear + statusImageView.image = R.image.ic_photo_unselected_narrow() + overlayView.isHidden = true + } + } + private func updateShadowPathIfNeeded() { guard imageWrapperView.frame != lastImageWrapperFrame else { return diff --git a/Mixin/UserInterface/Controllers/Chat/Cells/SelectedMediaCell.swift b/Mixin/UserInterface/Controllers/Chat/Cells/SelectedMediaCell.swift new file mode 100644 index 0000000000..6226c783d6 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Chat/Cells/SelectedMediaCell.swift @@ -0,0 +1,50 @@ +import UIKit +import Photos +import SDWebImage +import CoreServices +import MixinServices + +class SelectedMediaCell: UICollectionViewCell { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var mediaTypeView: MediaTypeOverlayView! + + var deselectAsset: (() -> Void)? + + private var requestId: PHImageRequestID? + private lazy var imageRequestOptions: PHImageRequestOptions = { + let options = PHImageRequestOptions() + options.version = .current + options.deliveryMode = .opportunistic + options.isNetworkAccessAllowed = true + return options + }() + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + if let id = requestId { + PHCachingImageManager.default().cancelImageRequest(id) + } + } + + func load(asset: PHAsset, size: CGSize) { + if asset.mediaType == .video { + mediaTypeView.style = .video(duration: asset.duration) + } else { + if let uti = asset.uniformTypeIdentifier, UTTypeConformsTo(uti as CFString, kUTTypeGIF) { + mediaTypeView.style = .gif + } else { + mediaTypeView.style = .hidden + } + } + requestId = PHImageManager.default().requestImage(for: asset, targetSize: size * UIScreen.main.scale, contentMode: .aspectFill, options: imageRequestOptions) { [weak self] (image, info) in + self?.imageView.image = image + } + } + + @IBAction func closeAction(_ sender: Any) { + deselectAsset?() + } + +} diff --git a/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift b/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift index 47ef23d7f0..a23f09d489 100644 --- a/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/ConversationInputViewController.swift @@ -385,9 +385,11 @@ class ConversationInputViewController: UIViewController { self?.quote = nil } - let recognizer = InteractiveResizeGestureRecognizer(target: self, action: #selector(interactiveResizeAction(_:))) - recognizer.delegate = self - view.addGestureRecognizer(recognizer) + if ScreenHeight.current > .short { + let recognizer = InteractiveResizeGestureRecognizer(target: self, action: #selector(interactiveResizeAction(_:))) + recognizer.delegate = self + view.addGestureRecognizer(recognizer) + } } func update(opponentUser user: UserItem) { @@ -407,6 +409,7 @@ class ConversationInputViewController: UIViewController { if minimize { setPreferredContentHeightAnimated(.minimized) } + photoViewController.dismissSelectedPhotoInputItemsViewControllerIfNeeded() UIView.animate(withDuration: 0.5, delay: 0, options: .overdampedCurve) { self.customInputContainerView.alpha = 0 } completion: { _ in diff --git a/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift b/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift index 60a92354b8..5da949e333 100644 --- a/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/PhotoInputGridViewController.swift @@ -3,11 +3,20 @@ import Photos import MobileCoreServices import MixinServices +protocol PhotoInputGridViewControllerDelegate: AnyObject { + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didSelect asset: PHAsset) + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didDeselect asset: PHAsset) + func photoInputGridViewControllerDidTapCamera(_ controller: PhotoInputGridViewController) +} + class PhotoInputGridViewController: UIViewController, ConversationAccessible, ConversationInputAccessible { @IBOutlet weak var collectionView: UICollectionView! @IBOutlet weak var collectionViewLayout: UICollectionViewFlowLayout! + weak var delegate: PhotoInputGridViewControllerDelegate? + weak var photoInputViewController: PhotoInputViewController? + var fetchResult: PHFetchResult? { didSet { guard isViewLoaded else { @@ -20,10 +29,14 @@ class PhotoInputGridViewController: UIViewController, ConversationAccessible, Co var firstCellIsCamera = true + private let maxSelectedCount = 99 private let interitemSpacing: CGFloat = 0 private let columnCount: CGFloat = 3 private let imageManager = PHCachingImageManager() + private var selectedAssets: [PHAsset] { + photoInputViewController?.selectedAssets ?? [] + } private lazy var imageRequestOptions: PHImageRequestOptions = { let options = PHImageRequestOptions() options.version = .current @@ -71,6 +84,18 @@ class PhotoInputGridViewController: UIViewController, ConversationAccessible, Co } +extension PhotoInputGridViewController { + + func updateVisibleCellBadge() { + for indexPath in collectionView.indexPathsForVisibleItems { + if let asset = asset(at: indexPath), let cell = collectionView.cellForItem(at: indexPath) as? PhotoInputGridCell { + cell.updateBadge(with: selectedAssets.firstIndex(of: asset)) + } + } + } + +} + extension PhotoInputGridViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { @@ -89,10 +114,15 @@ extension PhotoInputGridViewController: UICollectionViewDataSource { cell.imageView.image = R.image.conversation.ic_camera() cell.imageView.backgroundColor = R.color.camera_background() cell.mediaTypeView.style = .hidden + cell.indexLabel.isHidden = true + cell.statusImageView.isHidden = true } else if let asset = asset(at: indexPath) { cell.identifier = asset.localIdentifier cell.imageView.contentMode = .scaleAspectFill cell.imageView.backgroundColor = .background + cell.indexLabel.isHidden = false + cell.statusImageView.isHidden = false + cell.updateBadge(with: selectedAssets.firstIndex(of: asset)) imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: imageRequestOptions) { [weak cell] (image, _) in guard let cell = cell, cell.identifier == asset.localIdentifier else { return @@ -125,13 +155,15 @@ extension PhotoInputGridViewController: UICollectionViewDelegate { if firstCellIsCamera && indexPath.item == 0 { UIApplication.homeContainerViewController?.pipController?.pauseAction(self) conversationViewController?.imagePickerController.presentCamera() + delegate?.photoInputGridViewControllerDidTapCamera(self) } else if let asset = asset(at: indexPath) { - let vc = R.storyboard.chat.media_preview()! - vc.load(asset: asset) - vc.conversationInputViewController = conversationInputViewController - vc.transitioningDelegate = PopupPresentationManager.shared - vc.modalPresentationStyle = .custom - present(vc, animated: true, completion: nil) + if selectedAssets.contains(asset) { + delegate?.photoInputGridViewController(self, didDeselect: asset) + updateVisibleCellBadge() + } else if selectedAssets.count < maxSelectedCount { + delegate?.photoInputGridViewController(self, didSelect: asset) + updateVisibleCellBadge() + } } } diff --git a/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift b/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift index 6bb9168f9c..6c10f0bee6 100644 --- a/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift +++ b/Mixin/UserInterface/Controllers/Chat/PhotoInputViewController.swift @@ -21,11 +21,19 @@ class PhotoInputViewController: UIViewController, ConversationInputAccessible { } } + private weak var selectedPhotoInputItemsViewControllerIfLoaded: SelectedPhotoInputItemsViewController? + private lazy var selectedPhotoInputItemsViewController: SelectedPhotoInputItemsViewController = { + let controller = R.storyboard.chat.selected_photo_input_items()! + controller.delegate = self + selectedPhotoInputItemsViewControllerIfLoaded = controller + return controller + }() private var allPhotos: PHFetchResult? private var smartAlbums: PHFetchResult? private var sortedSmartAlbums: [PHAssetCollection]? private var userCollections: PHFetchResult? private var gridViewController: PhotoInputGridViewController! + private(set) var selectedAssets: [PHAsset] = [] deinit { PHPhotoLibrary.shared().unregisterChangeObserver(self) @@ -70,6 +78,8 @@ class PhotoInputViewController: UIViewController, ConversationInputAccessible { if let vc = segue.destination as? PhotoInputGridViewController { vc.fetchResult = allPhotos gridViewController = vc + gridViewController.delegate = self + gridViewController.photoInputViewController = self } } @@ -182,6 +192,7 @@ extension PhotoInputViewController: PHPhotoLibraryChangeObserver { func photoLibraryDidChange(_ changeInstance: PHChange) { DispatchQueue.main.sync { + self.dismissSelectedPhotoInputItemsViewControllerIfNeeded() if let allPhotos = self.allPhotos, let changeDetails = changeInstance.changeDetails(for: allPhotos) { self.allPhotos = changeDetails.fetchResultAfterChanges } @@ -220,6 +231,233 @@ extension PhotoInputViewController: PHPickerViewControllerDelegate { } +extension PhotoInputViewController: PhotoInputGridViewControllerDelegate { + + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didSelect asset: PHAsset) { + guard !selectedAssets.contains(asset) else { + return + } + if selectedAssets.isEmpty { + presentSelectedPhotoInputItemsViewControllerAnimated() + } + selectedAssets.append(asset) + selectedPhotoInputItemsViewController.add(asset) + } + + func photoInputGridViewController(_ controller: PhotoInputGridViewController, didDeselect asset: PHAsset) { + guard let index = selectedAssets.firstIndex(of: asset) else { + return + } + selectedAssets.remove(at: index) + if selectedAssets.isEmpty { + dismissSelectedPhotoInputItemsViewControllerAnimated() + } else { + selectedPhotoInputItemsViewController.remove(asset) + } + } + + func photoInputGridViewControllerDidTapCamera(_ controller: PhotoInputGridViewController) { + dismissSelectedPhotoInputItemsViewControllerIfNeeded() + } + +} + +extension PhotoInputViewController: SelectedPhotoInputItemsViewControllerDelegate { + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSend assets: [PHAsset]) { + sendItems(assets: assets) + } + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didCancelSend assets: [PHAsset]) { + conversationInputViewController?.dismiss() + } + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didDeselect asset: PHAsset) { + guard let index = selectedAssets.firstIndex(of: asset) else { + return + } + selectedAssets.remove(at: index) + gridViewController.updateVisibleCellBadge() + if selectedAssets.isEmpty { + dismissSelectedPhotoInputItemsViewControllerAnimated() + } + } + + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSelectAssetAt index: Int) { + conversationInputViewController?.setPreferredContentHeightAnimated(.regular) + let window = SelectedPhotoInputItemsPreviewWindow.instance() + window.load(assets: selectedAssets, initIndex: index) + window.delegate = self + window.presentPopupControllerAnimated() + } + +} + +extension PhotoInputViewController: SelectedPhotoInputItemsPreviewWindowDelegate { + + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, willDismissWindow assets: [PHAsset]) { + if assets.isEmpty { + selectedAssets.removeAll() + dismissSelectedPhotoInputItemsViewControllerAnimated() + } else { + selectedAssets = assets + selectedPhotoInputItemsViewController.updateAssets(assets) + } + gridViewController.updateVisibleCellBadge() + } + + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendItems assets: [PHAsset]) { + sendItems(assets: assets) + } + + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendFiles assets: [PHAsset]) { + sendAsFiles(assets: assets) + } + +} + +extension PhotoInputViewController { + + private func sendItems(assets: [PHAsset]) { + guard let controller = conversationInputViewController else { + return + } + assets.forEach(controller.send(asset:)) + selectedAssets.removeAll() + gridViewController.updateVisibleCellBadge() + dismissSelectedPhotoInputItemsViewControllerAnimated() + } + + private func sendAsFiles(assets: [PHAsset]) { + guard let controller = conversationInputViewController else { + return + } + let hud = Hud() + hud.show(style: .busy, text: "", on: AppDelegate.current.mainWindow) + requestURLs(for: assets) { [weak self] urls in + hud.hide() + guard let self = self else { + return + } + urls.forEach(controller.sendFile(url:)) + self.selectedAssets.removeAll() + self.gridViewController.updateVisibleCellBadge() + self.dismissSelectedPhotoInputItemsViewControllerAnimated() + } + } + + func dismissSelectedPhotoInputItemsViewControllerIfNeeded() { + guard let controller = selectedPhotoInputItemsViewControllerIfLoaded, controller.parent != nil else { + return + } + selectedAssets.removeAll() + gridViewController.updateVisibleCellBadge() + gridViewController.view.isUserInteractionEnabled = false + controller.removeAllAssets() + controller.view.removeFromSuperview() + controller.removeFromParent() + controller.view.snp.removeConstraints() + gridViewController.view.isUserInteractionEnabled = true + } + + private func presentSelectedPhotoInputItemsViewControllerAnimated() { + guard + selectedPhotoInputItemsViewController.parent == nil, + let conversationInputViewController = conversationInputViewController, + let inputBarView = conversationInputViewController.inputBarView, + let conversationViewController = conversationInputViewController.parent + else { + return + } + gridViewController.view.isUserInteractionEnabled = false + let controller = selectedPhotoInputItemsViewController + let viewHeight = selectedPhotoInputItemsViewController.viewHeight + addChild(controller) + view.insertSubview(controller.view, at: 0) + controller.view.snp.makeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalTo(inputBarView.snp.bottom).offset(0) + }) + view.layoutIfNeeded() + controller.view.snp.updateConstraints { make in + make.top.equalTo(inputBarView.snp.bottom).offset(-viewHeight) + } + UIView.animate(withDuration: 0.3, delay: 0, options: .overdampedCurve) { + self.view.layoutIfNeeded() + } completion: { _ in + conversationViewController.addChild(controller) + conversationViewController.view.addSubview(controller.view) + controller.view.snp.remakeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalTo(inputBarView.snp.bottom).offset(-viewHeight) + }) + self.gridViewController.view.isUserInteractionEnabled = true + } + } + + private func dismissSelectedPhotoInputItemsViewControllerAnimated() { + guard + selectedPhotoInputItemsViewController.parent != nil, + let conversationInputViewController = conversationInputViewController, + let inputBarView = conversationInputViewController.inputBarView + else { + return + } + gridViewController.view.isUserInteractionEnabled = false + let controller = selectedPhotoInputItemsViewController + let viewHeight = selectedPhotoInputItemsViewController.viewHeight + addChild(controller) + view.insertSubview(controller.view, at: 0) + controller.view.snp.remakeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalTo(inputBarView.snp.bottom).offset(-viewHeight) + }) + view.layoutIfNeeded() + controller.view.snp.updateConstraints { make in + make.top.equalTo(inputBarView.snp.bottom).offset(0) + } + UIView.animate(withDuration: 0.3, delay: 0, options: .overdampedCurve) { + self.view.layoutIfNeeded() + } completion: { _ in + controller.removeAllAssets() + controller.view.removeFromSuperview() + controller.removeFromParent() + controller.view.snp.removeConstraints() + self.gridViewController.view.isUserInteractionEnabled = true + } + } + + private func requestURLs(for assets: [PHAsset], completion: @escaping ((_ urls : [URL]) -> Void)) { + let group = DispatchGroup() + let queue = DispatchQueue(label: "one.mixin.messager.PhotoInputViewController.requestPHAssetsURLs", attributes: .concurrent) + var urls: [URL?] = Array(repeating: nil, count: assets.count) + for (index, asset) in assets.enumerated() { + group.enter() + queue.async(group: group) { + if asset.mediaType == .image { + let options = PHContentEditingInputRequestOptions() + options.canHandleAdjustmentData = { (adjustmeta: PHAdjustmentData) -> Bool in true } + asset.requestContentEditingInput(with: options, completionHandler: { (contentEditingInput, info) in + urls.insert(contentEditingInput?.fullSizeImageURL, at: index) + group.leave() + }) + } else if asset.mediaType == .video { + let options: PHVideoRequestOptions = PHVideoRequestOptions() + options.version = .original + PHImageManager.default().requestAVAsset(forVideo: asset, options: options, resultHandler: { (asset, audioMix, info) in + urls.insert((asset as? AVURLAsset)?.url, at: index) + group.leave() + }) + } + } + } + group.notify(queue: .main) { + completion(urls.compactMap { $0 }) + } + } + +} + fileprivate let collectionSubtypeOrder: [PHAssetCollectionSubtype: Int] = { var idx = -1 var autoIncrement: Int { diff --git a/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift b/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift new file mode 100644 index 0000000000..b6820b7826 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Chat/SelectedPhotoInputItemsViewController.swift @@ -0,0 +1,126 @@ +import UIKit +import Photos + +protocol SelectedPhotoInputItemsViewControllerDelegate: AnyObject { + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSend assets: [PHAsset]) + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didDeselect asset: PHAsset) + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didSelectAssetAt index: Int) + func selectedPhotoInputItemsViewController(_ controller: SelectedPhotoInputItemsViewController, didCancelSend assets: [PHAsset]) +} + +final class SelectedPhotoInputItemsViewController: UIViewController { + + let viewHeight: CGFloat = 224 + + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var sendButton: UIButton! + + weak var delegate: SelectedPhotoInputItemsViewControllerDelegate? + + private var cellSizeCache = [String: CGSize]() + private var assets = [PHAsset]() { + didSet { + sendButton.setTitle(R.string.localizable.send_count(assets.count), for: .normal) + } + } + + @IBAction func cancelAction(_ sender: Any) { + delegate?.selectedPhotoInputItemsViewController(self, didCancelSend: assets) + } + + @IBAction func sendAction(_ sender: Any) { + delegate?.selectedPhotoInputItemsViewController(self, didSend: assets) + } + +} + +extension SelectedPhotoInputItemsViewController { + + func add(_ asset: PHAsset) { + guard !assets.contains(asset) else { + return + } + assets.append(asset) + let index = IndexPath(item: assets.count - 1, section: 0) + collectionView.insertItems(at: [index]) + collectionView.scrollToItem(at: index, at: .centeredHorizontally, animated: true) + } + + func remove(_ asset: PHAsset) { + guard let index = assets.firstIndex(of: asset) else { + return + } + assets.remove(at: index) + cellSizeCache.removeValue(forKey: asset.localIdentifier) + collectionView.deleteItems(at: [IndexPath(item: index, section: 0)]) + } + + func removeAllAssets() { + cellSizeCache.removeAll() + assets.removeAll() + collectionView.reloadData() + } + + func updateAssets(_ selectedAssets: [PHAsset]) { + cellSizeCache.removeAll() + assets = selectedAssets + collectionView.reloadData() + } + +} + +extension SelectedPhotoInputItemsViewController: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + assets.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.selected_media, for: indexPath)! + if indexPath.item < assets.count { + let asset = assets[indexPath.item] + cell.load(asset: asset, size: cellSizeForItemAt(indexPath.item)) + cell.deselectAsset = { [weak self] in + guard let self = self else { + return + } + self.remove(asset) + self.delegate?.selectedPhotoInputItemsViewController(self, didDeselect: asset) + } + } + return cell + } + +} + +extension SelectedPhotoInputItemsViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + cellSizeForItemAt(indexPath.item) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + delegate?.selectedPhotoInputItemsViewController(self, didSelectAssetAt: indexPath.item) + } + +} + +extension SelectedPhotoInputItemsViewController { + + private func cellSizeForItemAt(_ index: Int) -> CGSize { + guard index < assets.count else { + return .zero + } + let asset = assets[index] + if let size = cellSizeCache[asset.localIdentifier] { + return size + } else { + let height: CGFloat = 160 + let width: CGFloat = ceil(height / CGFloat(asset.pixelHeight) * CGFloat(asset.pixelWidth)) + let size = CGSize(width: min(160, max(width, 62)), height: height) + cellSizeCache[asset.localIdentifier] = size + return size + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift index 4a27ecc559..8c27666ea5 100644 --- a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift +++ b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.swift @@ -13,6 +13,9 @@ class MediaTypeOverlayView: UIView, XibDesignable { @IBOutlet weak var gifFileTypeView: UILabel! @IBOutlet weak var videoTypeView: UIStackView! @IBOutlet weak var videoDurationLabel: UILabel! + @IBOutlet weak var videoImageView: UIImageView! + + @IBOutlet weak var typeViewBottomConstraint: NSLayoutConstraint! class var backgroundImage: UIImage? { return R.image.conversation.bg_photo_bottom_shadow() diff --git a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib index 368dd0924c..285f20ca37 100644 --- a/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib +++ b/Mixin/UserInterface/Controllers/Chat/Views/MediaTypeOverlayView.xib @@ -1,9 +1,9 @@ - + - + @@ -11,7 +11,9 @@ + + @@ -24,7 +26,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2354,20 +2506,52 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -2387,14 +2571,17 @@ + + + - + @@ -2806,6 +2993,8 @@ + + diff --git a/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift new file mode 100644 index 0000000000..bb7bd27ec8 --- /dev/null +++ b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.swift @@ -0,0 +1,59 @@ +import UIKit +import Photos +import SDWebImage +import CoreServices +import MixinServices + +class MediaPreviewCell: UICollectionViewCell { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var selectedStatusImageView: UIImageView! + @IBOutlet weak var mediaTypeView: MediaTypeOverlayView! + + private var requestId: PHImageRequestID? + private lazy var imageRequestOptions: PHImageRequestOptions = { + let options = PHImageRequestOptions() + options.version = .current + options.deliveryMode = .opportunistic + options.isNetworkAccessAllowed = true + return options + }() + + override func awakeFromNib() { + super.awakeFromNib() + selectedStatusImageView.isHidden = false + mediaTypeView.videoTypeView.spacing = 8 + mediaTypeView.typeViewBottomConstraint.constant = 8 + mediaTypeView.gifFileTypeView.font = .systemFont(ofSize: 16) + mediaTypeView.videoDurationLabel.font = .systemFont(ofSize: 16) + mediaTypeView.videoImageView.image = R.image.conversation.ic_video_bold() + } + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + if let id = requestId { + PHCachingImageManager.default().cancelImageRequest(id) + } + } + + func load(asset: PHAsset, size: CGSize) { + if asset.mediaType == .video { + mediaTypeView.style = .video(duration: asset.duration) + } else { + if let uti = asset.uniformTypeIdentifier, UTTypeConformsTo(uti as CFString, kUTTypeGIF) { + mediaTypeView.style = .gif + } else { + mediaTypeView.style = .hidden + } + } + requestId = PHImageManager.default().requestImage(for: asset, targetSize: size * UIScreen.main.scale, contentMode: .aspectFill, options: imageRequestOptions) { [weak self] (image, info) in + self?.imageView.image = image + } + } + + func updateSelectedStatus(isSelected: Bool) { + selectedStatusImageView.image = isSelected ? R.image.ic_photo_checkmark() : R.image.ic_photo_unselected() + } + +} diff --git a/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib new file mode 100644 index 0000000000..31ea635d0e --- /dev/null +++ b/Mixin/UserInterface/Windows/Cells/MediaPreviewCell.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift new file mode 100644 index 0000000000..c84429516b --- /dev/null +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.swift @@ -0,0 +1,198 @@ +import UIKit +import Photos + +protocol SelectedPhotoInputItemsPreviewWindowDelegate: AnyObject { + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendItems assets: [PHAsset]) + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, didTapSendFiles assets: [PHAsset]) + func selectedPhotoInputItemsPreviewWindow(_ window: SelectedPhotoInputItemsPreviewWindow, willDismissWindow assets: [PHAsset]) +} + +final class SelectedPhotoInputItemsPreviewWindow: BottomSheetView { + + @IBOutlet weak var label: UILabel! + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var sendPhotoButton: RoundedButton! + @IBOutlet weak var sendFileButton: UIButton! + @IBOutlet weak var flowLayout: SnapCenterFlowLayout! + + @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint! + + weak var delegate: SelectedPhotoInputItemsPreviewWindowDelegate? + + private var assets = [PHAsset]() + private var selectedAssets = [PHAsset]() + private var lastWidth: CGFloat = 0 + private var isSending = false + private var cellSizeCache = [String: CGSize]() + + override func awakeFromNib() { + super.awakeFromNib() + sendFileButton.setTitleColor(.theme, for: .normal) + sendFileButton.setTitleColor(R.color.button_background_disabled(), for: .disabled) + collectionView.decelerationRate = .fast + collectionView.isPagingEnabled = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.allowsMultipleSelection = true + collectionView.register(R.nib.mediaPreviewCell) + } + + override func layoutSubviews() { + super.layoutSubviews() + let width = bounds.width + if lastWidth != width { + lastWidth = width + let inset = (width - collectionViewHeightConstraint.constant) / 2 + flowLayout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset) + } + } + + override func dismissPopupController(animated: Bool) { + if !isSending { + delegate?.selectedPhotoInputItemsPreviewWindow(self, willDismissWindow: selectedAssets) + } + super.dismissPopupController(animated: animated) + } + + @IBAction func closeAction(_ sender: Any) { + isSending = false + dismissPopupController(animated: true) + } + + @IBAction func sendPhotosAction(_ sender: Any) { + isSending = true + delegate?.selectedPhotoInputItemsPreviewWindow(self, didTapSendItems: selectedAssets) + dismissPopupController(animated: true) + } + + @IBAction func sendAsFilesAction(_ sender: Any) { + isSending = true + delegate?.selectedPhotoInputItemsPreviewWindow(self, didTapSendFiles: selectedAssets) + dismissPopupController(animated: true) + } + + func load(assets: [PHAsset], initIndex: Int) { + self.assets = assets + selectedAssets = assets + updateUI() + collectionView.reloadData() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.collectionView.scrollToItem(at: IndexPath(item: initIndex, section: 0), at: .centeredHorizontally, animated: false) + } + } + + class func instance() -> SelectedPhotoInputItemsPreviewWindow { + R.nib.selectedPhotoInputItemsPreviewWindow(owner: nil)! + } + +} + +extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + assets.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.media_preview, for: indexPath)! + if indexPath.item < assets.count { + let asset = assets[indexPath.item] + cell.load(asset: asset, size: cellSizeForItemAt(indexPath.item)) + cell.updateSelectedStatus(isSelected: selectedAssets.contains(asset)) + } + return cell + } + +} + +extension SelectedPhotoInputItemsPreviewWindow: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + cellSizeForItemAt(indexPath.item) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + guard let cell = collectionView.cellForItem(at: indexPath) as? MediaPreviewCell else { + return + } + let asset = assets[indexPath.item] + if let index = selectedAssets.firstIndex(of: asset) { + selectedAssets.remove(at: index) + cell.updateSelectedStatus(isSelected: false) + } else { + selectedAssets.append(asset) + cell.updateSelectedStatus(isSelected: true) + } + updateUI() + } + +} + +extension SelectedPhotoInputItemsPreviewWindow { + + private func updateUI() { + let title: String + let sendPhotoButtonTitle: String + let sendFileButtonTitle: String + let isEnabled: Bool + if selectedAssets.count == 0 { + title = R.string.localizable.no_items_selected() + sendPhotoButtonTitle = R.string.localizable.send_item_count(0) + sendFileButtonTitle = R.string.localizable.send_as_files() + isEnabled = false + } else if selectedAssets.count == 1 { + switch selectedAssets[0].mediaType { + case .image: + title = R.string.localizable.selected_photo() + sendPhotoButtonTitle = R.string.localizable.send_photo() + case .video: + title = R.string.localizable.selected_video() + sendPhotoButtonTitle = R.string.localizable.send_video() + default: + title = R.string.localizable.selected_item() + sendPhotoButtonTitle = R.string.localizable.send_item() + } + sendFileButtonTitle = R.string.localizable.send_as_file() + isEnabled = true + } else { + let count = selectedAssets.count + let isAllImages = selectedAssets.allSatisfy { $0.mediaType == .image } + let isAllVideos = selectedAssets.allSatisfy { $0.mediaType == .video } + if isAllImages { + title = R.string.localizable.selected_photo_count(count) + sendPhotoButtonTitle = R.string.localizable.send_photo_count(count) + } else if isAllVideos { + title = R.string.localizable.selected_video_count(count) + sendPhotoButtonTitle = R.string.localizable.send_video_count(count) + } else { + title = R.string.localizable.selected_item_count(count) + sendPhotoButtonTitle = R.string.localizable.send_item_count(count) + } + sendFileButtonTitle = R.string.localizable.send_as_files() + isEnabled = true + } + label.text = title + sendPhotoButton.setTitle(sendPhotoButtonTitle, for: .normal) + sendFileButton.setTitle(sendFileButtonTitle, for: .normal) + sendPhotoButton.isEnabled = isEnabled + sendFileButton.isEnabled = isEnabled + } + + private func cellSizeForItemAt(_ index: Int) -> CGSize { + guard index < assets.count else { + return .zero + } + let asset = assets[index] + if let size = cellSizeCache[asset.localIdentifier] { + return size + } else { + let height: CGFloat = 312 + let width: CGFloat = ceil(height / CGFloat(asset.pixelHeight) * CGFloat(asset.pixelWidth)) + let size = CGSize(width: min(312, max(width, 120)), height: height) + cellSizeCache[asset.localIdentifier] = size + return size + } + } + +} diff --git a/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib new file mode 100644 index 0000000000..1d85a1ebb1 --- /dev/null +++ b/Mixin/UserInterface/Windows/SelectedPhotoInputItemsPreviewWindow.xib @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +