diff --git a/Podfile b/Podfile index 070f1f1e2..f1aa82087 100644 --- a/Podfile +++ b/Podfile @@ -12,6 +12,7 @@ target 'Signal' do pod 'Reachability' pod 'SignalServiceKit', path: '.' pod 'SocketRocket', :git => 'https://github.com/facebook/SocketRocket.git' + pod 'YYImage' target 'SignalTests' do inherit! :search_paths end diff --git a/Podfile.lock b/Podfile.lock index b5c15e928..d144bb215 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -110,6 +110,9 @@ PODS: - YapDatabase/SQLCipher/Core - YapDatabase/SQLCipher/Extensions/Views (2.9.3): - YapDatabase/SQLCipher/Core + - YYImage (1.0.4): + - YYImage/Core (= 1.0.4) + - YYImage/Core (1.0.4) DEPENDENCIES: - ATAppUpdater @@ -120,6 +123,7 @@ DEPENDENCIES: - Reachability - SignalServiceKit (from `.`) - SocketRocket (from `https://github.com/facebook/SocketRocket.git`) + - YYImage EXTERNAL SOURCES: AxolotlKit: @@ -170,7 +174,8 @@ SPEC CHECKSUMS: TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d YapDatabase: cd911121580ff16675f65ad742a9eb0ab4d9e266 + YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54 -PODFILE CHECKSUM: 2f847bb25e70d1d376f38cf21ae08624fa6ed67d +PODFILE CHECKSUM: 00831faaa7677029090c311c00ceadaa44f65c0f COCOAPODS: 1.2.1 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8acc49725..c2ab286c5 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -91,6 +91,7 @@ 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; }; 34CE88EC1F3237260098030F /* OWSProfileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88EA1F3237260098030F /* OWSProfileManager.m */; }; 34CE88ED1F3237260098030F /* ProfileFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88EB1F3237260098030F /* ProfileFetcherJob.swift */; }; + 34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */; }; 34D5CC961EA6AFAD005515DB /* OWSContactsSyncing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */; }; 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; }; 34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCB01EAE7E7F005515DB /* SelectRecipientViewController.m */; }; @@ -549,6 +550,7 @@ 34CE88E91F3237260098030F /* OWSProfileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProfileManager.h; sourceTree = ""; }; 34CE88EA1F3237260098030F /* OWSProfileManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProfileManager.m; sourceTree = ""; }; 34CE88EB1F3237260098030F /* ProfileFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileFetcherJob.swift; sourceTree = ""; }; + 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerCell.swift; sourceTree = ""; }; 34D5CC941EA6AFAD005515DB /* OWSContactsSyncing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSyncing.h; sourceTree = ""; }; 34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSyncing.m; sourceTree = ""; }; 34D5CC981EA6EB79005515DB /* OWSMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageCollectionViewCell.h; sourceTree = ""; }; @@ -1132,6 +1134,7 @@ 34BECE2C1F7ABCE000D7438D /* GifPicker */ = { isa = PBXGroup; children = ( + 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */, 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */, 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */, ); @@ -2220,6 +2223,7 @@ 34B3F8741E8DF1700035BE1A /* AttachmentSharing.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, 45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */, + 34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */, 34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */, 34D9134B1F62D4A500722898 /* SignalAttachment.swift in Sources */, 34B3F88E1E8DF1700035BE1A /* PrivacySettingsTableViewController.m in Sources */, diff --git a/Signal/Images.xcassets/giphy_logo.imageset/Contents.json b/Signal/Images.xcassets/giphy_logo.imageset/Contents.json new file mode 100644 index 000000000..3220d7b54 --- /dev/null +++ b/Signal/Images.xcassets/giphy_logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "giphy_logo@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "giphy_logo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "giphy_logo@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@1x.png b/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@1x.png new file mode 100644 index 000000000..77c2b062d Binary files /dev/null and b/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@1x.png differ diff --git a/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@2x.png b/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@2x.png new file mode 100644 index 000000000..9c79f27ec Binary files /dev/null and b/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@2x.png differ diff --git a/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@3x.png b/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@3x.png new file mode 100644 index 000000000..6af136d61 Binary files /dev/null and b/Signal/Images.xcassets/giphy_logo.imageset/giphy_logo@3x.png differ diff --git a/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift b/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift new file mode 100644 index 000000000..fe3afe0e2 --- /dev/null +++ b/Signal/src/ViewControllers/GifPicker/GifPickerCell.swift @@ -0,0 +1,652 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +//import MediaPlayer + +class GifPickerCell: UICollectionViewCell + //, OWSAudioAttachmentPlayerDelegate +{ + let TAG = "[GifPickerCell]" + + // MARK: Properties + + var imageInfo: GiphyImageInfo? +// +// let searchBar: UISearchBar +// let layout: GifPickerLayout +// let collectionView: UICollectionView +// var logoImageView : UIImageView? +// +// var imageInfos = [GiphyImageInfo]() +// +// // let attachment: SignalAttachment +// // +// // var successCompletion : (() -> Void)? +// // +// // var videoPlayer: MPMoviePlayerController? +// // +// // var audioPlayer: OWSAudioAttachmentPlayer? +// // var audioStatusLabel: UILabel? +// // var audioPlayButton: UIButton? +// // var isAudioPlayingFlag = false +// // var isAudioPaused = false +// // var audioProgressSeconds: CGFloat = 0 +// // var audioDurationSeconds: CGFloat = 0 +// +// // MARK: Initializers +// +// @available(*, unavailable, message:"use attachment: constructor instead.") +// required init?(coder aDecoder: NSCoder) { +// self.searchBar = UISearchBar() +// self.layout = GifPickerLayout() +// self.collectionView = UICollectionView(frame:CGRect.zero, collectionViewLayout:self.layout) +// // self.attachment = SignalAttachment.empty() +// super.init(coder: aDecoder) +// owsFail("\(self.TAG) invalid constructor") +// } +// +// required init() { +// self.searchBar = UISearchBar() +// self.layout = GifPickerLayout() +// self.collectionView = UICollectionView(frame:CGRect.zero, collectionViewLayout:self.layout) +// // assert(!attachment.hasError) +// // self.attachment = attachment +// // self.successCompletion = successCompletion +// super.init(nibName: nil, bundle: nil) +// } + + override func prepareForReuse() { + super.prepareForReuse() + } +// +// // MARK: View Lifecycle +// +// override func viewDidLoad() { +// super.viewDidLoad() +// +// view.backgroundColor = UIColor.black +// +// self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem:.stop, +// target:self, +// action:#selector(donePressed)) +// self.navigationItem.title = NSLocalizedString("GIF_PICKER_VIEW_TITLE", +// comment: "Title for the 'gif picker' dialog.") +// +// createViews() +// } +// +// // MARK: Views +// +// private func createViews() { +// +// view.backgroundColor = UIColor.black +// +// // Search +//// searchBar.searchBarStyle = .minimal +// searchBar.searchBarStyle = .default +// searchBar.delegate = self +// searchBar.placeholder = NSLocalizedString("GIF_VIEW_SEARCH_PLACEHOLDER_TEXT", +// comment:"Placeholder text for the search field in gif view") +//// searchBar.backgroundColor = UIColor(white:0.6, alpha:1.0) +//// searchBar.backgroundColor = UIColor.white +//// searchBar.backgroundColor = UIColor.black +//// searchBar.barTintColor = UIColor.red +// searchBar.isTranslucent = false +//// searchBar.backgroundColor = UIColor.white +// searchBar.backgroundImage = UIImage(color:UIColor.clear) +// searchBar.barTintColor = UIColor.black +// searchBar.tintColor = UIColor.white +// self.view.addSubview(searchBar) +// searchBar.autoPinWidthToSuperview() +// searchBar.autoPin(toTopLayoutGuideOf: self, withInset:0) +// // [searchBar sizeToFit]; +// +// self.collectionView.delegate = self +// self.collectionView.dataSource = self +// self.collectionView.backgroundColor = UIColor.black +// self.view.addSubview(self.collectionView) +// self.collectionView.autoPinWidthToSuperview() +// self.collectionView.autoPinEdge(.top, to:.bottom, of:searchBar) +// self.collectionView.autoPin(toBottomLayoutGuideOf: self, withInset:0) +// +// let logoImage = UIImage(named:"giphy_logo") +// let logoImageView = UIImageView(image:logoImage) +// self.logoImageView = logoImageView +// self.view.addSubview(logoImageView) +// logoImageView.autoCenterInSuperview() +// +// self.updateContents() +// // [self updateTableContents]; +// } +// +// private func setContentVisible(_ isVisible:Bool) { +// self.collectionView.isHidden = !isVisible +// if let logoImageView = self.logoImageView { +// logoImageView.isHidden = isVisible +// } +// } +// +// private func updateContents() { +// if imageInfos.count < 1 { +// setContentVisible(false) +// } else { +// setContentVisible(true) +// } +// +// self.collectionView.collectionViewLayout.invalidateLayout() +// self.collectionView.reloadData() +// } +// +// // override func viewDidLoad() { +// // super.viewDidLoad() +// // +// // view.backgroundColor = UIColor.white +// // +// // self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem:.stop, +// // target:self, +// // action:#selector(donePressed)) +// // self.navigationItem.title = dialogTitle() +// // +// // createViews() +// // } +// // +// // private func dialogTitle() -> String { +// // guard let filename = formattedFileName() else { +// // return NSLocalizedString("ATTACHMENT_APPROVAL_DIALOG_TITLE", +// // comment: "Title for the 'attachment approval' dialog.") +// // } +// // return filename +// // } +// // +// // override func viewWillAppear(_ animated: Bool) { +// // super.viewWillAppear(animated) +// // +// // ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(true) +// // } +// // +// // override func viewWillDisappear(_ animated: Bool) { +// // super.viewWillDisappear(animated) +// // +// // ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(false) +// // } +// // +// // // MARK: - Create Views +// // +// // private func createViews() { +// // let previewTopMargin: CGFloat = 30 +// // let previewHMargin: CGFloat = 20 +// // +// // let attachmentPreviewView = UIView() +// // self.view.addSubview(attachmentPreviewView) +// // attachmentPreviewView.autoPinWidthToSuperview(withMargin:previewHMargin) +// // attachmentPreviewView.autoPin(toTopLayoutGuideOf: self, withInset:previewTopMargin) +// // +// // createButtonRow(attachmentPreviewView:attachmentPreviewView) +// // +// // if attachment.isAnimatedImage { +// // createAnimatedPreview(attachmentPreviewView:attachmentPreviewView) +// // } else if attachment.isImage { +// // createImagePreview(attachmentPreviewView:attachmentPreviewView) +// // } else if attachment.isVideo { +// // createVideoPreview(attachmentPreviewView:attachmentPreviewView) +// // } else if attachment.isAudio { +// // createAudioPreview(attachmentPreviewView:attachmentPreviewView) +// // } else { +// // createGenericPreview(attachmentPreviewView:attachmentPreviewView) +// // } +// // } +// // +// // private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { +// // assert(subviews.count > 0) +// // +// // let stackView = UIView() +// // +// // var lastView: UIView? +// // for subview in subviews { +// // +// // stackView.addSubview(subview) +// // subview.autoHCenterInSuperview() +// // +// // if lastView == nil { +// // subview.autoPinEdge(toSuperviewEdge:.top) +// // } else { +// // subview.autoPinEdge(.top, to:.bottom, of:lastView!, withOffset:10) +// // } +// // +// // lastView = subview +// // } +// // +// // lastView?.autoPinEdge(toSuperviewEdge:.bottom) +// // +// // return stackView +// // } +// // +// // private func createAudioPreview(attachmentPreviewView: UIView) { +// // guard let dataUrl = attachment.dataUrl else { +// // createGenericPreview(attachmentPreviewView:attachmentPreviewView) +// // return +// // } +// // +// // audioPlayer = OWSAudioAttachmentPlayer(mediaUrl: dataUrl, delegate: self) +// // +// // var subviews = [UIView]() +// // +// // let audioPlayButton = UIButton() +// // self.audioPlayButton = audioPlayButton +// // setAudioIconToPlay() +// // audioPlayButton.imageView?.layer.minificationFilter = kCAFilterTrilinear +// // audioPlayButton.imageView?.layer.magnificationFilter = kCAFilterTrilinear +// // audioPlayButton.addTarget(self, action:#selector(audioPlayButtonPressed), for:.touchUpInside) +// // let buttonSize = createHeroViewSize() +// // audioPlayButton.autoSetDimension(.width, toSize:buttonSize) +// // audioPlayButton.autoSetDimension(.height, toSize:buttonSize) +// // subviews.append(audioPlayButton) +// // +// // let fileNameLabel = createFileNameLabel() +// // if let fileNameLabel = fileNameLabel { +// // subviews.append(fileNameLabel) +// // } +// // +// // let fileSizeLabel = createFileSizeLabel() +// // subviews.append(fileSizeLabel) +// // +// // let audioStatusLabel = createAudioStatusLabel() +// // self.audioStatusLabel = audioStatusLabel +// // updateAudioStatusLabel() +// // subviews.append(audioStatusLabel) +// // +// // let stackView = wrapViewsInVerticalStack(subviews:subviews) +// // attachmentPreviewView.addSubview(stackView) +// // fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) +// // stackView.autoPinWidthToSuperview() +// // stackView.autoVCenterInSuperview() +// // } +// // +// // private func createAnimatedPreview(attachmentPreviewView: UIView) { +// // guard attachment.isValidImage else { +// // return +// // } +// // let data = attachment.data +// // // Use Flipboard FLAnimatedImage library to display gifs +// // guard let animatedImage = FLAnimatedImage(gifData:data) else { +// // createGenericPreview(attachmentPreviewView:attachmentPreviewView) +// // return +// // } +// // let animatedImageView = FLAnimatedImageView() +// // animatedImageView.animatedImage = animatedImage +// // animatedImageView.contentMode = .scaleAspectFit +// // attachmentPreviewView.addSubview(animatedImageView) +// // animatedImageView.autoPinWidthToSuperview() +// // animatedImageView.autoPinHeightToSuperview() +// // } +// // +// // private func createImagePreview(attachmentPreviewView: UIView) { +// // var image = attachment.image +// // if image == nil { +// // image = UIImage(data:attachment.data) +// // } +// // guard image != nil else { +// // createGenericPreview(attachmentPreviewView:attachmentPreviewView) +// // return +// // } +// // +// // let imageView = UIImageView(image:image) +// // imageView.layer.minificationFilter = kCAFilterTrilinear +// // imageView.layer.magnificationFilter = kCAFilterTrilinear +// // imageView.contentMode = .scaleAspectFit +// // attachmentPreviewView.addSubview(imageView) +// // imageView.autoPinWidthToSuperview() +// // imageView.autoPinHeightToSuperview() +// // } +// // +// // private func createVideoPreview(attachmentPreviewView: UIView) { +// // guard let dataUrl = attachment.dataUrl else { +// // createGenericPreview(attachmentPreviewView:attachmentPreviewView) +// // return +// // } +// // guard let videoPlayer = MPMoviePlayerController(contentURL:dataUrl) else { +// // createGenericPreview(attachmentPreviewView:attachmentPreviewView) +// // return +// // } +// // videoPlayer.prepareToPlay() +// // +// // videoPlayer.controlStyle = .default +// // videoPlayer.shouldAutoplay = false +// // +// // attachmentPreviewView.addSubview(videoPlayer.view) +// // self.videoPlayer = videoPlayer +// // videoPlayer.view.autoPinWidthToSuperview() +// // videoPlayer.view.autoPinHeightToSuperview() +// // } +// // +// // private func createGenericPreview(attachmentPreviewView: UIView) { +// // var subviews = [UIView]() +// // +// // let imageView = createHeroImageView(imageName: "file-thin-black-filled-large") +// // subviews.append(imageView) +// // +// // let fileNameLabel = createFileNameLabel() +// // if let fileNameLabel = fileNameLabel { +// // subviews.append(fileNameLabel) +// // } +// // +// // let fileSizeLabel = createFileSizeLabel() +// // subviews.append(fileSizeLabel) +// // +// // let stackView = wrapViewsInVerticalStack(subviews:subviews) +// // attachmentPreviewView.addSubview(stackView) +// // fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) +// // stackView.autoPinWidthToSuperview() +// // stackView.autoVCenterInSuperview() +// // } +// // +// // private func createHeroViewSize() -> CGFloat { +// // return ScaleFromIPhone5To7Plus(175, 225) +// // } +// // +// // private func createHeroImageView(imageName: String) -> UIView { +// // let imageSize = createHeroViewSize() +// // let image = UIImage(named:imageName) +// // assert(image != nil) +// // let imageView = UIImageView(image:image) +// // imageView.layer.minificationFilter = kCAFilterTrilinear +// // imageView.layer.magnificationFilter = kCAFilterTrilinear +// // imageView.layer.shadowColor = UIColor.black.cgColor +// // let shadowScaling = 5.0 +// // imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling) +// // imageView.layer.shadowOpacity = 0.25 +// // imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling) +// // imageView.autoSetDimension(.width, toSize:imageSize) +// // imageView.autoSetDimension(.height, toSize:imageSize) +// // +// // return imageView +// // } +// // +// // private func labelFont() -> UIFont { +// // return UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(18, 24)) +// // } +// // +// // private func formattedFileExtension() -> String? { +// // guard let fileExtension = attachment.fileExtension else { +// // return nil +// // } +// // +// // return String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT", +// // comment: "Format string for file extension label in call interstitial view"), +// // fileExtension.uppercased()) +// // } +// // +// // private func formattedFileName() -> String? { +// // guard let sourceFilename = attachment.sourceFilename else { +// // return nil +// // } +// // let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) +// // guard filename.characters.count > 0 else { +// // return nil +// // } +// // return filename +// // } +// // +// // private func createFileNameLabel() -> UIView? { +// // let filename = formattedFileName() ?? formattedFileExtension() +// // +// // guard filename != nil else { +// // return nil +// // } +// // +// // let label = UILabel() +// // label.text = filename +// // label.textColor = UIColor.ows_materialBlue() +// // label.font = labelFont() +// // label.textAlignment = .center +// // label.lineBreakMode = .byTruncatingMiddle +// // return label +// // } +// // +// // private func createFileSizeLabel() -> UIView { +// // let label = UILabel() +// // let fileSize = attachment.dataLength +// // label.text = String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT", +// // comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}."), +// // ViewControllerUtils.formatFileSize(UInt(fileSize))) +// // +// // label.textColor = UIColor.ows_materialBlue() +// // label.font = labelFont() +// // label.textAlignment = .center +// // +// // return label +// // } +// // +// // private func createAudioStatusLabel() -> UILabel { +// // let label = UILabel() +// // label.textColor = UIColor.ows_materialBlue() +// // label.font = labelFont() +// // label.textAlignment = .center +// // +// // return label +// // } +// // +// // private func createButtonRow(attachmentPreviewView: UIView) { +// // let buttonTopMargin = ScaleFromIPhone5To7Plus(30, 40) +// // let buttonBottomMargin = ScaleFromIPhone5To7Plus(25, 40) +// // let buttonHSpacing = ScaleFromIPhone5To7Plus(20, 30) +// // +// // let buttonRow = UIView() +// // self.view.addSubview(buttonRow) +// // buttonRow.autoPinWidthToSuperview() +// // buttonRow.autoPinEdge(toSuperviewEdge:.bottom, withInset:buttonBottomMargin) +// // buttonRow.autoPinEdge(.top, to:.bottom, of:attachmentPreviewView, withOffset:buttonTopMargin) +// // +// // // We use this invisible subview to ensure that the buttons are centered +// // // horizontally. +// // let buttonSpacer = UIView() +// // buttonRow.addSubview(buttonSpacer) +// // // Vertical positioning of this view doesn't matter. +// // buttonSpacer.autoPinEdge(toSuperviewEdge:.top) +// // buttonSpacer.autoSetDimension(.width, toSize:buttonHSpacing) +// // buttonSpacer.autoHCenterInSuperview() +// // +// // let cancelButton = createButton(title: CommonStrings.cancelButton, +// // color : UIColor.ows_destructiveRed(), +// // action: #selector(cancelPressed)) +// // buttonRow.addSubview(cancelButton) +// // cancelButton.autoPinEdge(toSuperviewEdge:.top) +// // cancelButton.autoPinEdge(toSuperviewEdge:.bottom) +// // cancelButton.autoPinEdge(.right, to:.left, of:buttonSpacer) +// // +// // let sendButton = createButton(title: NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON", +// // comment: "Label for 'send' button in the 'attachment approval' dialog."), +// // color : UIColor(rgbHex:0x2ecc71), +// // action: #selector(sendPressed)) +// // buttonRow.addSubview(sendButton) +// // sendButton.autoPinEdge(toSuperviewEdge:.top) +// // sendButton.autoPinEdge(toSuperviewEdge:.bottom) +// // sendButton.autoPinEdge(.left, to:.right, of:buttonSpacer) +// // } +// // +// // private func createButton(title: String, color: UIColor, action: Selector) -> UIView { +// // let buttonWidth = ScaleFromIPhone5To7Plus(110, 140) +// // let buttonHeight = ScaleFromIPhone5To7Plus(35, 45) +// // +// // return OWSFlatButton.button(title:title, +// // titleColor:UIColor.white, +// // backgroundColor:color, +// // width:buttonWidth, +// // height:buttonHeight, +// // target:target, +// // selector:action) +// // } +// // +// // // MARK: - Event Handlers +// // +// // func donePressed(sender: UIButton) { +// // dismiss(animated: true, completion:nil) +// // } +// // +// // func cancelPressed(sender: UIButton) { +// // dismiss(animated: true, completion:nil) +// // } +// // +// // func sendPressed(sender: UIButton) { +// // let successCompletion = self.successCompletion +// // dismiss(animated: true, completion: { +// // successCompletion?() +// // }) +// // } +// // +// // func audioPlayButtonPressed(sender: UIButton) { +// // audioPlayer?.togglePlayState() +// // } +// // +// // // MARK: - OWSAudioAttachmentPlayerDelegate +// // +// // public func isAudioPlaying() -> Bool { +// // return isAudioPlayingFlag +// // } +// // +// // public func setIsAudioPlaying(_ isAudioPlaying: Bool) { +// // isAudioPlayingFlag = isAudioPlaying +// // +// // updateAudioStatusLabel() +// // } +// // +// // public func isPaused() -> Bool { +// // return isAudioPaused +// // } +// // +// // public func setIsPaused(_ isPaused: Bool) { +// // isAudioPaused = isPaused +// // } +// // +// // public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) { +// // audioProgressSeconds = progress +// // audioDurationSeconds = duration +// // +// // updateAudioStatusLabel() +// // } +// // +// // private func updateAudioStatusLabel() { +// // guard let audioStatusLabel = self.audioStatusLabel else { +// // owsFail("Missing audio status label") +// // return +// // } +// // +// // if isAudioPlayingFlag && audioProgressSeconds > 0 && audioDurationSeconds > 0 { +// // audioStatusLabel.text = String(format:"%@ / %@", +// // ViewControllerUtils.formatDurationSeconds(Int(round(self.audioProgressSeconds))), +// // ViewControllerUtils.formatDurationSeconds(Int(round(self.audioDurationSeconds)))) +// // } else { +// // audioStatusLabel.text = " " +// // } +// // } +// // +// // public func setAudioIconToPlay() { +// // let image = UIImage(named:"audio_play_black_large")?.withRenderingMode(.alwaysTemplate) +// // assert(image != nil) +// // audioPlayButton?.setImage(image, for:.normal) +// // audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue() +// // } +// // +// // public func setAudioIconToPause() { +// // let image = UIImage(named:"audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) +// // assert(image != nil) +// // audioPlayButton?.setImage(image, for:.normal) +// // audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue() +// // } +// +// // MARK: - UICollectionViewDataSource +// +// override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { +// return imageInfos.count +// } +// +// // The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: +// override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { +// let cell = self.dequeueReusableCell(withReuseIdentifier identifier: String, for indexPath: IndexPath) -> UICollectionViewCell +// } +// +// +// // MARK: - UICollectionViewDelegate +// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool // called when the user taps on an already-selected item in multi-select mode +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) +//// +//// +//// @available(iOS 8.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) +//// +//// @available(iOS 8.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) +//// +//// +//// // These methods provide support for copy/paste actions on cells. +//// // All three should be implemented if any are. +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool +//// +//// @available(iOS 6.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) +//// +//// +//// // support for custom transition layout +//// @available(iOS 7.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, newLayout toLayout: UICollectionViewLayout) -> UICollectionViewTransitionLayout +//// +//// +//// // Focus +//// @available(iOS 9.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool +//// +//// @available(iOS 9.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool +//// +//// @available(iOS 9.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) +//// +//// @available(iOS 9.0, *) +//// optional public func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? +//// +//// +//// @available(iOS 9.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath +//// +//// +//// @available(iOS 9.0, *) +//// optional public func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint // customize the content offset to be applied during transition or update animations +////} +// +// // MARK: - Event Handlers +// +// func donePressed(sender: UIButton) { +// dismiss(animated: true, completion:nil) +// } +} diff --git a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift index 7549a5f5e..549116ba8 100644 --- a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift +++ b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift @@ -5,9 +5,7 @@ import Foundation //import MediaPlayer -class GifPickerViewController: OWSViewController, UISearchBarDelegate - //, OWSAudioAttachmentPlayerDelegate -{ +class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollectionViewDataSource, UICollectionViewDelegate { let TAG = "[GifPickerViewController]" // MARK: Properties @@ -15,20 +13,11 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate let searchBar: UISearchBar let layout: GifPickerLayout let collectionView: UICollectionView + var logoImageView: UIImageView? - // let attachment: SignalAttachment - // - // var successCompletion : (() -> Void)? - // - // var videoPlayer: MPMoviePlayerController? - // - // var audioPlayer: OWSAudioAttachmentPlayer? - // var audioStatusLabel: UILabel? - // var audioPlayButton: UIButton? - // var isAudioPlayingFlag = false - // var isAudioPaused = false - // var audioProgressSeconds: CGFloat = 0 - // var audioDurationSeconds: CGFloat = 0 + var imageInfos = [GiphyImageInfo]() + + private let kCellReuseIdentifier = "kCellReuseIdentifier" // MARK: Initializers @@ -57,7 +46,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = UIColor.white + view.backgroundColor = UIColor.black self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem:.stop, target:self, @@ -68,37 +57,76 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate createViews() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + search(query:"funny") + +// self.view.layoutSubviews() +// updateImageLayout() + } + // MARK: Views private func createViews() { - // @property (nonatomic, readonly) UISearchBar *searchBar; - view.backgroundColor = UIColor.white + view.backgroundColor = UIColor.black // Search - searchBar.searchBarStyle = .minimal +// searchBar.searchBarStyle = .minimal + searchBar.searchBarStyle = .default searchBar.delegate = self searchBar.placeholder = NSLocalizedString("GIF_VIEW_SEARCH_PLACEHOLDER_TEXT", comment:"Placeholder text for the search field in gif view") - searchBar.backgroundColor = UIColor.white +// searchBar.backgroundColor = UIColor(white:0.6, alpha:1.0) +// searchBar.backgroundColor = UIColor.white +// searchBar.backgroundColor = UIColor.black +// searchBar.barTintColor = UIColor.red + searchBar.isTranslucent = false +// searchBar.backgroundColor = UIColor.white + searchBar.backgroundImage = UIImage(color:UIColor.clear) + searchBar.barTintColor = UIColor.black + searchBar.tintColor = UIColor.white self.view.addSubview(searchBar) searchBar.autoPinWidthToSuperview() searchBar.autoPin(toTopLayoutGuideOf: self, withInset:0) // [searchBar sizeToFit]; -// self.collectionView.delegate = self -// self.collectionView.dataSource = self + self.collectionView.delegate = self + self.collectionView.dataSource = self + self.collectionView.backgroundColor = UIColor.black + self.collectionView.register(GifPickerCell.self, forCellWithReuseIdentifier: kCellReuseIdentifier) self.view.addSubview(self.collectionView) self.collectionView.autoPinWidthToSuperview() self.collectionView.autoPinEdge(.top, to:.bottom, of:searchBar) self.collectionView.autoPin(toBottomLayoutGuideOf: self, withInset:0) + let logoImage = UIImage(named:"giphy_logo") + let logoImageView = UIImageView(image:logoImage) + self.logoImageView = logoImageView + self.view.addSubview(logoImageView) + logoImageView.autoCenterInSuperview() + self.updateContents() // [self updateTableContents]; } - private func updateContents() { + private func setContentVisible(_ isVisible: Bool) { + self.collectionView.isHidden = !isVisible + if let logoImageView = self.logoImageView { + logoImageView.isHidden = isVisible + } + } + private func updateContents() { + if imageInfos.count < 1 { + setContentVisible(false) + } else { + setContentVisible(true) + } + + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.reloadData() } // override func viewDidLoad() { @@ -457,70 +485,66 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate // successCompletion?() // }) // } - // - // func audioPlayButtonPressed(sender: UIButton) { - // audioPlayer?.togglePlayState() - // } - // - // // MARK: - OWSAudioAttachmentPlayerDelegate - // - // public func isAudioPlaying() -> Bool { - // return isAudioPlayingFlag - // } - // - // public func setIsAudioPlaying(_ isAudioPlaying: Bool) { - // isAudioPlayingFlag = isAudioPlaying - // - // updateAudioStatusLabel() - // } - // - // public func isPaused() -> Bool { - // return isAudioPaused - // } - // - // public func setIsPaused(_ isPaused: Bool) { - // isAudioPaused = isPaused - // } - // - // public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) { - // audioProgressSeconds = progress - // audioDurationSeconds = duration - // - // updateAudioStatusLabel() - // } - // - // private func updateAudioStatusLabel() { - // guard let audioStatusLabel = self.audioStatusLabel else { - // owsFail("Missing audio status label") - // return - // } - // - // if isAudioPlayingFlag && audioProgressSeconds > 0 && audioDurationSeconds > 0 { - // audioStatusLabel.text = String(format:"%@ / %@", - // ViewControllerUtils.formatDurationSeconds(Int(round(self.audioProgressSeconds))), - // ViewControllerUtils.formatDurationSeconds(Int(round(self.audioDurationSeconds)))) - // } else { - // audioStatusLabel.text = " " - // } - // } - // - // public func setAudioIconToPlay() { - // let image = UIImage(named:"audio_play_black_large")?.withRenderingMode(.alwaysTemplate) - // assert(image != nil) - // audioPlayButton?.setImage(image, for:.normal) - // audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue() - // } - // - // public func setAudioIconToPause() { - // let image = UIImage(named:"audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) - // assert(image != nil) - // audioPlayButton?.setImage(image, for:.normal) - // audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue() - // } + + // MARK: - UICollectionViewDataSource + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return imageInfos.count + } + + // The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath: + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let imageInfo = imageInfos[indexPath.row] + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier:kCellReuseIdentifier, for: indexPath) as! GifPickerCell + cell.imageInfo = imageInfo + return cell + } + + // MARK: - UICollectionViewDelegate + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let imageInfo = imageInfos[indexPath.row] + } + + public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + + } + + public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + + } // MARK: - Event Handlers func donePressed(sender: UIButton) { dismiss(animated: true, completion:nil) } + + // MARK: - UISearchBarDelegate + + public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + // TODO: We could do progressive search as the user types. + } + + public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + guard let text = searchBar.text else { + // TODO: Alert? + return + } + search(query:text) + } + + private func search(query: String) { + GifManager.sharedInstance.search(query: query, success: { [weak self] imageInfos in + guard let strongSelf = self else { return } + Logger.info("\(strongSelf.TAG) search complete") + strongSelf.imageInfos = imageInfos + strongSelf.updateContents() + }, + failure: { [weak self] in + guard let strongSelf = self else { return } + Logger.info("\(strongSelf.TAG) search failed.") + }) + } } diff --git a/Signal/src/network/GifManager.swift b/Signal/src/network/GifManager.swift index ce8b127f3..9f2c36635 100644 --- a/Signal/src/network/GifManager.swift +++ b/Signal/src/network/GifManager.swift @@ -40,6 +40,42 @@ enum GiphyFormat { self.giphyId = giphyId self.renditions = renditions } + + let kMaxDimension = UInt(618) + let kMinDimension = UInt(101) + let kMaxFileSize = SignalAttachment.kMaxFileSizeAnimatedImage + + public func pickGifRendition() -> GiphyRendition? { + var bestRendition: GiphyRendition? + + for rendition in renditions { + guard rendition.format == .gif else { + continue + } + guard !rendition.name.hasSuffix("_still") + else { + continue + } + guard rendition.width >= kMinDimension && + rendition.width <= kMaxDimension && + rendition.height >= kMinDimension && + rendition.height <= kMaxDimension && + rendition.fileSize <= kMaxFileSize + else { + continue + } + + if let currentBestRendition = bestRendition { + if rendition.width > currentBestRendition.width { + bestRendition = rendition + } + } else { + bestRendition = rendition + } + } + + return bestRendition + } } @objc class GifManager: NSObject { @@ -81,13 +117,23 @@ enum GiphyFormat { return sessionManager } + // TODO: public func test() { + search(query:"monkey", + success: { _ in + }, failure: { + }) + } + + public func search(query: String, success: @escaping (([GiphyImageInfo]) -> Void), failure: @escaping (() -> Void)) { guard let sessionManager = giphySessionManager() else { Logger.error("\(GifManager.TAG) Couldn't create session manager.") + failure() return } guard NSURL(string:kGiphyBaseURL) != nil else { Logger.error("\(GifManager.TAG) Invalid base URL.") + failure() return } @@ -96,27 +142,27 @@ enum GiphyFormat { let kGiphyPageSize = 200 // TODO: let kGiphyPageOffset = 0 - // TODO: - let query = "monkey" - // TODO: guard let queryEncoded = query.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { Logger.error("\(GifManager.TAG) Could not URL encode query: \(query).") + failure() return } - // Logger.error("\(GifManager.TAG) queryEncoded: \(queryEncoded) \(queryEncoded).") let urlString = "/v1/gifs/search?api_key=\(kGiphyApiKey)&offset=\(kGiphyPageOffset)&limit=\(kGiphyPageSize)&q=\(queryEncoded)" - // Logger.error("\(GifManager.TAG) urlString: \(urlString).") - // Logger.error("\(GifManager.TAG) baseUrl: \(baseUrl).") sessionManager.get(urlString, parameters: {}, progress:nil, success: { _, value in Logger.error("\(GifManager.TAG) search request succeeded") - self.parseGiphyImages(responseJson:value) + guard let imageInfos = self.parseGiphyImages(responseJson:value) else { + failure() + return + } + success(imageInfos) }, failure: { _, error in Logger.error("\(GifManager.TAG) search request failed: \(error)") + failure() }) } @@ -172,7 +218,7 @@ enum GiphyFormat { Logger.warn("\(GifManager.TAG) Image has no valid renditions.") return nil } - Logger.debug("\(GifManager.TAG) Image successfully parsed.") +// Logger.debug("\(GifManager.TAG) Image successfully parsed.") return GiphyImageInfo(giphyId : giphyId, renditions : renditions) } @@ -204,13 +250,12 @@ enum GiphyFormat { Logger.warn("\(GifManager.TAG) Rendition url missing file extension.") return nil } - Logger.error("\(GifManager.TAG) fileExtension: \(fileExtension).") guard fileExtension.lowercased() == "gif" else { - Logger.debug("\(GifManager.TAG) Rendition has invalid type: \(fileExtension).") +// Logger.verbose("\(GifManager.TAG) Rendition has invalid type: \(fileExtension).") return nil } - Logger.debug("\(GifManager.TAG) Rendition successfully parsed.") +// Logger.debug("\(GifManager.TAG) Rendition successfully parsed.") return GiphyRendition( format : .gif, name : renditionName, @@ -221,6 +266,8 @@ enum GiphyFormat { ) } + // Giphy API results are often incompl + // // { // height = 65; // mp4 = "https://media3.giphy.com/media/42YlR8u9gV5Cw/100w.mp4"; @@ -233,19 +280,19 @@ enum GiphyFormat { // } private func parsePositiveUInt(dict: [String:Any], key: String, typeName: String) -> UInt? { guard let value = dict[key] else { - Logger.debug("\(GifManager.TAG) \(typeName) missing \(key).") +// Logger.verbose("\(GifManager.TAG) \(typeName) missing \(key).") return nil } guard let stringValue = value as? String else { - Logger.warn("\(GifManager.TAG) \(typeName) has invalid \(key): \(value).") +// Logger.verbose("\(GifManager.TAG) \(typeName) has invalid \(key): \(value).") return nil } guard let parsedValue = UInt(stringValue) else { - Logger.warn("\(GifManager.TAG) \(typeName) has invalid \(key): \(stringValue).") +// Logger.verbose("\(GifManager.TAG) \(typeName) has invalid \(key): \(stringValue).") return nil } guard parsedValue > 0 else { - Logger.debug("\(GifManager.TAG) \(typeName) has non-positive \(key): \(parsedValue).") + Logger.verbose("\(GifManager.TAG) \(typeName) has non-positive \(key): \(parsedValue).") return nil } return parsedValue