Merge branch 'mkirk/delete-from-gallery'

pull/1/head
Michael Kirk 7 years ago
commit 6d45c38b49

@ -999,7 +999,6 @@ const CGFloat OWSMessageCellCornerRadius = 17;
self.customView.userInteractionEnabled = NO;
[self.payloadView addSubview:self.customView];
[self.contentConstraints addObjectsFromArray:[self.customView autoPinToSuperviewEdges]];
[self cropMediaViewToBubbbleShape:self.customView];
}
- (CGSize)textBubbleSizeForContentWidth:(int)contentWidth

@ -123,6 +123,10 @@ NS_ASSUME_NONNULL_BEGIN
actionBlock:^{
[DebugUIMessages createFakeThreads:1000 withFakeMessages:1];
}],
[OWSTableItem itemWithTitle:@"🖼 fake media messages: 5"
actionBlock:^{
[DebugUIMessages sendFakeMediaMessages:5 thread:thread];
}],
[OWSTableItem itemWithTitle:@"🖼 fake media messages: 100"
actionBlock:^{
[DebugUIMessages sendFakeMediaMessages:100 thread:thread];

@ -17,7 +17,9 @@ typedef NS_OPTIONS(NSInteger, MediaGalleryOption) {
@protocol MediaDetailViewControllerDelegate <NSObject>
- (void)dismissSelfAnimated:(BOOL)isAnimated completion:(void (^_Nullable)(void))completionBlock;
- (void)mediaDetailViewController:(MediaDetailViewController *)mediaDetailViewController
requestDeleteConversationViewItem:(ConversationViewItem *)conversationViewItem;
- (void)mediaDetailViewController:(MediaDetailViewController *)mediaDetailViewController
isPlayingVideo:(BOOL)isPlayingVideo;

@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) TSAttachmentStream *attachmentStream;
@property (nonatomic, nullable) ConversationViewItem *viewItem;
@property (nonatomic, readonly) UIImage *image;
@property (nonatomic, nullable) OWSVideoPlayer *videoPlayer;
@property (nonatomic, nullable) UIButton *playVideoButton;
@ -94,6 +95,8 @@ NS_ASSUME_NONNULL_BEGIN
_galleryItemBox = galleryItemBox;
_viewItem = viewItem;
// We cache the image data in case the attachment stream is deleted.
_image = galleryItemBox.attachmentStream.image;
return self;
}
@ -119,11 +122,6 @@ NS_ASSUME_NONNULL_BEGIN
return _fileData;
}
- (UIImage *)image
{
return self.attachmentStream.image;
}
- (BOOL)isAnimated
{
return self.attachmentStream.isAnimated;
@ -407,43 +405,7 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
UIAlertController *actionSheet =
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil)
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
OWSAssert([self.presentingViewController
isKindOfClass:[UINavigationController class]]);
UINavigationController *navController
= (UINavigationController *)self.presentingViewController;
if ([navController.topViewController
isKindOfClass:[ConversationViewController class]]) {
[self.delegate dismissSelfAnimated:YES
completion:^{
[self.viewItem deleteAction];
}];
} else if ([navController.topViewController
isKindOfClass:[MessageDetailViewController class]]) {
[self.delegate dismissSelfAnimated:YES
completion:^{
[self.viewItem deleteAction];
}];
[navController popViewControllerAnimated:YES];
} else {
OWSFail(@"Unexpected presentation context.");
[self.delegate dismissSelfAnimated:YES
completion:^{
[self.viewItem deleteAction];
}];
}
}]];
[actionSheet addAction:[OWSAlerts cancelAction]];
[self presentViewController:actionSheet animated:YES completion:nil];
[self.delegate mediaDetailViewController:self requestDeleteConversationViewItem:self.viewItem];
}
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender

@ -175,6 +175,13 @@ protocol MediaGalleryDataSource: class {
func showAllMedia(focusedItem: MediaGalleryItem)
func dismissMediaDetailViewController(_ mediaDetailViewController: MediaPageViewController, animated isAnimated: Bool, completion: (() -> Void)?)
func delete(message: TSMessage)
}
protocol MediaGalleryDataSourceDelegate: class {
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete message: TSMessage)
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath])
}
class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource, MediaTileViewControllerDelegate {
@ -182,6 +189,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
private var pageViewController: MediaPageViewController?
private let uiDatabaseConnection: YapDatabaseConnection
private let editingDatabaseConnection: YapDatabaseConnection
private let mediaGalleryFinder: OWSMediaGalleryFinder
private var initialDetailItem: MediaGalleryItem?
@ -199,6 +207,9 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
self.thread = thread
assert(uiDatabaseConnection.isInLongLivedReadTransaction())
self.uiDatabaseConnection = uiDatabaseConnection
self.editingDatabaseConnection = OWSPrimaryStorage.shared().newDatabaseConnection()
self.options = options
self.mediaGalleryFinder = OWSMediaGalleryFinder(thread: thread)
@ -549,6 +560,12 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
lazy var mediaTileViewController: MediaTileViewController = {
let vc = MediaTileViewController(mediaGalleryDataSource: self, uiDatabaseConnection: self.uiDatabaseConnection)
vc.delegate = self
// dataSourceDelegate will either be this tile view, or the MessageDetailView, but they should
// be mutually exclusive
assert(self.dataSourceDelegate == nil)
self.dataSourceDelegate = vc
return vc
}()
@ -631,6 +648,12 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
Logger.debug("\(self.logTag) in \(#function) fetching set: \(unfetchedSet)")
let nsRange: NSRange = NSRange(location: unfetchedSet.min()!, length: unfetchedSet.count)
self.mediaGalleryFinder.enumerateMediaMessages(range: nsRange, transaction: transaction) { (message: TSMessage) in
guard !self.deletedMessages.contains(message) else {
Logger.debug("\(self.logTag) skipping \(message) which has been deleted.")
return
}
guard let item: MediaGalleryItem = self.buildGalleryItem(message: message, transaction: transaction) else {
owsFail("\(self.logTag) in \(#function) unexpectedly failed to buildGalleryItem")
return
@ -700,6 +723,62 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
}
}
weak var dataSourceDelegate: MediaGalleryDataSourceDelegate?
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)
self.editingDatabaseConnection.asyncReadWrite { transaction in
message.remove(with: transaction)
}
self.deletedMessages.insert(message)
var deletedSections: IndexSet = IndexSet()
var deletedIndexPaths: [IndexPath] = []
guard let itemIndex = galleryItems.index(where: { $0.message == message }) else {
owsFail("\(logTag) in \(#function) removing unknown item.")
return
}
let item: MediaGalleryItem = galleryItems[itemIndex]
self.galleryItems.remove(at: itemIndex)
guard let sectionIndex = sectionDates.index(where: { $0 == item.galleryDate }) else {
owsFail("\(logTag) in \(#function) item with unknown date.")
return
}
guard var sectionItems = self.sections[item.galleryDate] else {
owsFail("\(logTag) in \(#function) item with unknown section")
return
}
if sectionItems == [item] {
// Last item in section. Delete section.
self.sections[item.galleryDate] = nil
self.sectionDates.remove(at: sectionIndex)
deletedSections.insert(sectionIndex + 1)
deletedIndexPaths.append(IndexPath(row: 0, section: sectionIndex + 1))
} else {
guard let sectionRowIndex = sectionItems.index(of: item) else {
owsFail("\(logTag) in \(#function) item with unknown sectionRowIndex")
return
}
sectionItems.remove(at: sectionRowIndex)
self.sections[item.galleryDate] = sectionItems
deletedIndexPaths.append(IndexPath(row: sectionRowIndex, section: sectionIndex + 1))
}
self.dataSourceDelegate?.mediaGalleryDataSource(self, deletedSections: deletedSections, deletedItems: deletedIndexPaths)
}
let kGallerySwipeLoadBatchSize: UInt = 5
internal func galleryItem(after currentItem: MediaGalleryItem) -> MediaGalleryItem? {
@ -735,6 +814,6 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource
self.uiDatabaseConnection.read { (transaction: YapDatabaseReadTransaction) in
count = self.mediaGalleryFinder.mediaCount(transaction: transaction)
}
return Int(count)
return Int(count) - deletedMessages.count
}
}

@ -47,14 +47,18 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
return currentViewController.galleryItemBox.value
}
set {
guard let galleryPage = self.buildGalleryPage(galleryItem: newValue) else {
owsFail("unexpetedly unable to build new gallery page")
return
}
setCurrentItem(newValue, direction: .forward, animated: false)
}
}
self.updateTitle(item: newValue)
self.setViewControllers([galleryPage], direction: .forward, animated: false, completion: nil)
private func setCurrentItem(_ item: MediaGalleryItem, direction: UIPageViewControllerNavigationDirection, animated isAnimated: Bool) {
guard let galleryPage = self.buildGalleryPage(galleryItem: item) else {
owsFail("unexpetedly unable to build new gallery page")
return
}
self.updateTitle(item: item)
self.setViewControllers([galleryPage], direction: direction, animated: isAnimated)
}
private let uiDatabaseConnection: YapDatabaseConnection
@ -302,7 +306,33 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
owsFail("\(logTag) in \(#function) currentViewController was unexpectedly nil")
return
}
currentViewController.didPressDelete(sender)
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
return
}
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""),
style: .destructive) { _ in
let deletedItem = currentViewController.galleryItem
if !self.sliderEnabled {
// In message details, which doesn't use the slider, so don't swap pages.
} else if let nextItem = mediaGalleryDataSource.galleryItem(after: deletedItem) {
self.setCurrentItem(nextItem, direction: .forward, animated: true)
} else if let previousItem = mediaGalleryDataSource.galleryItem(before: deletedItem) {
self.setCurrentItem(previousItem, direction: .reverse, animated: true)
} else {
// else we deleted the last piece of media, return to the conversation view
self.dismissSelf(animated: true)
}
mediaGalleryDataSource.delete(message: deletedItem.message)
}
actionSheet.addAction(OWSAlerts.cancelAction)
actionSheet.addAction(deleteAction)
self.present(actionSheet, animated: true)
}
@objc
@ -440,8 +470,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
return viewController
}
// MARK: MediaDetailViewControllerDelegate
public func dismissSelf(animated isAnimated: Bool, completion: (() -> Void)? = nil) {
// Swapping mediaView for presentationView will be perceptible if we're not zoomed out all the way.
// currentVC
@ -457,6 +485,28 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
mediaGalleryDataSource.dismissMediaDetailViewController(self, animated: isAnimated, completion: completion)
}
// MARK: MediaDetailViewControllerDelegate
public func mediaDetailViewController(_ mediaDetailViewController: MediaDetailViewController, requestDelete conversationViewItem: ConversationViewItem) {
guard let mediaGalleryDataSource = self.mediaGalleryDataSource else {
owsFail("\(logTag) in \(#function) mediaGalleryDataSource was unexpectedly nil")
self.presentingViewController?.dismiss(animated: true)
return
}
guard let message = conversationViewItem.interaction as? TSMessage else {
owsFail("\(logTag) in \(#function) unexpected interaction: \(type(of: conversationViewItem))")
self.presentingViewController?.dismiss(animated: true)
return
}
dismissSelf(animated: true) {
mediaGalleryDataSource.delete(message: message)
}
}
public func mediaDetailViewController(_ mediaDetailViewController: MediaDetailViewController, isPlayingVideo: Bool) {
guard mediaDetailViewController == currentViewController else {
Logger.verbose("\(logTag) in \(#function) ignoring stale delegate.")

@ -8,7 +8,7 @@ public protocol MediaTileViewControllerDelegate: class {
func mediaTileViewController(_ viewController: MediaTileViewController, didTapView tappedView: UIView, mediaGalleryItem: MediaGalleryItem)
}
public class MediaTileViewController: UICollectionViewController, MediaGalleryCellDelegate {
public class MediaTileViewController: UICollectionViewController, MediaGalleryCellDelegate, MediaGalleryDataSourceDelegate {
private weak var mediaGalleryDataSource: MediaGalleryDataSource?
@ -143,7 +143,39 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe
}
}
// MARK: UIColletionViewDataSource
// MARK: MediaGalleryDataSourceDelegate
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete message: TSMessage) {
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]) {
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
}
// If collectionView hasn't been laid out yet, it won't have the sections/rows to remove.
collectionView.performBatchUpdates({
collectionView.deleteSections(deletedSections)
collectionView.deleteItems(at: deletedItems)
})
}
// MARK: UICollectionViewDataSource
override public func numberOfSections(in collectionView: UICollectionView) -> Int {
guard galleryDates.count > 0 else {

@ -12,10 +12,7 @@ enum MessageMetadataViewMode: UInt {
case focusOnMetadata
}
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, MediaDetailPresenter {
static let TAG = "[MessageDetailViewController]"
let TAG = "[MessageDetailViewController]"
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, MediaDetailPresenter, MediaGalleryDataSourceDelegate {
// MARK: Properties
@ -29,6 +26,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
let mode: MessageMetadataViewMode
let viewItem: ConversationViewItem
var message: TSMessage
var wasDeleted: Bool = false
var mediaMessageView: MediaMessageView?
@ -171,7 +169,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
private func updateContent() {
guard let contentView = contentView else {
owsFail("\(TAG) Missing contentView")
owsFail("\(logTag) Missing contentView")
return
}
@ -392,7 +390,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
if rows.count == 0 {
// Neither attachment nor body.
owsFail("\(self.TAG) Message has neither attachment nor body.")
owsFail("\(self.logTag) Message has neither attachment nor body.")
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_NO_ATTACHMENT_OR_BODY",
comment: "Label for messages without a body or attachment in the 'message metadata' view."),
value: ""))
@ -411,7 +409,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
}
guard let attachment = TSAttachment.fetch(uniqueId: attachmentId, transaction: transaction) else {
Logger.warn("\(TAG) Missing attachment. Was it deleted?")
Logger.warn("\(logTag) Missing attachment. Was it deleted?")
return nil
}
@ -422,7 +420,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
var rows = [UIView]()
guard let attachment = self.attachment else {
Logger.warn("\(TAG) Missing attachment. Was it deleted?")
Logger.warn("\(logTag) Missing attachment. Was it deleted?")
return rows
}
@ -555,7 +553,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
func shareButtonPressed() {
guard let attachmentStream = attachmentStream else {
Logger.error("\(TAG) Share button should only be shown with attachment, but no attachment found.")
Logger.error("\(logTag) Share button should only be shown with attachment, but no attachment found.")
return
}
AttachmentSharing.showShareUI(forAttachment: attachmentStream)
@ -568,15 +566,15 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
}
guard let attachmentStream = attachmentStream else {
Logger.error("\(TAG) Message has neither attachment nor message body.")
Logger.error("\(logTag) Message has neither attachment nor message body.")
return
}
guard let utiType = MIMETypeUtil.utiType(forMIMEType: attachmentStream.contentType) else {
Logger.error("\(TAG) Attachment has invalid MIME type: \(attachmentStream.contentType).")
Logger.error("\(logTag) Attachment has invalid MIME type: \(attachmentStream.contentType).")
return
}
guard let dataSource = dataSource else {
Logger.error("\(TAG) Attachment missing data source.")
Logger.error("\(logTag) Attachment missing data source.")
return
}
let data = dataSource.data()
@ -592,11 +590,11 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
self.uiDatabaseConnection.read { transaction in
guard let uniqueId = self.message.uniqueId else {
Logger.error("\(self.TAG) Message is missing uniqueId.")
Logger.error("\(self.logTag) Message is missing uniqueId.")
return
}
guard let newMessage = TSInteraction.fetch(uniqueId: uniqueId, transaction: transaction) as? TSMessage else {
Logger.error("\(self.TAG) Couldn't reload message.")
Logger.error("\(self.logTag) Couldn't reload message.")
return
}
self.message = newMessage
@ -607,21 +605,25 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
internal func yapDatabaseModified(notification: NSNotification) {
AssertIsOnMainThread()
guard !wasDeleted else {
// Item was deleted. Don't bother re-rendering, it will fail and we'll soon be dismissed.
return
}
let notifications = self.uiDatabaseConnection.beginLongLivedReadTransaction()
guard let uniqueId = self.message.uniqueId else {
Logger.error("\(self.TAG) Message is missing uniqueId.")
Logger.error("\(self.logTag) Message is missing uniqueId.")
return
}
guard self.uiDatabaseConnection.hasChange(forKey: uniqueId,
inCollection: TSInteraction.collection(),
in: notifications) else {
Logger.debug("\(TAG) No relevant changes.")
Logger.debug("\(logTag) No relevant changes.")
return
}
updateDBConnectionAndMessageToLatest()
updateContent()
}
@ -669,44 +671,44 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
return
}
guard let messageTextProxyView = messageTextProxyView else {
owsFail("\(TAG) Missing messageTextProxyView")
owsFail("\(logTag) Missing messageTextProxyView")
return
}
guard let scrollView = scrollView else {
owsFail("\(TAG) Missing scrollView")
owsFail("\(logTag) Missing scrollView")
return
}
guard let contentView = contentView else {
owsFail("\(TAG) Missing contentView")
owsFail("\(logTag) Missing contentView")
return
}
guard let bubbleView = bubbleView else {
owsFail("\(TAG) Missing bubbleView")
owsFail("\(logTag) Missing bubbleView")
return
}
guard let bubbleSuperview = bubbleView.superview else {
owsFail("\(TAG) Missing bubbleSuperview")
owsFail("\(logTag) Missing bubbleSuperview")
return
}
guard let messageTextTopConstraint = messageTextTopConstraint else {
owsFail("\(TAG) Missing messageTextTopConstraint")
owsFail("\(logTag) Missing messageTextTopConstraint")
return
}
guard let messageTextHeightLayoutConstraint = messageTextHeightLayoutConstraint else {
owsFail("\(TAG) Missing messageTextHeightLayoutConstraint")
owsFail("\(logTag) Missing messageTextHeightLayoutConstraint")
return
}
guard let messageTextProxyViewHeightConstraint = messageTextProxyViewHeightConstraint else {
owsFail("\(TAG) Missing messageTextProxyViewHeightConstraint")
owsFail("\(logTag) Missing messageTextProxyViewHeightConstraint")
return
}
guard let bubbleViewWidthConstraint = bubbleViewWidthConstraint else {
owsFail("\(TAG) Missing bubbleViewWidthConstraint")
owsFail("\(logTag) Missing bubbleViewWidthConstraint")
return
}
if messageTextView.width() != messageTextProxyView.width() {
owsFail("\(TAG) messageTextView.width \(messageTextView.width) != messageTextProxyView.width \(messageTextProxyView.width)")
owsFail("\(logTag) messageTextView.width \(messageTextView.width) != messageTextProxyView.width \(messageTextProxyView.width)")
}
let maxBubbleWidth = bubbleSuperview.width() - (bubbleViewHMargin * 2)
@ -749,11 +751,30 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
Logger.verbose("\(TAG) scrollViewDidScroll")
Logger.verbose("\(logTag) scrollViewDidScroll")
updateTextLayout()
}
// MediaGalleryDataSourceDelegate
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete message: TSMessage) {
Logger.info("\(self.logTag) in \(#function)")
guard message == self.message else {
// Should only be one message we can delete when viewing message details
owsFail("\(logTag) in \(#function) Unexpectedly informed of irrelevant message deletion")
return
}
self.wasDeleted = true
}
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath]) {
self.dismiss(animated: true) {
self.navigationController?.popViewController(animated: true)
}
}
// MARK: MediaDetailPresenter
public func presentDetails(mediaMessageView: MediaMessageView, fromView: UIView) {
@ -763,6 +784,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi
}
let mediaGalleryViewController = MediaGalleryViewController(thread: self.thread, uiDatabaseConnection: self.uiDatabaseConnection)
mediaGalleryViewController.dataSourceDelegate = self
mediaGalleryViewController.presentDetailView(fromViewController: self, mediaMessage: self.message, replacingView: fromView)
}
}

@ -774,7 +774,7 @@
"FINISH_GROUP_CREATION_LABEL" = "Finish creating group";
/* Label indicating media gallery is empty */
"GALLERY_TILES_EMPTY_GALLERY" = "You haven't sent or received any media in this conversation yet.";
"GALLERY_TILES_EMPTY_GALLERY" = "You don't have any media in this conversation.";
/* Label indicating loading is in progress */
"GALLERY_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Media...";

Loading…
Cancel
Save