Merge branch 'mkirk/batch-delete'

pull/1/head
Michael Kirk 8 years ago
commit a30ef6a448

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "BlueCheckSelected_31x31_@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "BlueCheckSelected_31x31_@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "BlueCheckSelected_31x31_@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

@ -176,11 +176,11 @@ protocol MediaGalleryDataSource: class {
func showAllMedia(focusedItem: MediaGalleryItem) func showAllMedia(focusedItem: MediaGalleryItem)
func dismissMediaDetailViewController(_ mediaDetailViewController: MediaPageViewController, animated isAnimated: Bool, completion: (() -> Void)?) func dismissMediaDetailViewController(_ mediaDetailViewController: MediaPageViewController, animated isAnimated: Bool, completion: (() -> Void)?)
func delete(message: TSMessage) func delete(items: [MediaGalleryItem])
} }
protocol MediaGalleryDataSourceDelegate: class { protocol MediaGalleryDataSourceDelegate: class {
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete message: TSMessage) func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem])
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath]) func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath])
} }
@ -725,25 +725,32 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
weak var dataSourceDelegate: MediaGalleryDataSourceDelegate? weak var dataSourceDelegate: MediaGalleryDataSourceDelegate?
var deletedMessages: Set<TSMessage> = Set() var deletedMessages: Set<TSMessage> = Set()
func delete(message: TSMessage) {
Logger.info("\(logTag) in \(#function) with message: \(String(describing: message.uniqueId)) attachmentId: \(String(describing: message.attachmentIds.firstObject))")
self.dataSourceDelegate?.mediaGalleryDataSource(self, willDelete: message) func delete(items: [MediaGalleryItem]) {
AssertIsOnMainThread()
Logger.info("\(logTag) in \(#function) with items: \(items)")
dataSourceDelegate?.mediaGalleryDataSource(self, willDelete: items)
self.editingDatabaseConnection.asyncReadWrite { transaction in self.editingDatabaseConnection.asyncReadWrite { transaction in
for item in items {
let message = item.message
message.remove(with: transaction) message.remove(with: transaction)
}
self.deletedMessages.insert(message) self.deletedMessages.insert(message)
}
}
var deletedSections: IndexSet = IndexSet() var deletedSections: IndexSet = IndexSet()
var deletedIndexPaths: [IndexPath] = [] var deletedIndexPaths: [IndexPath] = []
let originalSections = self.sections
let originalSectionDates = self.sectionDates
guard let itemIndex = galleryItems.index(where: { $0.message == message }) else { for item in items {
guard let itemIndex = galleryItems.index(of: item) else {
owsFail("\(logTag) in \(#function) removing unknown item.") owsFail("\(logTag) in \(#function) removing unknown item.")
return return
} }
let item: MediaGalleryItem = galleryItems[itemIndex]
self.galleryItems.remove(at: itemIndex) self.galleryItems.remove(at: itemIndex)
@ -757,26 +764,43 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
return return
} }
guard let sectionRowIndex = sectionItems.index(of: item) else {
owsFail("\(logTag) in \(#function) item with unknown sectionRowIndex")
return
}
// We need to calculate the index of the deleted item with respect to it's original position.
guard let originalSectionIndex = originalSectionDates.index(where: { $0 == item.galleryDate }) else {
owsFail("\(logTag) in \(#function) item with unknown date.")
return
}
guard let originalSectionItems = originalSections[item.galleryDate] else {
owsFail("\(logTag) in \(#function) item with unknown section")
return
}
guard let originalSectionRowIndex = originalSectionItems.index(of: item) else {
owsFail("\(logTag) in \(#function) item with unknown sectionRowIndex")
return
}
if sectionItems == [item] { if sectionItems == [item] {
// Last item in section. Delete section. // Last item in section. Delete section.
self.sections[item.galleryDate] = nil self.sections[item.galleryDate] = nil
self.sectionDates.remove(at: sectionIndex) self.sectionDates.remove(at: sectionIndex)
deletedSections.insert(sectionIndex + 1) deletedSections.insert(originalSectionIndex + 1)
deletedIndexPaths.append(IndexPath(row: 0, section: sectionIndex + 1)) deletedIndexPaths.append(IndexPath(row: originalSectionRowIndex, section: originalSectionIndex + 1))
} else { } else {
guard let sectionRowIndex = sectionItems.index(of: item) else {
owsFail("\(logTag) in \(#function) item with unknown sectionRowIndex")
return
}
sectionItems.remove(at: sectionRowIndex) sectionItems.remove(at: sectionRowIndex)
self.sections[item.galleryDate] = sectionItems self.sections[item.galleryDate] = sectionItems
deletedIndexPaths.append(IndexPath(row: sectionRowIndex, section: sectionIndex + 1)) deletedIndexPaths.append(IndexPath(row: originalSectionRowIndex, section: originalSectionIndex + 1))
}
} }
self.dataSourceDelegate?.mediaGalleryDataSource(self, deletedSections: deletedSections, deletedItems: deletedIndexPaths) dataSourceDelegate?.mediaGalleryDataSource(self, deletedSections: deletedSections, deletedItems: deletedIndexPaths)
} }
let kGallerySwipeLoadBatchSize: UInt = 5 let kGallerySwipeLoadBatchSize: UInt = 5

@ -59,6 +59,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
self.updateTitle(item: item) self.updateTitle(item: item)
self.setViewControllers([galleryPage], direction: direction, animated: isAnimated) self.setViewControllers([galleryPage], direction: direction, animated: isAnimated)
self.updateFooterBarButtonItems(isPlayingVideo: false)
} }
private let uiDatabaseConnection: YapDatabaseConnection private let uiDatabaseConnection: YapDatabaseConnection
@ -327,7 +328,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
// else we deleted the last piece of media, return to the conversation view // else we deleted the last piece of media, return to the conversation view
self.dismissSelf(animated: true) self.dismissSelf(animated: true)
} }
mediaGalleryDataSource.delete(message: deletedItem.message) mediaGalleryDataSource.delete(items: [deletedItem])
} }
actionSheet.addAction(OWSAlerts.cancelAction) actionSheet.addAction(OWSAlerts.cancelAction)
actionSheet.addAction(deleteAction) actionSheet.addAction(deleteAction)
@ -502,8 +503,15 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
return return
} }
guard let galleryItem = self.mediaGalleryDataSource?.galleryItems.first(where: { $0.message == message }) else {
owsFail("\(logTag) in \(#function) unexpected interaction: \(type(of: conversationViewItem))")
self.presentingViewController?.dismiss(animated: true)
return
}
dismissSelf(animated: true) { dismissSelf(animated: true) {
mediaGalleryDataSource.delete(message: message) mediaGalleryDataSource.delete(items: [galleryItem])
} }
} }

@ -8,7 +8,7 @@ public protocol MediaTileViewControllerDelegate: class {
func mediaTileViewController(_ viewController: MediaTileViewController, didTapView tappedView: UIView, mediaGalleryItem: MediaGalleryItem) func mediaTileViewController(_ viewController: MediaTileViewController, didTapView tappedView: UIView, mediaGalleryItem: MediaGalleryItem)
} }
public class MediaTileViewController: UICollectionViewController, MediaGalleryCellDelegate, MediaGalleryDataSourceDelegate { public class MediaTileViewController: UICollectionViewController, MediaGalleryDataSourceDelegate {
private weak var mediaGalleryDataSource: MediaGalleryDataSource? private weak var mediaGalleryDataSource: MediaGalleryDataSource?
@ -88,10 +88,30 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
collectionView.delegate = self collectionView.delegate = self
// TODO iPhoneX
// feels a bit weird to have content smashed all the way to the bottom edge. // feels a bit weird to have content smashed all the way to the bottom edge.
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)
let footerBar = UIToolbar()
self.footerBar = footerBar
let deleteButton = UIBarButtonItem(barButtonSystemItem: .trash,
target:self,
action:#selector(didPressDelete))
self.deleteButton = deleteButton
let footerItems = [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target:nil, action:nil),
deleteButton,
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target:nil, action:nil),
]
footerBar.setItems(footerItems, animated: false)
self.view.addSubview(self.footerBar)
footerBar.barTintColor = UIColor.ows_signalBrandBlue
footerBar.autoPinWidthToSuperview()
footerBar.autoSetDimension(.height, toSize: kFooterBarHeight)
self.footerBarBottomConstraint = footerBar.autoPinEdge(toSuperviewEdge: .bottom, withInset: -kFooterBarHeight)
updateSelectButton()
self.view.layoutIfNeeded() self.view.layoutIfNeeded()
scrollToBottom(animated: false) scrollToBottom(animated: false)
} }
@ -123,7 +143,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
self.collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: false) self.collectionView?.scrollToItem(at: indexPath, at: .centeredVertically, animated: false)
} }
// MARK: UIColletionViewDelegate // MARK: UICollectionViewDelegate
override public func scrollViewDidScroll(_ scrollView: UIScrollView) { override public func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.autoLoadMoreIfNecessary() self.autoLoadMoreIfNecessary()
@ -137,42 +157,87 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
self.isUserScrolling = false self.isUserScrolling = false
} }
private var isUserScrolling: Bool = false { override public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
didSet {
autoLoadMoreIfNecessary() Logger.debug("\(self.logTag) in \(#function)")
guard galleryDates.count > 0 else {
return false
}
switch indexPath.section {
case kLoadOlderSectionIdx, loadNewerSectionIdx:
return false
default:
return true
} }
} }
// MARK: MediaGalleryDataSourceDelegate override public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool {
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete message: TSMessage) { Logger.debug("\(self.logTag) in \(#function)")
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil") guard galleryDates.count > 0 else {
return return false
} }
// We've got to lay out the collectionView before any changes are made to the date source switch indexPath.section {
// otherwise we'll fail when we try to remove the deleted sections/rows case kLoadOlderSectionIdx, loadNewerSectionIdx:
collectionView.layoutIfNeeded() return false
default:
return true
}
} }
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath]) { public override func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpetedly nil") Logger.debug("\(self.logTag) in \(#function)")
guard galleryDates.count > 0 else {
return false
}
switch indexPath.section {
case kLoadOlderSectionIdx, loadNewerSectionIdx:
return false
default:
return true
}
}
override public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
Logger.debug("\(self.logTag) in \(#function)")
guard let galleryCell = self.collectionView(collectionView, cellForItemAt: indexPath) as? MediaGalleryCell else {
owsFail("\(logTag) in \(#function) galleryCell was unexpectedly nil")
return return
} }
guard mediaGalleryDataSource.galleryItemCount > 0 else { guard let galleryItem = galleryCell.item else {
// Show Empty owsFail("\(logTag) in \(#function) galleryItem was unexpectedly nil")
self.collectionView?.reloadData()
return return
} }
// If collectionView hasn't been laid out yet, it won't have the sections/rows to remove. if isInBatchSelectMode {
collectionView.performBatchUpdates({ updateDeleteButton()
collectionView.deleteSections(deletedSections) } else {
collectionView.deleteItems(at: deletedItems) collectionView.deselectItem(at: indexPath, animated: true)
}) self.delegate?.mediaTileViewController(self, didTapView: galleryCell.imageView, mediaGalleryItem: galleryItem)
}
}
public override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
Logger.debug("\(self.logTag) in \(#function)")
if isInBatchSelectMode {
updateDeleteButton()
}
}
private var isUserScrolling: Bool = false {
didSet {
autoLoadMoreIfNecessary()
}
} }
// MARK: UICollectionViewDataSource // MARK: UICollectionViewDataSource
@ -288,30 +353,39 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
owsFail("\(logTag) in \(#function) unexpected cell for loadNewerSectionIdx") owsFail("\(logTag) in \(#function) unexpected cell for loadNewerSectionIdx")
return defaultCell return defaultCell
default: default:
guard let sectionDate = self.galleryDates[safe: indexPath.section - 1] else { guard let galleryItem = galleryItem(at: indexPath) else {
owsFail("\(logTag) in \(#function) unknown section: \(indexPath.section)") owsFail("\(logTag) in \(#function) no message for path: \(indexPath)")
return defaultCell return defaultCell
} }
guard let sectionItems = self.galleryItems[sectionDate] else { guard let cell = self.collectionView?.dequeueReusableCell(withReuseIdentifier: MediaGalleryCell.reuseIdentifier, for: indexPath) as? MediaGalleryCell else {
owsFail("\(logTag) in \(#function) no section for date: \(sectionDate)") owsFail("\(logTag) in \(#function) unexpected cell for indexPath: \(indexPath)")
return defaultCell return defaultCell
} }
guard let galleryItem = sectionItems[safe: indexPath.row] else { cell.configure(item: galleryItem)
owsFail("\(logTag) in \(#function) no message for row: \(indexPath.row)")
return defaultCell return cell
}
} }
guard let cell = self.collectionView?.dequeueReusableCell(withReuseIdentifier: MediaGalleryCell.reuseIdentifier, for: indexPath) as? MediaGalleryCell else { func galleryItem(at indexPath: IndexPath) -> MediaGalleryItem? {
owsFail("\(logTag) in \(#function) unexpected cell for indexPath: \(indexPath)") guard let sectionDate = self.galleryDates[safe: indexPath.section - 1] else {
return defaultCell owsFail("\(logTag) in \(#function) unknown section: \(indexPath.section)")
return nil
} }
cell.configure(item: galleryItem, delegate: self) guard let sectionItems = self.galleryItems[sectionDate] else {
owsFail("\(logTag) in \(#function) no section for date: \(sectionDate)")
return nil
}
return cell guard let galleryItem = sectionItems[safe: indexPath.row] else {
owsFail("\(logTag) in \(#function) no message for row: \(indexPath.row)")
return nil
} }
return galleryItem
} }
// MARK: UICollectionViewDelegateFlowLayout // MARK: UICollectionViewDelegateFlowLayout
@ -343,11 +417,173 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
return kMonthHeaderSize return kMonthHeaderSize
} }
} }
// MARK: MediaGalleryDelegate
fileprivate func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) { // MARK: Batch Selection
Logger.debug("\(logTag) in \(#function)")
self.delegate?.mediaTileViewController(self, didTapView: cell.imageView, mediaGalleryItem: item) var isInBatchSelectMode = false {
didSet {
collectionView!.allowsMultipleSelection = isInBatchSelectMode
updateSelectButton()
updateDeleteButton()
}
}
func updateDeleteButton() {
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
return
}
if let count = collectionView.indexPathsForSelectedItems?.count, count > 0 {
self.deleteButton.isEnabled = true
} else {
self.deleteButton.isEnabled = false
}
}
func updateSelectButton() {
if isInBatchSelectMode {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didCancelSelect))
} else {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("BUTTON_SELECT", comment: "Button text to enable batch selection mode"),
style: .plain,
target: self,
action: #selector(didTapSelect))
}
}
@objc
func didTapSelect(_ sender: Any) {
isInBatchSelectMode = true
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
return
}
// show toolbar
UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseInOut, animations: {
NSLayoutConstraint.deactivate([self.footerBarBottomConstraint])
self.footerBarBottomConstraint = self.footerBar.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
self.footerBar.superview?.layoutIfNeeded()
// ensure toolbar doesn't cover bottom row.
collectionView.contentInset.bottom += self.kFooterBarHeight
}, completion: nil)
// disabled until at least one item is selected
self.deleteButton.isEnabled = false
// Don't allow the user to leave mid-selection, so they realized they have
// to cancel (lose) their selection if they leave.
self.navigationItem.hidesBackButton = true
}
@objc
func didCancelSelect(_ sender: Any) {
isInBatchSelectMode = false
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
return
}
// hide toolbar
UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseInOut, animations: {
NSLayoutConstraint.deactivate([self.footerBarBottomConstraint])
self.footerBarBottomConstraint = self.footerBar.autoPinEdge(toSuperviewEdge: .bottom, withInset: -self.kFooterBarHeight)
self.footerBar.superview?.layoutIfNeeded()
// undo "ensure toolbar doesn't cover bottom row."
collectionView.contentInset.bottom -= self.kFooterBarHeight
}, completion: nil)
self.navigationItem.hidesBackButton = false
// deselect any selected
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)}
}
@objc
func didPressDelete(_ sender: Any) {
Logger.debug("\(self.logTag) in \(#function)")
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
return
}
guard let indexPaths = collectionView.indexPathsForSelectedItems else {
owsFail("\(logTag) in \(#function) indexPaths was unexpectedly nil")
return
}
let items: [MediaGalleryItem] = indexPaths.flatMap { return self.galleryItem(at: $0) }
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
return
}
let confirmationTitle: String = {
if indexPaths.count == 1 {
return NSLocalizedString("MEDIA_GALLERY_DELETE_SINGLE_MESSAGE", comment: "Confirmation button text to delete selected media message from the gallery")
} else {
let format = NSLocalizedString("MEDIA_GALLERY_DELETE_MULTIPLE_MESSAGES_FORMAT", comment: "Confirmation button text to delete selected media from the gallery, embeds {{number of messages}}")
return String(format: format, indexPaths.count)
}
}()
let deleteAction = UIAlertAction(title: confirmationTitle, style: .destructive) { _ in
mediaGalleryDataSource.delete(items: items)
}
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
actionSheet.addAction(deleteAction)
actionSheet.addAction(OWSAlerts.cancelAction)
present(actionSheet, animated: true)
}
var footerBar: UIToolbar!
var deleteButton: UIBarButtonItem!
var footerBarBottomConstraint: NSLayoutConstraint!
let kFooterBarHeight: CGFloat = 40
// MARK: MediaGalleryDataSourceDelegate
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem]) {
Logger.debug("\(self.logTag) in \(#function)")
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpectedly nil")
return
}
// We've got to lay out the collectionView before any changes are made to the date source
// otherwise we'll fail when we try to remove the deleted sections/rows
collectionView.layoutIfNeeded()
}
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath]) {
Logger.debug("\(self.logTag) in \(#function) with deletedSections: \(deletedSections) deletedItems: \(deletedItems)")
guard let collectionView = self.collectionView else {
owsFail("\(logTag) in \(#function) collectionView was unexpetedly nil")
return
}
guard mediaGalleryDataSource.galleryItemCount > 0 else {
// Show Empty
self.collectionView?.reloadData()
return
}
collectionView.performBatchUpdates({
collectionView.deleteSections(deletedSections)
collectionView.deleteItems(at: deletedItems)
})
} }
// MARK: Lazy Loading // MARK: Lazy Loading
@ -577,10 +813,6 @@ fileprivate class MediaGallerySectionHeader: UICollectionReusableView {
} }
} }
fileprivate protocol MediaGalleryCellDelegate: class {
func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem)
}
fileprivate class MediaGalleryStaticHeader: UICollectionViewCell { fileprivate class MediaGalleryStaticHeader: UICollectionViewCell {
static let reuseIdentifier = "MediaGalleryStaticHeader" static let reuseIdentifier = "MediaGalleryStaticHeader"
@ -616,40 +848,78 @@ fileprivate class MediaGalleryCell: UICollectionViewCell {
static let reuseIdentifier = "MediaGalleryCell" static let reuseIdentifier = "MediaGalleryCell"
public let imageView: UIImageView public let imageView: UIImageView
private var tapGesture: UITapGestureRecognizer!
private let badgeView: UIImageView private let contentTypeBadgeView: UIImageView
private let selectedBadgeView: UIImageView
private let highlightedView: UIView
private let selectedView: UIView
private var item: MediaGalleryItem? fileprivate var item: MediaGalleryItem?
public weak var delegate: MediaGalleryCellDelegate?
static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video") static let videoBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_video")
static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif") static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif")
static let selectedBadgeImage = #imageLiteral(resourceName: "selected_blue_circle")
override var isSelected: Bool {
didSet {
self.selectedBadgeView.isHidden = !self.isSelected
self.selectedView.isHidden = !self.isSelected
}
}
override var isHighlighted: Bool {
didSet {
self.highlightedView.isHidden = !self.isHighlighted
}
}
override init(frame: CGRect) { override init(frame: CGRect) {
self.imageView = UIImageView() self.imageView = UIImageView()
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
self.badgeView = UIImageView() self.contentTypeBadgeView = UIImageView()
badgeView.isHidden = true contentTypeBadgeView.isHidden = true
super.init(frame: frame) self.selectedBadgeView = UIImageView()
selectedBadgeView.image = MediaGalleryCell.selectedBadgeImage
selectedBadgeView.isHidden = true
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) self.highlightedView = UIView()
self.addGestureRecognizer(tapGesture) highlightedView.alpha = 0.2
highlightedView.backgroundColor = .black
highlightedView.isHidden = true
self.selectedView = UIView()
selectedView.alpha = 0.3
selectedView.backgroundColor = .white
selectedView.isHidden = true
super.init(frame: frame)
self.clipsToBounds = true self.clipsToBounds = true
self.contentView.addSubview(imageView) self.contentView.addSubview(imageView)
self.contentView.addSubview(badgeView) self.contentView.addSubview(contentTypeBadgeView)
self.contentView.addSubview(highlightedView)
self.contentView.addSubview(selectedView)
self.contentView.addSubview(selectedBadgeView)
imageView.autoPinEdgesToSuperviewEdges() imageView.autoPinEdgesToSuperviewEdges()
highlightedView.autoPinEdgesToSuperviewEdges()
selectedView.autoPinEdgesToSuperviewEdges()
// Note assets were rendered to match exactly. We don't want to re-size with // Note assets were rendered to match exactly. We don't want to re-size with
// content mode lest they become less legible. // content mode lest they become less legible.
let kBadgeSize = CGSize(width: 18, height: 12) let kContentTypeBadgeSize = CGSize(width: 18, height: 12)
badgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3) contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .leading, withInset: 3)
badgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3) contentTypeBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 3)
badgeView.autoSetDimensions(to: kBadgeSize) contentTypeBadgeView.autoSetDimensions(to: kContentTypeBadgeSize)
let kSelectedBadgeSize = CGSize(width: 31, height: 31)
selectedBadgeView.autoPinEdge(toSuperviewEdge: .trailing, withInset: 0)
selectedBadgeView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0)
selectedBadgeView.autoSetDimensions(to: kSelectedBadgeSize)
} }
@available(*, unavailable, message: "Unimplemented") @available(*, unavailable, message: "Unimplemented")
@ -657,21 +927,19 @@ fileprivate class MediaGalleryCell: UICollectionViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public func configure(item: MediaGalleryItem, delegate: MediaGalleryCellDelegate) { public func configure(item: MediaGalleryItem) {
self.item = item self.item = item
self.imageView.image = item.thumbnailImage self.imageView.image = item.thumbnailImage
if item.isVideo { if item.isVideo {
self.badgeView.isHidden = false self.contentTypeBadgeView.isHidden = false
self.badgeView.image = MediaGalleryCell.videoBadgeImage self.contentTypeBadgeView.image = MediaGalleryCell.videoBadgeImage
} else if item.isAnimated { } else if item.isAnimated {
self.badgeView.isHidden = false self.contentTypeBadgeView.isHidden = false
self.badgeView.image = MediaGalleryCell.animatedBadgeImage self.contentTypeBadgeView.image = MediaGalleryCell.animatedBadgeImage
} else { } else {
assert(item.isImage) assert(item.isImage)
self.badgeView.isHidden = true self.contentTypeBadgeView.isHidden = true
} }
self.delegate = delegate
} }
override public func prepareForReuse() { override public func prepareForReuse() {
@ -679,18 +947,9 @@ fileprivate class MediaGalleryCell: UICollectionViewCell {
self.item = nil self.item = nil
self.imageView.image = nil self.imageView.image = nil
self.badgeView.isHidden = true self.contentTypeBadgeView.isHidden = true
self.delegate = nil self.highlightedView.isHidden = true
} self.selectedView.isHidden = true
self.selectedBadgeView.isHidden = true
// MARK: Events
func didTap(gestureRecognizer: UITapGestureRecognizer) {
guard let item = self.item else {
owsFail("\(logTag) item was unexpectedly nil")
return
}
self.delegate?.didTapCell(self, item: item)
} }
} }

@ -758,9 +758,10 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
// MediaGalleryDataSourceDelegate // MediaGalleryDataSourceDelegate
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete message: TSMessage) { func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem]) {
Logger.info("\(self.logTag) in \(#function)") Logger.info("\(self.logTag) in \(#function)")
guard message == self.message else {
guard (items.map({ $0.message }) == [self.message]) else {
// Should only be one message we can delete when viewing message details // Should only be one message we can delete when viewing message details
owsFail("\(logTag) in \(#function) Unexpectedly informed of irrelevant message deletion") owsFail("\(logTag) in \(#function) Unexpectedly informed of irrelevant message deletion")
return return

@ -262,6 +262,9 @@
/* Label for generic done button. */ /* Label for generic done button. */
"BUTTON_DONE" = "Done"; "BUTTON_DONE" = "Done";
/* Button text to enable batch selection mode */
"BUTTON_SELECT" = "Select";
/* Alert message when calling and permissions for microphone are missing */ /* Alert message when calling and permissions for microphone are missing */
"CALL_AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to make calls and record voice messages. You can grant this permission in the Settings app."; "CALL_AUDIO_PERMISSION_MESSAGE" = "Signal requires access to your microphone to make calls and record voice messages. You can grant this permission in the Settings app.";
@ -983,6 +986,12 @@
/* media picker option to choose from library */ /* media picker option to choose from library */
"MEDIA_FROM_LIBRARY_BUTTON" = "Photo Library"; "MEDIA_FROM_LIBRARY_BUTTON" = "Photo Library";
/* Confirmation button text to delete selected media from the gallery, embeds {{number of messages}} */
"MEDIA_GALLERY_DELETE_MULTIPLE_MESSAGES_FORMAT" = "Delete %d Messages";
/* Confirmation button text to delete selected media message from the gallery */
"MEDIA_GALLERY_DELETE_SINGLE_MESSAGE" = "Delete Message";
/* Short sender label for media sent by you */ /* Short sender label for media sent by you */
"MEDIA_GALLERY_SENDER_NAME_YOU" = "You"; "MEDIA_GALLERY_SENDER_NAME_YOU" = "You";

Loading…
Cancel
Save