Add AttachmentApprovalViewController.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent cd928cd9be
commit 27b515ea45

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@ -1545,11 +1545,11 @@
<viewControllerLayoutGuide type="bottom" id="kH6-9L-pzh"/> <viewControllerLayoutGuide type="bottom" id="kH6-9L-pzh"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="P0X-AM-Yjw"> <view key="view" contentMode="scaleToFill" id="P0X-AM-Yjw">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <rect key="frame" x="0.0" y="64" width="375" height="603"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ukg-om-VX3" userLabel="Group Details"> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ukg-om-VX3" userLabel="Group Details">
<rect key="frame" x="0.0" y="20" width="375" height="100"/> <rect key="frame" x="0.0" y="0.0" width="375" height="100"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ul8-NY-i4c"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ul8-NY-i4c">
<rect key="frame" x="8" y="20" width="60" height="60"/> <rect key="frame" x="8" y="20" width="60" height="60"/>
@ -1584,7 +1584,7 @@
</constraints> </constraints>
</view> </view>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsMultipleSelection="YES" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="cFo-AT-Srf"> <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsMultipleSelection="YES" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="cFo-AT-Srf">
<rect key="frame" x="0.0" y="128" width="375" height="539"/> <rect key="frame" x="0.0" y="108" width="375" height="495"/>
<color key="backgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.94901960780000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.94901960780000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<view key="tableHeaderView" contentMode="scaleToFill" id="ekO-kw-iHV" userLabel="Header View"> <view key="tableHeaderView" contentMode="scaleToFill" id="ekO-kw-iHV" userLabel="Header View">
<rect key="frame" x="0.0" y="0.0" width="375" height="40"/> <rect key="frame" x="0.0" y="0.0" width="375" height="40"/>
@ -1721,8 +1721,8 @@
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/> <simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
</simulatedMetricsContainer> </simulatedMetricsContainer>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="E8S-Yc-X7E"/>
<segue reference="I6y-pT-nEd"/>
<segue reference="wgA-Oo-kKq"/> <segue reference="wgA-Oo-kKq"/>
<segue reference="D0d-4f-lcI"/>
<segue reference="G2B-Fr-Ezs"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
</document> </document>

@ -18,5 +18,7 @@
+ (UIColor *)ows_errorMessageBorderColor; + (UIColor *)ows_errorMessageBorderColor;
+ (UIColor *)ows_infoMessageBorderColor; + (UIColor *)ows_infoMessageBorderColor;
+ (UIColor *)backgroundColorForContact:(NSString *)contactIdentifier; + (UIColor *)backgroundColorForContact:(NSString *)contactIdentifier;
+ (UIColor *)colorWithRGBHex:(unsigned long)value;
+ (UIColor *)colorWithARGBHex:(unsigned long)value;
@end @end

@ -107,4 +107,21 @@
return [colors objectAtIndex:(choose % [colors count])]; return [colors objectAtIndex:(choose % [colors count])];
} }
+ (UIColor *)colorWithRGBHex:(unsigned long)value
{
CGFloat red = ((value >> 16) & 0xff) / 255.f;
CGFloat green = ((value >> 8) & 0xff) / 255.f;
CGFloat blue = ((value >> 0) & 0xff) / 255.f;
return [UIColor colorWithRed:red green:green blue:blue alpha:1.f];
}
+ (UIColor *)colorWithARGBHex:(unsigned long)value
{
CGFloat alpha = ((value >> 24) & 0xff) / 255.f;
CGFloat red = ((value >> 16) & 0xff) / 255.f;
CGFloat green = ((value >> 8) & 0xff) / 255.f;
CGFloat blue = ((value >> 0) & 0xff) / 255.f;
return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
}
@end @end

@ -2496,11 +2496,15 @@ typedef enum : NSUInteger {
[attachment hasError]) { [attachment hasError]) {
// TODO: Add UI. // TODO: Add UI.
} else { } else {
__weak MessagesViewController *weakSelf = self;
// TODO: Add UI. UIViewController *viewController = [[AttachmentApprovalViewController alloc] initWithAttachment:attachment
UIViewController *viewController = [[AttachmentApprovalViewController alloc] initWithAttachment:attachment]; successCompletion:^{
// [self tryToSendMessageAttachmentWithData:imageData [weakSelf sendMessageAttachment:attachment];
// dataUTI:dataUTI]; }];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[self.navigationController presentViewController:navigationController
animated:YES
completion:nil];
} }
} }

@ -51,18 +51,23 @@ class SignalAttachment: NSObject {
} }
} }
// To avoid redundant work of repeatedly compressing/uncompressing
// images, we cache the UIImage associated with this attachment if
// possible.
public var image: UIImage?
// MARK: Constants // MARK: Constants
/** /**
* Media Size constraints from Signal-Android * Media Size constraints from Signal-Android
* (org/thoughtcrime/securesms/mms/PushMediaConstraints.java) * (org/thoughtcrime/securesms/mms/PushMediaConstraints.java)
*/ */
static let kMaxFileSize_Gif = 5 * 1024 * 1024 static let kMaxFileSizeGif = 5 * 1024 * 1024
static let kMaxFileSize_Image = 420 * 1024 static let kMaxFileSizeImage = 420 * 1024
static let kMaxFileSize_Video = 100 * 1024 * 1024 static let kMaxFileSizeVideo = 100 * 1024 * 1024
static let kMaxFileSize_Audio = 100 * 1024 * 1024 static let kMaxFileSizeAudio = 100 * 1024 * 1024
// TODO: What should the max file size on "other" attachments be? // TODO: What should the max file size on "other" attachments be?
static let kMaxFileSize_Generic = 25 * 1024 * 1024 static let kMaxFileSizeGeneric = 100 * 1024 * 1024
// MARK: Constructor // MARK: Constructor
@ -88,6 +93,16 @@ class SignalAttachment: NSObject {
return mimeType?.takeRetainedValue() as? String return mimeType?.takeRetainedValue() as? String
} }
// Returns the file extension for this attachment or nil if no file extension
// can be identified.
public func fileExtension() -> String? {
let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassFilenameExtension)
guard fileExtension != nil else {
return nil
}
return fileExtension?.takeRetainedValue() as? String
}
// Returns the set of UTIs that correspond to valid _input_ image formats // Returns the set of UTIs that correspond to valid _input_ image formats
// for Signal attachments. // for Signal attachments.
// //
@ -99,7 +114,7 @@ class SignalAttachment: NSObject {
return [ return [
kUTTypeJPEG as String, kUTTypeJPEG as String,
kUTTypeGIF as String, kUTTypeGIF as String,
kUTTypePNG as String, kUTTypePNG as String
] ]
} }
@ -111,7 +126,7 @@ class SignalAttachment: NSObject {
return [ return [
kUTTypeJPEG as String, kUTTypeJPEG as String,
kUTTypeGIF as String, kUTTypeGIF as String,
kUTTypePNG as String, kUTTypePNG as String
] ]
} }
@ -121,7 +136,7 @@ class SignalAttachment: NSObject {
// TODO: We need to finalize which formats we support. // TODO: We need to finalize which formats we support.
private class func videoUTISet() -> Set<String>! { private class func videoUTISet() -> Set<String>! {
return [ return [
kUTTypeMPEG4 as String, kUTTypeMPEG4 as String
] ]
} }
@ -132,7 +147,7 @@ class SignalAttachment: NSObject {
private class func audioUTISet() -> Set<String>! { private class func audioUTISet() -> Set<String>! {
return [ return [
kUTTypeMP3 as String, kUTTypeMP3 as String,
kUTTypeMPEG4Audio as String, kUTTypeMPEG4Audio as String
] ]
} }
@ -142,6 +157,18 @@ class SignalAttachment: NSObject {
return inputImageUTISet().union(videoUTISet().union(audioUTISet())) return inputImageUTISet().union(videoUTISet().union(audioUTISet()))
} }
public func isImage() -> Bool {
return SignalAttachment.outputImageUTISet().contains(dataUTI)
}
public func isVideo() -> Bool {
return SignalAttachment.videoUTISet().contains(dataUTI)
}
public func isAudio() -> Bool {
return SignalAttachment.audioUTISet().contains(dataUTI)
}
// Returns an attachment from the pasteboard, or nil if no attachment // Returns an attachment from the pasteboard, or nil if no attachment
// can be found. // can be found.
// //
@ -206,7 +233,7 @@ class SignalAttachment: NSObject {
} }
if dataUTI == kUTTypeGIF as String { if dataUTI == kUTTypeGIF as String {
guard imageData.count <= kMaxFileSize_Gif else { guard imageData.count <= kMaxFileSizeGif else {
attachment.error = .fileSizeTooLarge attachment.error = .fileSizeTooLarge
return attachment return attachment
} }
@ -214,33 +241,46 @@ class SignalAttachment: NSObject {
// animated. // animated.
// //
// TODO: Consider re-encoding non-animated GIFs as JPEG? // TODO: Consider re-encoding non-animated GIFs as JPEG?
Logger.verbose("\(TAG) Sending raw image/gif to retain any animation") Logger.verbose("\(TAG) Sending raw \(attachment.mimeType()) to retain any animation")
return attachment return attachment
} else { } else {
guard let image = UIImage(data:imageData) else { guard let image = UIImage(data:imageData) else {
attachment.error = .couldNotParseImage attachment.error = .couldNotParseImage
return attachment return attachment
} }
attachment.image = image
if isInputImageValidOutputImage(image: image, imageData: imageData, dataUTI: dataUTI) {
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType())")
return attachment
}
Logger.verbose("\(TAG) Compressing attachment as image/jpeg")
return compressImageAsJPEG(image : image, attachment : attachment)
}
}
// If the proposed attachment already is a JPEG, // If the proposed attachment already is a JPEG, and already conforms to the
// and already conforms to the file size and // file size and content size limits, don't recompress it.
// content size limits, don't recompress it.
// //
// TODO: Should non-JPEGs always be converted to JPEG? // TODO: Should non-JPEGs always be converted to JPEG?
private class func isInputImageValidOutputImage(image: UIImage?, imageData: Data?, dataUTI: String!) -> Bool {
guard let image = image else {
return false
}
guard let imageData = imageData else {
return false
}
if dataUTI == kUTTypeJPEG as String { if dataUTI == kUTTypeJPEG as String {
let imageUploadQuality = Environment.preferences().imageUploadQuality() let imageUploadQuality = Environment.preferences().imageUploadQuality()
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality) let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
if (image.size.width <= maxSize && if image.size.width <= maxSize &&
image.size.height <= maxSize && image.size.height <= maxSize &&
imageData.count <= kMaxFileSize_Image) { imageData.count <= kMaxFileSizeImage {
Logger.verbose("\(TAG) Sending raw image/jpeg") return true
return attachment
} }
} }
return false
Logger.verbose("\(TAG) Converting attachment to image/jpeg")
return recompressImageAsJPEG(image : image, attachment : attachment)
}
} }
// Factory method for an image attachment. // Factory method for an image attachment.
@ -256,27 +296,34 @@ class SignalAttachment: NSObject {
// Make a placeholder attachment on which to hang errors if necessary. // Make a placeholder attachment on which to hang errors if necessary.
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI) let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
attachment.image = image
Logger.verbose("\(TAG) Converting attachment to image/jpeg") Logger.verbose("\(TAG) Writing \(attachment.mimeType()) as image/jpeg")
return recompressImageAsJPEG(image : image, attachment : attachment) return compressImageAsJPEG(image : image, attachment : attachment)
} }
private class func recompressImageAsJPEG(image : UIImage!, attachment : SignalAttachment!) -> SignalAttachment! { private class func compressImageAsJPEG(image: UIImage!, attachment: SignalAttachment!) -> SignalAttachment! {
assert(attachment.error == nil) assert(attachment.error == nil)
var imageUploadQuality = Environment.preferences().imageUploadQuality() var imageUploadQuality = Environment.preferences().imageUploadQuality()
while true { while true {
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality) let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
let resizedImage = imageScaled(image, toMaxSize: maxSize) var dstImage: UIImage! = image
guard let jpgImageData = UIImageJPEGRepresentation(resizedImage, if image.size.width > maxSize ||
image.size.height > maxSize {
dstImage = imageScaled(image, toMaxSize: maxSize)
}
guard let jpgImageData = UIImageJPEGRepresentation(dstImage,
jpegCompressionQuality(imageUploadQuality:imageUploadQuality)) else { jpegCompressionQuality(imageUploadQuality:imageUploadQuality)) else {
attachment.error = .couldNotConvertToJpeg attachment.error = .couldNotConvertToJpeg
return attachment return attachment
} }
if jpgImageData.count <= kMaxFileSize_Image { if jpgImageData.count <= kMaxFileSizeImage {
return SignalAttachment(data : jpgImageData, dataUTI: kUTTypeJPEG as String) let recompressedAttachment = SignalAttachment(data : jpgImageData, dataUTI: kUTTypeJPEG as String)
recompressedAttachment.image = dstImage
return recompressedAttachment
} }
// If the JPEG output is larger than the file size limit, // If the JPEG output is larger than the file size limit,
@ -301,8 +348,7 @@ class SignalAttachment: NSObject {
let aspectRatio: CGFloat = image.size.height / image.size.width let aspectRatio: CGFloat = image.size.height / image.size.width
if aspectRatio > 1 { if aspectRatio > 1 {
scaleFactor = size / image.size.width scaleFactor = size / image.size.width
} } else {
else {
scaleFactor = size / image.size.height scaleFactor = size / image.size.height
} }
let newSize = CGSize(width: CGFloat(image.size.width * scaleFactor), height: CGFloat(image.size.height * scaleFactor)) let newSize = CGSize(width: CGFloat(image.size.width * scaleFactor), height: CGFloat(image.size.height * scaleFactor))
@ -349,7 +395,7 @@ class SignalAttachment: NSObject {
return newAttachment(withData : data, return newAttachment(withData : data,
dataUTI : dataUTI, dataUTI : dataUTI,
validUTISet : videoUTISet(), validUTISet : videoUTISet(),
maxFileSize : kMaxFileSize_Video) maxFileSize : kMaxFileSizeVideo)
} }
// MARK: Audio Attachments // MARK: Audio Attachments
@ -362,7 +408,7 @@ class SignalAttachment: NSObject {
return newAttachment(withData : data, return newAttachment(withData : data,
dataUTI : dataUTI, dataUTI : dataUTI,
validUTISet : audioUTISet(), validUTISet : audioUTISet(),
maxFileSize : kMaxFileSize_Audio) maxFileSize : kMaxFileSizeAudio)
} }
// MARK: Generic Attachments // MARK: Generic Attachments
@ -375,7 +421,7 @@ class SignalAttachment: NSObject {
return newAttachment(withData : data, return newAttachment(withData : data,
dataUTI : dataUTI, dataUTI : dataUTI,
validUTISet : nil, validUTISet : nil,
maxFileSize : kMaxFileSize_Generic) maxFileSize : kMaxFileSizeGeneric)
} }
// MARK: Helper Methods // MARK: Helper Methods

@ -52,6 +52,21 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"ATTACHMENT" = "Attachment"; "ATTACHMENT" = "Attachment";
/* Label for 'cancel' button in the 'attachment approval' dialog. */
"ATTACHMENT_APPROVAL_CANCEL_BUTTON" = "Cancel";
/* Title for the 'attachment approval' dialog. */
"ATTACHMENT_APPROVAL_DIALOG_TITLE" = "Attachment";
/* Format string for file extension label in call interstitial view */
"ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT" = "File type: %@";
/* Format string for file size label in call interstitial view */
"ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT" = "File size: %@";
/* Label for 'send' button in the 'attachment approval' dialog. */
"ATTACHMENT_APPROVAL_SEND_BUTTON" = "Send";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"ATTACHMENT_DOWNLOAD_FAILED" = "Attachment download failed, tap to retry."; "ATTACHMENT_DOWNLOAD_FAILED" = "Attachment download failed, tap to retry.";

Loading…
Cancel
Save