New downloading progress view (#2006)

Replace previous "scary" warning-style attachment notifications with
something less alarming.

Includes file name and file type emoji when discernable.

// FREEBIE
This commit is contained in:
Michael Kirk 2017-04-19 18:50:27 -04:00 committed by GitHub
parent 67e94e2b55
commit d9e3e87735
8 changed files with 355 additions and 61 deletions

View File

@ -138,7 +138,7 @@ CHECKOUT OPTIONS:
:commit: 7054e4b13ee5bcd6d524adb6dc9a726e8c466308
:git: https://github.com/WhisperSystems/JSQMessagesViewController.git
SignalServiceKit:
:commit: 1032588da1b3c8a006110b1cbd024bda5ff79f2c
:commit: 1fe093074077a686931acf527e0d8255ea73a22d
:git: https://github.com/WhisperSystems/SignalServiceKit.git
SocketRocket:
:commit: 877ac7438be3ad0b45ef5ca3969574e4b97112bf

View File

@ -86,6 +86,8 @@
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
452C46901E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; };
452EA0971EA662330078744B /* AttachmentPointerAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */; };
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; };
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
452ECA4E1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */; };
@ -460,6 +462,8 @@
4526BD481CA61C8D00166BC8 /* OWSMessageEditing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageEditing.h; sourceTree = "<group>"; };
452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = "<group>"; };
452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = "<group>"; };
452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerAdapter.swift; sourceTree = "<group>"; };
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = "<group>"; };
452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageFetcherJob.swift; path = Jobs/MessageFetcherJob.swift; sourceTree = "<group>"; };
4531C9C21DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMessagesCollectionViewCell+OWS.h"; sourceTree = "<group>"; };
4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMessagesCollectionViewCell+OWS.m"; sourceTree = "<group>"; };
@ -1265,6 +1269,7 @@
45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */,
34330AA11E79686200DF2FB9 /* OWSProgressView.h */,
34330AA21E79686200DF2FB9 /* OWSProgressView.m */,
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */,
);
name = Views;
path = views;
@ -1311,6 +1316,7 @@
B6A3EB4A1A423B3800B2236B /* TSPhotoAdapter.m */,
A5E9D4BA1A65FAD800E4481C /* TSVideoAttachmentAdapter.h */,
A5E9D4B91A65FAD800E4481C /* TSVideoAttachmentAdapter.m */,
452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */,
);
name = TSMessageAdapters;
path = TSMessageAdapaters;
@ -1989,6 +1995,7 @@
450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */,
B6258B331C29E2E60014138E /* NotificationsManager.m in Sources */,
34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */,
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */,
45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */,
34B3F87A1E8DF1700035BE1A /* DebugUITableViewController.m in Sources */,
34B3F87C1E8DF1700035BE1A /* FingerprintViewController.m in Sources */,
@ -2001,6 +2008,7 @@
45C681C61D305C9E0050903A /* OWSDisplayedMessageCollectionViewCell.m in Sources */,
34B3F8861E8DF1700035BE1A /* NotificationSettingsOptionsViewController.m in Sources */,
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */,
452EA0971EA662330078744B /* AttachmentPointerAdapter.swift in Sources */,
34DFCB851E8E04B500053165 /* AddToBlockListViewController.m in Sources */,
34B3F8321E8DF11D0035BE1A /* GroupContactsResult.m in Sources */,
45855F371D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */,

View File

@ -0,0 +1,110 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import UIKit
/**
* Represents a download-in-progress
*/
class AttachmentPointerAdapter: JSQMediaItem, OWSMessageEditing {
let TAG = "[AttachmentPointerAdapter]"
let isIncoming: Bool
let attachmentPointer: TSAttachmentPointer
var cachedView: UIView?
var attachmentPointerView: AttachmentPointerView?
required init(attachmentPointer: TSAttachmentPointer, isIncoming: Bool) {
self.isIncoming = isIncoming
self.attachmentPointer = attachmentPointer
super.init(maskAsOutgoing: !isIncoming)
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
assertionFailure("init(coder:) has not been implemented")
self.isIncoming = true
self.attachmentPointer = TSAttachmentPointer()
super.init(coder: aDecoder)
}
func canPerformAction(_ action: Selector) -> Bool {
// No actions can be performed on a downloading attachment.
return false
}
func performAction(_ action: Selector) {
// Should not get here, as you can't perform any actions on a downloading attachment.
Logger.error("\(TAG) unexpectedly trying to perform action: \(action) on downloading attachment.")
assertionFailure()
}
override func mediaViewDisplaySize() -> CGSize {
return CGSize(width: 200, height: 90)
}
override func mediaView() -> UIView? {
guard self.cachedView == nil else {
return self.cachedView
}
let frame = CGRect(origin: CGPoint.zero, size: self.mediaViewDisplaySize())
let view = UIView(frame: frame)
self.cachedView = view
JSQMessagesMediaViewBubbleImageMasker.applyBubbleImageMask(toMediaView: view, isOutgoing:!isIncoming)
view.isUserInteractionEnabled = false
view.clipsToBounds = true
let attachmentPointerView = AttachmentPointerView(attachmentPointer: attachmentPointer, isIncoming: self.isIncoming)
self.attachmentPointerView = attachmentPointerView
view.addSubview(attachmentPointerView)
attachmentPointerView.autoPinWidthToSuperview(withMargin: 20)
attachmentPointerView.autoVCenterInSuperview()
switch attachmentPointer.state {
case .downloading:
NotificationCenter.default.addObserver(self, selector: #selector(attachmentDownloadProgress), name: NSNotification.Name.attachmentDownloadProgress, object: nil)
view.backgroundColor = isIncoming ? UIColor.jsq_messageBubbleLightGray() : UIColor.ows_fadedBlue()
case .enqueued:
view.backgroundColor = isIncoming ? UIColor.jsq_messageBubbleLightGray() : UIColor.ows_fadedBlue()
case .failed:
view.backgroundColor = UIColor.gray
}
return cachedView
}
func attachmentDownloadProgress(_ notification: NSNotification) {
guard let attachmentPointerView = self.attachmentPointerView else {
Logger.error("\(TAG) downloading view was unexpectedly nil for notification: \(notification)")
assertionFailure()
return
}
guard let userInfo = notification.userInfo else {
Logger.error("\(TAG) user info was unexpectedly nil for notification: \(notification)")
assertionFailure()
return
}
guard let progress = userInfo[kAttachmentDownloadProgressKey] as? CGFloat else {
Logger.error("\(TAG) missing progress measure for notification user info: \(userInfo)")
assertionFailure()
return
}
guard let attachmentId = userInfo[kAttachmentDownloadAttachmentIDKey] as? String else {
Logger.error("\(TAG) missing attachmentId for notification user info: \(userInfo)")
assertionFailure()
return
}
if (self.attachmentPointer.uniqueId == attachmentId) {
attachmentPointerView.progress = progress
}
}
}

View File

@ -177,19 +177,9 @@ NS_ASSUME_NONNULL_BEGIN
}
} else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
TSAttachmentPointer *pointer = (TSAttachmentPointer *)attachment;
adapter.messageType = TSInfoMessageAdapter;
switch (pointer.state) {
case TSAttachmentPointerStateEnqueued:
adapter.messageBody = NSLocalizedString(@"ATTACHMENT_QUEUED", nil);
break;
case TSAttachmentPointerStateDownloading:
adapter.messageBody = NSLocalizedString(@"ATTACHMENT_DOWNLOADING", nil);
break;
case TSAttachmentPointerStateFailed:
adapter.messageBody = NSLocalizedString(@"ATTACHMENT_DOWNLOAD_FAILED", nil);
break;
}
adapter.mediaItem =
[[AttachmentPointerAdapter alloc] initWithAttachmentPointer:pointer
isIncoming:isIncomingAttachment];
} else {
DDLogError(@"We retrieved an attachment that doesn't have a known type : %@",
NSStringFromClass([attachment class]));

View File

@ -16,6 +16,8 @@
#import "OWSDispatch.h"
#import "OWSError.h"
#import "OWSLogger.h"
#import "OWSMessageEditing.h"
#import "OWSProgressView.h"
#import "OWSWebRTCDataProtos.pb.h"
#import "PrivacySettingsTableViewController.h"
#import "PropertyListPreferences.h"
@ -27,6 +29,9 @@
#import "UIUtil.h"
#import "UIView+OWS.h"
#import "ViewControllerUtils.h"
#import <JSQMessagesViewController/JSQMediaItem.h>
#import <JSQMessagesViewController/JSQMessagesMediaViewBubbleImageMasker.h>
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
#import <JSQSystemSoundPlayer.h>
#import <PureLayout/PureLayout.h>
#import <SignalServiceKit/Contact.h>
@ -36,6 +41,7 @@
#import <SignalServiceKit/NSData+Base64.h>
#import <SignalServiceKit/NSDate+millisecondTimeStamp.h>
#import <SignalServiceKit/OWSAcknowledgeMessageDeliveryRequest.h>
#import <SignalServiceKit/OWSAttachmentsProcessor.h>
#import <SignalServiceKit/OWSCallAnswerMessage.h>
#import <SignalServiceKit/OWSCallBusyMessage.h>
#import <SignalServiceKit/OWSCallHangupMessage.h>
@ -53,6 +59,7 @@
#import <SignalServiceKit/SignalRecipient.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSAttachment.h>
#import <SignalServiceKit/TSAttachmentPointer.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSCall.h>
#import <SignalServiceKit/TSContactThread.h>

View File

@ -1201,6 +1201,15 @@ typedef enum : NSUInteger {
// JSQM does some setup in super method
[super collectionView:collectionView shouldShowMenuForItemAtIndexPath:indexPath];
// Don't show menu for in-progress downloads.
// We don't want to give the user the wrong idea that deleting would "cancel" the download.
id<OWSMessageData> message = [self messageAtIndexPath:indexPath];
if ([message.media isKindOfClass:[AttachmentPointerAdapter class]]) {
AttachmentPointerAdapter *attachmentPointerAdapter = (AttachmentPointerAdapter *)message.media;
return attachmentPointerAdapter.attachmentPointer.state == TSAttachmentPointerStateFailed;
}
// Super method returns false for media methods. We want menu for *all* items
return YES;
}
@ -1856,15 +1865,32 @@ typedef enum : NSUInteger {
}
}
}
} else if ([messageItem.media isKindOfClass:[AttachmentPointerAdapter class]]) {
AttachmentPointerAdapter *attachmentPointerAdadpter = (AttachmentPointerAdapter *)messageItem.media;
TSAttachmentPointer *attachmentPointer = attachmentPointerAdadpter.attachmentPointer;
// Restart failed downloads
if (attachmentPointer.state == TSAttachmentPointerStateFailed) {
if (![interaction isKindOfClass:[TSMessage class]]) {
DDLogError(@"%@ Expected attachment downloads from an instance of message, but found: %@", self.tag, interaction);
OWSAssert(NO);
return;
}
TSMessage *message = (TSMessage *)interaction;
[self handleFailedDownloadTapForMessage:message attachmentPointer:attachmentPointer];
} else {
DDLogVerbose(@"%@ Ignoring tap for attachment pointer %@ with state %lu",
self.tag,
attachmentPointer,
(unsigned long)attachmentPointer.state);
}
} else {
DDLogDebug(@"%@ Unhandled tap on 'media item' with media: %@", self.tag, messageItem.media);
}
}
} break;
case TSErrorMessageAdapter:
[self handleErrorMessageTap:(TSErrorMessage *)interaction];
break;
case TSInfoMessageAdapter:
[self handleWarningTap:interaction];
break;
case TSCallAdapter:
break;
default:
@ -1896,41 +1922,6 @@ typedef enum : NSUInteger {
}
}
- (void)handleWarningTap:(TSInteraction *)interaction
{
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *message = (TSIncomingMessage *)interaction;
for (NSString *attachmentId in message.attachmentIds) {
__block TSAttachment *attachment;
[self.editingDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
}];
if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
TSAttachmentPointer *pointer = (TSAttachmentPointer *)attachment;
// FIXME possible for pointer to get stuck in isDownloading state if app is closed while downloading.
// see: https://github.com/WhisperSystems/Signal-iOS/issues/1254
if (pointer.state != TSAttachmentPointerStateDownloading) {
OWSAttachmentsProcessor *processor =
[[OWSAttachmentsProcessor alloc] initWithAttachmentPointer:pointer
networkManager:self.networkManager];
[processor fetchAttachmentsForMessage:message
success:^(TSAttachmentStream *_Nonnull attachmentStream) {
DDLogInfo(
@"%@ Successfully redownloaded attachment in thread: %@", self.tag, message.thread);
}
failure:^(NSError *_Nonnull error) {
DDLogWarn(@"%@ Failed to redownload message with error: %@", self.tag, error);
}];
}
}
}
}
}
// There's more than one way to exit the fullscreen video playback.
// There's a done button, a "toggle fullscreen" button and I think
// there's some gestures too. These fire slightly different notifications.
@ -2042,6 +2033,50 @@ typedef enum : NSUInteger {
#pragma mark Bubble User Actions
- (void)handleFailedDownloadTapForMessage:(TSMessage *)message
attachmentPointer:(TSAttachmentPointer *)attachmentPointer
{
UIAlertController *actionSheetController = [UIAlertController
alertControllerWithTitle:NSLocalizedString(@"MESSAGES_VIEW_FAILED_DOWNLOAD_ACTIONSHEET_TITLE", comment
: "Action sheet title after tapping on failed download.")
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"")
style:UIAlertActionStyleCancel
handler:nil];
[actionSheetController addAction:dismissAction];
UIAlertAction *deleteMessageAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", @"")
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *_Nonnull action) {
[message remove];
}];
[actionSheetController addAction:deleteMessageAction];
UIAlertAction *resendMessageAction = [UIAlertAction
actionWithTitle:NSLocalizedString(@"MESSAGES_VIEW_FAILED_DOWNLOAD_RETRY_ACTION", @"Action sheet button text")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
OWSAttachmentsProcessor *processor =
[[OWSAttachmentsProcessor alloc] initWithAttachmentPointer:attachmentPointer
networkManager:self.networkManager];
[processor fetchAttachmentsForMessage:message
success:^(TSAttachmentStream *_Nonnull attachmentStream) {
DDLogInfo(
@"%@ Successfully redownloaded attachment in thread: %@", self.tag, message.thread);
}
failure:^(NSError *_Nonnull error) {
DDLogWarn(@"%@ Failed to redownload message with error: %@", self.tag, error);
}];
}];
[actionSheetController addAction:resendMessageAction];
[self presentViewController:actionSheetController animated:YES completion:nil];
}
- (void)handleUnsentMessageTap:(TSOutgoingMessage *)message {
UIAlertController *actionSheetController = [UIAlertController alertControllerWithTitle:message.mostRecentFailureText
message:nil

View File

@ -0,0 +1,135 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
class AttachmentPointerView: UIView {
let TAG = "[AttachmentPointerView]"
let progressView = OWSProgressView()
let nameLabel = UILabel()
let statusLabel = UILabel()
let isIncoming: Bool
let filename: String
let attachmentPointer: TSAttachmentPointer
let genericFilename = NSLocalizedString("ATTACHMENT_DOWNLOADING_DEFAULT_ATTACHMENT_LABEL", comment: "Generic name label when downloading an attachment with no known name.")
var progress: CGFloat = 0 {
didSet {
self.progressView.progress = progress
}
}
required init(attachmentPointer: TSAttachmentPointer, isIncoming: Bool) {
self.isIncoming = isIncoming
self.attachmentPointer = attachmentPointer
let attachmentPointerFilename = attachmentPointer.filename
if let filename = attachmentPointerFilename, !filename.isEmpty {
self.filename = filename
} else {
self.filename = genericFilename
}
super.init(frame: CGRect.zero)
createSubviews()
updateViews()
}
@available(*, unavailable)
override init(frame: CGRect) {
assertionFailure()
// This initializer should never be called, but we assign some bogus values to keep the compiler happy.
self.filename = genericFilename
self.isIncoming = false
self.attachmentPointer = TSAttachmentPointer()
super.init(frame: frame)
self.createSubviews()
self.updateViews()
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
assertionFailure()
// This initializer should never be called, but we assign some bogus values to keep the compiler happy.
self.filename = genericFilename
self.isIncoming = false
self.attachmentPointer = TSAttachmentPointer()
super.init(coder: aDecoder)
self.createSubviews()
self.updateViews()
}
func createSubviews() {
self.addSubview(nameLabel)
// truncate middle to be sure we include file extension
nameLabel.lineBreakMode = .byTruncatingMiddle
nameLabel.textAlignment = .center
nameLabel.textColor = self.textColor
nameLabel.font = UIFont.ows_dynamicTypeBody()
nameLabel.autoPinWidthToSuperview()
nameLabel.autoPinEdge(toSuperviewEdge: .top)
self.addSubview(progressView)
progressView.autoPinWidthToSuperview()
progressView.autoPinEdge(.top, to: .bottom, of: nameLabel, withOffset: 6)
progressView.autoSetDimension(.height, toSize: 8)
self.addSubview(statusLabel)
statusLabel.textAlignment = .center
statusLabel.adjustsFontSizeToFitWidth = true
statusLabel.textColor = self.textColor
statusLabel.font = UIFont.ows_footnote()
statusLabel.autoPinWidthToSuperview()
statusLabel.autoPinEdge(.top, to: .bottom, of: progressView, withOffset: 4)
statusLabel.autoPinEdge(toSuperviewEdge: .bottom)
}
func emojiForContentType(_ contentType: String) -> String {
if MIMETypeUtil.isImage(contentType) {
return "📷"
} else if MIMETypeUtil.isVideo(contentType) {
return "📽"
} else if MIMETypeUtil.isAudio(contentType) {
return "📻"
} else if MIMETypeUtil.isAnimated(contentType) {
return "🎡"
} else {
// generic file
return "📁"
}
}
func updateViews() {
let emoji = self.emojiForContentType(self.attachmentPointer.contentType)
nameLabel.text = "\(emoji) \(self.filename)"
statusLabel.text = {
switch self.attachmentPointer.state {
case .enqueued:
return NSLocalizedString("ATTACHMENT_DOWNLOADING_STATUS_QUEUED", comment: "Status label when an attachment is enqueued, but hasn't yet started downloading")
case .downloading:
return NSLocalizedString("ATTACHMENT_DOWNLOADING_STATUS_IN_PROGRESS", comment: "Status label when an attachment is currently downloading")
case .failed:
return NSLocalizedString("ATTACHMENT_DOWNLOADING_STATUS_FAILED", comment: "Status label when an attachment download has failed.")
}
}()
if attachmentPointer.state == .downloading {
progressView.isHidden = false
} else {
progressView.isHidden = true
}
}
var textColor: UIColor {
return self.isIncoming ? UIColor.darkText : UIColor.white
}
}

View File

@ -61,11 +61,17 @@
/* Label for 'send' button in the 'attachment approval' dialog. */
"ATTACHMENT_APPROVAL_SEND_BUTTON" = "Send";
/* No comment provided by engineer. */
"ATTACHMENT_DOWNLOAD_FAILED" = "Attachment download failed, tap to retry.";
/* Generic name label when downloading an attachment with no known name. */
"ATTACHMENT_DOWNLOADING_DEFAULT_ATTACHMENT_LABEL" = "Attachment";
/* No comment provided by engineer. */
"ATTACHMENT_DOWNLOADING" = "Attachment is downloading";
/* Status label when an attachment download has failed. */
"ATTACHMENT_DOWNLOADING_STATUS_FAILED" = "Failed. Tap to retry.";
/* Status label when an attachment is currently downloading */
"ATTACHMENT_DOWNLOADING_STATUS_IN_PROGRESS" = "Downloading...";
/* Status label when an attachment is enqueued, but hasn't yet started downloading */
"ATTACHMENT_DOWNLOADING_STATUS_QUEUED" = "Queued";
/* The title of the 'attachment error' alert. */
"ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment";
@ -94,9 +100,6 @@
/* Accessibility label for attaching photos */
"ATTACHMENT_LABEL" = "Attachment";
/* No comment provided by engineer. */
"ATTACHMENT_QUEUED" = "New attachment queued for retrieval.";
/* An explanation of the consequences of blocking another user. */
"BLOCK_BEHAVIOR_EXPLANATION" = "Blocked users will not be able to call you or send you messages.";
@ -607,6 +610,12 @@
/* Indicates that this 1:1 conversation has been blocked. */
"MESSAGES_VIEW_CONTACT_BLOCKED" = "You Blocked this User";
/* Action sheet title after tapping on failed download. */
"MESSAGES_VIEW_FAILED_DOWNLOAD_ACTIONSHEET_TITLE" = "Download Failed.";
/* Action sheet button text */
"MESSAGES_VIEW_FAILED_DOWNLOAD_RETRY_ACTION" = "Download Again";
/* Indicates that a single member of this group has been blocked. */
"MESSAGES_VIEW_GROUP_1_MEMBER_BLOCKED" = "You Blocked 1 Member of this Group";
@ -818,7 +827,7 @@
"REGISTER_CONTACTS_WELCOME" = "Welcome!";
/* No comment provided by engineer. */
"REGISTER_FAILED_TRY_AGAIN" = "Try again";
"REGISTER_FAILED_TRY_AGAIN" = "Try Again";
/* No comment provided by engineer. */
"REGISTER_RATE_LIMITING_BODY" = "You have tried too often. Please wait a minute before trying again.";
@ -896,7 +905,7 @@
"SECURE_SESSION_RESET" = "Secure session was reset.";
/* No comment provided by engineer. */
"SEND_AGAIN_BUTTON" = "Send again";
"SEND_AGAIN_BUTTON" = "Send Again";
/* No comment provided by engineer. */
"SEND_BUTTON_TITLE" = "Send";