From d9e3e87735b97cfe212057c4094e5b10e50ee720 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 19 Apr 2017 18:50:27 -0400 Subject: [PATCH] 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 --- Podfile.lock | 2 +- Signal.xcodeproj/project.pbxproj | 8 ++ .../AttachmentPointerAdapter.swift | 110 ++++++++++++++ .../TSMessageAdapaters/TSMessageAdapter.m | 16 +-- Signal/src/Signal-Bridging-Header.h | 7 + .../ViewControllers/MessagesViewController.m | 111 +++++++++----- Signal/src/views/AttachmentPointerView.swift | 135 ++++++++++++++++++ .../translations/en.lproj/Localizable.strings | 27 ++-- 8 files changed, 355 insertions(+), 61 deletions(-) create mode 100644 Signal/src/Models/TSMessageAdapaters/AttachmentPointerAdapter.swift create mode 100644 Signal/src/views/AttachmentPointerView.swift diff --git a/Podfile.lock b/Podfile.lock index 5689a230d..14edb17b8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -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 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 158907289..be126cb98 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -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 = ""; }; 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = ""; }; 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = ""; }; + 452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerAdapter.swift; sourceTree = ""; }; + 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = ""; }; 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageFetcherJob.swift; path = Jobs/MessageFetcherJob.swift; sourceTree = ""; }; 4531C9C21DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMessagesCollectionViewCell+OWS.h"; sourceTree = ""; }; 4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMessagesCollectionViewCell+OWS.m"; sourceTree = ""; }; @@ -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 */, diff --git a/Signal/src/Models/TSMessageAdapaters/AttachmentPointerAdapter.swift b/Signal/src/Models/TSMessageAdapaters/AttachmentPointerAdapter.swift new file mode 100644 index 000000000..84aeb8d16 --- /dev/null +++ b/Signal/src/Models/TSMessageAdapaters/AttachmentPointerAdapter.swift @@ -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 + } + } +} diff --git a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m index dc88745d0..16229c5cb 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m @@ -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])); diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 86eb96639..ea74aff40 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -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 +#import +#import #import #import #import @@ -36,6 +41,7 @@ #import #import #import +#import #import #import #import @@ -53,6 +59,7 @@ #import #import #import +#import #import #import #import diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index 8f272469a..c2019075b 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -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 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 diff --git a/Signal/src/views/AttachmentPointerView.swift b/Signal/src/views/AttachmentPointerView.swift new file mode 100644 index 000000000..f5c03ed6f --- /dev/null +++ b/Signal/src/views/AttachmentPointerView.swift @@ -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 + } +} diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 886ea8715..e7fb1741d 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -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";