Merge branch 'charlesmchen/paste'
This commit is contained in:
commit
ec06cf76e4
@ -174,4 +174,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 48dbf2fb380d626bb799a782dd41b6bf1e466506
|
||||
|
||||
COCOAPODS: 1.2.0
|
||||
COCOAPODS: 1.1.1
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */; };
|
||||
344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F2F661E57A932000D9322 /* UIViewController+OWS.m */; };
|
||||
34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; };
|
||||
348A08421E6A044E0057E290 /* MessagesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 348A08411E6A044E0057E290 /* MessagesViewController.xib */; };
|
||||
348A08441E6A1D2C0057E290 /* OWSMessagesToolbarContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 348A08431E6A1D2C0057E290 /* OWSMessagesToolbarContentView.xib */; };
|
||||
348A08511E6C73490057E290 /* AttachmentApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348A08501E6C73490057E290 /* AttachmentApprovalViewController.swift */; };
|
||||
348A08531E6C75590057E290 /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348A08521E6C75590057E290 /* SignalAttachment.swift */; };
|
||||
348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */; };
|
||||
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
|
||||
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; };
|
||||
@ -624,6 +628,10 @@
|
||||
344F2F661E57A932000D9322 /* UIViewController+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+OWS.m"; path = "util/UIViewController+OWS.m"; sourceTree = "<group>"; };
|
||||
34535D801E256BE9008A4747 /* UIView+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+OWS.h"; sourceTree = "<group>"; };
|
||||
34535D811E256BE9008A4747 /* UIView+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+OWS.m"; sourceTree = "<group>"; };
|
||||
348A08411E6A044E0057E290 /* MessagesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessagesViewController.xib; sourceTree = "<group>"; };
|
||||
348A08431E6A1D2C0057E290 /* OWSMessagesToolbarContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSMessagesToolbarContentView.xib; sourceTree = "<group>"; };
|
||||
348A08501E6C73490057E290 /* AttachmentApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentApprovalViewController.swift; sourceTree = "<group>"; };
|
||||
348A08521E6C75590057E290 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAttachment.swift; sourceTree = "<group>"; };
|
||||
348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInterstitialViewController.swift; sourceTree = "<group>"; };
|
||||
34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = "<group>"; };
|
||||
34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAnyTouchGestureRecognizer.m; path = views/OWSAnyTouchGestureRecognizer.m; sourceTree = "<group>"; };
|
||||
@ -2653,6 +2661,7 @@
|
||||
FC3196321A08142D0094C78E /* Signals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
348A08501E6C73490057E290 /* AttachmentApprovalViewController.swift */,
|
||||
348F3A4E1E4A533900750D44 /* CallInterstitialViewController.swift */,
|
||||
4509E79B1DD6545B0025A59F /* CallViewController.swift */,
|
||||
FC31962B1A06A2190094C78E /* FingerprintViewController.h */,
|
||||
@ -2663,12 +2672,15 @@
|
||||
FC3196291A067D8F0094C78E /* MessageComposeTableViewController.m */,
|
||||
FCAC964F19FF0A6E0046DFC5 /* MessagesViewController.h */,
|
||||
FCAC965019FF0A6E0046DFC5 /* MessagesViewController.m */,
|
||||
348A08411E6A044E0057E290 /* MessagesViewController.xib */,
|
||||
FCFD256D1A151BCB00F4C644 /* NewGroupViewController.h */,
|
||||
FCFD256E1A151BCB00F4C644 /* NewGroupViewController.m */,
|
||||
452E3C8C1D935C77002A45B0 /* OWSConversationSettingsTableViewController.h */,
|
||||
452E3C8D1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m */,
|
||||
348A08431E6A1D2C0057E290 /* OWSMessagesToolbarContentView.xib */,
|
||||
A5D0699A1A50E9CB004CB540 /* ShowGroupMembersViewController.h */,
|
||||
A5D069991A50E9CB004CB540 /* ShowGroupMembersViewController.m */,
|
||||
348A08521E6C75590057E290 /* SignalAttachment.swift */,
|
||||
FC4FA0241A1B9DC600DA100A /* SignalsNavigationController.h */,
|
||||
FC4FA0251A1B9DC600DA100A /* SignalsNavigationController.m */,
|
||||
FCAC963A19FEF9280046DFC5 /* SignalsViewController.h */,
|
||||
@ -2897,6 +2909,7 @@
|
||||
E94066151DFC5B7B00B15392 /* ContactsPicker.xib in Resources */,
|
||||
AD41D7B61A6F6F0600241130 /* play_button@2x.png in Resources */,
|
||||
AD83FF3F1A73426500B5C81A /* audio_pause_button_blue.png in Resources */,
|
||||
348A08421E6A044E0057E290 /* MessagesViewController.xib in Resources */,
|
||||
45E1F3A31DEF1DF000852CF1 /* NoSignalContactsView.xib in Resources */,
|
||||
A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */,
|
||||
A507A3B11A6C60E300BEED0D /* InboxTableViewCell.xib in Resources */,
|
||||
@ -2941,6 +2954,7 @@
|
||||
E1370BE618A0686C00826894 /* sonarping.mp3 in Resources */,
|
||||
B10C9B5F1A7049EC00ECA2BF /* pause_icon.png in Resources */,
|
||||
AD83FF471A73428300B5C81A /* audio_play_button_blue.png in Resources */,
|
||||
348A08441E6A1D2C0057E290 /* OWSMessagesToolbarContentView.xib in Resources */,
|
||||
AD83FF451A73426500B5C81A /* audio_pause_button@2x.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -3137,6 +3151,7 @@
|
||||
45387B041E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */,
|
||||
E197B61818BBEC1A00F073E5 /* RemoteIOAudio.m in Sources */,
|
||||
B67ADDC41989FF8700E1A773 /* RPServerRequestsManager.m in Sources */,
|
||||
348A08511E6C73490057E290 /* AttachmentApprovalViewController.swift in Sources */,
|
||||
348F3A4F1E4A533900750D44 /* CallInterstitialViewController.swift in Sources */,
|
||||
EF764C351DB67CC5000D9A87 /* UIViewController+CameraPermissions.m in Sources */,
|
||||
453201251E71100C00F20761 /* DisplayableTextFilter.swift in Sources */,
|
||||
@ -3147,6 +3162,7 @@
|
||||
76EB05E018170B33006006FC /* NetworkStream.m in Sources */,
|
||||
45794E861E00620000066731 /* CallUIAdapter.swift in Sources */,
|
||||
FCFA64B71A24F6730007FB87 /* UIFont+OWS.m in Sources */,
|
||||
348A08531E6C75590057E290 /* SignalAttachment.swift in Sources */,
|
||||
B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */,
|
||||
45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */,
|
||||
76EB05D618170B33006006FC /* ZrtpResponder.m in Sources */,
|
||||
|
||||
21
Signal/Images.xcassets/file-icon-large.imageset/Contents.json
vendored
Normal file
21
Signal/Images.xcassets/file-icon-large.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/file-icon-large.imageset/file-icon-large@1x.png
vendored
Normal file
BIN
Signal/Images.xcassets/file-icon-large.imageset/file-icon-large@1x.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@ -32,6 +32,7 @@
|
||||
#import <SignalServiceKit/Contact.h>
|
||||
#import <SignalServiceKit/ContactsUpdater.h>
|
||||
#import <SignalServiceKit/Cryptography.h>
|
||||
#import <SignalServiceKit/MIMETypeUtil.h>
|
||||
#import <SignalServiceKit/NSData+Base64.h>
|
||||
#import <SignalServiceKit/NSDate+millisecondTimeStamp.h>
|
||||
#import <SignalServiceKit/OWSAcknowledgeMessageDeliveryRequest.h>
|
||||
|
||||
@ -120,12 +120,33 @@
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="inputToolbar" destination="BdJ-vY-dHA" id="EfK-zI-nSM"/>
|
||||
<segue destination="4oU-Rv-yJi" kind="push" identifier="OWSMessagesViewControllerSeguePushConversationSettings" id="vOd-aS-6Wx"/>
|
||||
<segue destination="urv-62-RsD" kind="modal" identifier="fingerprintSegue" id="tfr-ZV-qWs"/>
|
||||
<segue destination="Tyf-mN-gzf" kind="modal" identifier="initiateCallSegue" modalTransitionStyle="crossDissolve" id="I6y-pT-nEd"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="yXZ-iE-5va" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="BdJ-vY-dHA" customClass="OWSMessagesInputToolbar">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<items>
|
||||
<barButtonItem title="Item" id="4F3-az-L9s"/>
|
||||
</items>
|
||||
</toolbar>
|
||||
<view contentMode="scaleToFill" id="X6H-b7-xZK" customClass="OWSMessagesToolbarContentView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="240" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<connections>
|
||||
<outlet property="textView" destination="XUl-oy-JS1" id="kzd-TM-IJT"/>
|
||||
</connections>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" id="XUl-oy-JS1" customClass="JSQMessagesComposerTextView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="240" height="128"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2287" y="-1516"/>
|
||||
</scene>
|
||||
@ -352,7 +373,7 @@
|
||||
<!--Conversation Settings-->
|
||||
<scene sceneID="Flt-X5-Amc">
|
||||
<objects>
|
||||
<tableViewController title="Contact Information" id="4oU-Rv-yJi" userLabel="Conversation Settings" customClass="OWSConversationSettingsTableViewController" sceneMemberID="viewController">
|
||||
<tableViewController storyboardIdentifier="OWSConversationSettingsTableViewController" title="Contact Information" id="4oU-Rv-yJi" userLabel="Conversation Settings" customClass="OWSConversationSettingsTableViewController" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" sectionIndexMinimumDisplayRowCount="1" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="Dgp-fo-Lyr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@ -625,7 +646,7 @@
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ftx-dN-loa" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-1393" y="-1482"/>
|
||||
<point key="canvasLocation" x="-1448" y="-1570"/>
|
||||
</scene>
|
||||
<!--Show Group Members View Controller-->
|
||||
<scene sceneID="VBt-Ax-0G9">
|
||||
@ -1524,11 +1545,11 @@
|
||||
<viewControllerLayoutGuide type="bottom" id="kH6-9L-pzh"/>
|
||||
</layoutGuides>
|
||||
<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"/>
|
||||
<subviews>
|
||||
<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>
|
||||
<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"/>
|
||||
@ -1563,7 +1584,7 @@
|
||||
</constraints>
|
||||
</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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -1700,8 +1721,8 @@
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="E8S-Yc-X7E"/>
|
||||
<segue reference="wgA-Oo-kKq"/>
|
||||
<segue reference="D0d-4f-lcI"/>
|
||||
<segue reference="G2B-Fr-Ezs"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
</document>
|
||||
|
||||
@ -18,5 +18,6 @@
|
||||
+ (UIColor *)ows_errorMessageBorderColor;
|
||||
+ (UIColor *)ows_infoMessageBorderColor;
|
||||
+ (UIColor *)backgroundColorForContact:(NSString *)contactIdentifier;
|
||||
+ (UIColor *)colorWithRGBHex:(unsigned long)value;
|
||||
|
||||
@end
|
||||
|
||||
@ -107,4 +107,12 @@
|
||||
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];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -13,13 +13,6 @@ typedef NS_ENUM(NSUInteger, NotificationType) {
|
||||
NotificationNamePreview,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, TSImageQuality) {
|
||||
TSImageQualityUncropped = 1,
|
||||
TSImageQualityHigh = 2,
|
||||
TSImageQualityMedium = 3,
|
||||
TSImageQualityLow = 4
|
||||
};
|
||||
|
||||
// Used when migrating logging to NSUserDefaults.
|
||||
extern NSString *const PropertyListPreferencesSignalDatabaseCollection;
|
||||
extern NSString *const PropertyListPreferencesKeyEnableDebugLog;
|
||||
@ -59,8 +52,6 @@ extern NSString *const PropertyListPreferencesKeyEnableDebugLog;
|
||||
- (BOOL)hasRegisteredVOIPPush;
|
||||
- (void)setHasRegisteredVOIPPush:(BOOL)enabled;
|
||||
|
||||
- (TSImageQuality)imageUploadQuality;
|
||||
|
||||
+ (nullable NSString *)lastRanVersion;
|
||||
+ (NSString *)setAndGetCurrentVersion;
|
||||
|
||||
|
||||
@ -110,12 +110,6 @@ NSString *const PropertyListPreferencesKeyCallsHideIPAddress = @"CallsHideIPAddr
|
||||
}
|
||||
}
|
||||
|
||||
- (TSImageQuality)imageUploadQuality
|
||||
{
|
||||
// always return average image quality
|
||||
return TSImageQualityMedium;
|
||||
}
|
||||
|
||||
- (void)setScreenSecurity:(BOOL)flag
|
||||
{
|
||||
[self setValueForKey:PropertyListPreferencesKeyScreenSecurity toValue:@(flag)];
|
||||
|
||||
@ -0,0 +1,215 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AttachmentApprovalViewController: UIViewController {
|
||||
|
||||
let TAG = "[AttachmentApprovalViewController]"
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let attachment: SignalAttachment
|
||||
|
||||
var successCompletion : (() -> Void)?
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"use attachment: constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
self.attachment = SignalAttachment.genericAttachment(data: nil,
|
||||
dataUTI: kUTTypeContent as String)
|
||||
super.init(coder: aDecoder)
|
||||
assert(false)
|
||||
}
|
||||
|
||||
required init(attachment: SignalAttachment, successCompletion : @escaping () -> Void) {
|
||||
assert(!attachment.hasError)
|
||||
self.attachment = attachment
|
||||
self.successCompletion = successCompletion
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
// MARK: View Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = UIColor.black
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem:.done,
|
||||
target:self,
|
||||
action:#selector(donePressed))
|
||||
self.navigationItem.title = NSLocalizedString("ATTACHMENT_APPROVAL_DIALOG_TITLE",
|
||||
comment: "Title for the 'attachment approval' dialog.")
|
||||
|
||||
createViews()
|
||||
}
|
||||
|
||||
// MARK: - Create Views
|
||||
|
||||
private func createViews() {
|
||||
let previewTopMargin: CGFloat = 30
|
||||
let previewHMargin: CGFloat = 20
|
||||
|
||||
let attachmentPreviewView = UIView()
|
||||
self.view.addSubview(attachmentPreviewView)
|
||||
attachmentPreviewView.autoPinWidthToSuperview(withMargin:previewHMargin)
|
||||
attachmentPreviewView.autoPin(toTopLayoutGuideOf: self, withInset:previewTopMargin)
|
||||
|
||||
createButtonRow(attachmentPreviewView:attachmentPreviewView)
|
||||
|
||||
if attachment.isImage {
|
||||
createImagePreview(attachmentPreviewView:attachmentPreviewView)
|
||||
} else {
|
||||
createGenericPreview(attachmentPreviewView:attachmentPreviewView)
|
||||
}
|
||||
}
|
||||
|
||||
private func createImagePreview(attachmentPreviewView: UIView) {
|
||||
var image = attachment.image
|
||||
if image == nil {
|
||||
image = UIImage(data:attachment.data)
|
||||
}
|
||||
if image != nil {
|
||||
let imageView = UIImageView(image:image)
|
||||
imageView.layer.minificationFilter = kCAFilterTrilinear
|
||||
imageView.layer.magnificationFilter = kCAFilterTrilinear
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
attachmentPreviewView.addSubview(imageView)
|
||||
imageView.autoPinWidthToSuperview()
|
||||
imageView.autoPinHeightToSuperview()
|
||||
} else {
|
||||
createGenericPreview(attachmentPreviewView:attachmentPreviewView)
|
||||
}
|
||||
}
|
||||
|
||||
private func createGenericPreview(attachmentPreviewView: UIView) {
|
||||
let stackView = UIView()
|
||||
attachmentPreviewView.addSubview(stackView)
|
||||
stackView.autoCenterInSuperview()
|
||||
|
||||
let imageSize = ScaleFromIPhone5To7Plus(175, 225)
|
||||
let image = UIImage(named:"file-icon-large")
|
||||
assert(image != nil)
|
||||
let imageView = UIImageView(image:image)
|
||||
imageView.layer.minificationFilter = kCAFilterTrilinear
|
||||
imageView.layer.magnificationFilter = kCAFilterTrilinear
|
||||
stackView.addSubview(imageView)
|
||||
imageView.autoHCenterInSuperview()
|
||||
imageView.autoPinEdge(toSuperviewEdge:.top)
|
||||
imageView.autoSetDimension(.width, toSize:imageSize)
|
||||
imageView.autoSetDimension(.height, toSize:imageSize)
|
||||
|
||||
var lastView: UIView = imageView
|
||||
|
||||
let labelFont = UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(18, 24))
|
||||
|
||||
if let fileExtension = attachment.fileExtension {
|
||||
let fileExtensionLabel = UILabel()
|
||||
fileExtensionLabel.text = String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT",
|
||||
comment: "Format string for file extension label in call interstitial view"),
|
||||
fileExtension.capitalized)
|
||||
|
||||
fileExtensionLabel.textColor = UIColor.white
|
||||
fileExtensionLabel.font = labelFont
|
||||
fileExtensionLabel.textAlignment = .center
|
||||
stackView.addSubview(fileExtensionLabel)
|
||||
fileExtensionLabel.autoHCenterInSuperview()
|
||||
fileExtensionLabel.autoPinEdge(.top, to:.bottom, of:lastView, withOffset:10)
|
||||
|
||||
lastView = fileExtensionLabel
|
||||
}
|
||||
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = NumberFormatter.Style.decimal
|
||||
let fileSizeLabel = UILabel()
|
||||
fileSizeLabel.text = String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT",
|
||||
comment: "Format string for file size label in call interstitial view"),
|
||||
numberFormatter.string(from: NSNumber(value: attachment.data.count))!)
|
||||
|
||||
fileSizeLabel.textColor = UIColor.white
|
||||
fileSizeLabel.font = labelFont
|
||||
fileSizeLabel.textAlignment = .center
|
||||
stackView.addSubview(fileSizeLabel)
|
||||
fileSizeLabel.autoHCenterInSuperview()
|
||||
fileSizeLabel.autoPinEdge(.top, to:.bottom, of:lastView, withOffset:10)
|
||||
fileSizeLabel.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
}
|
||||
|
||||
private func createButtonRow(attachmentPreviewView: UIView) {
|
||||
let buttonTopMargin = ScaleFromIPhone5To7Plus(30, 40)
|
||||
let buttonBottomMargin = ScaleFromIPhone5To7Plus(25, 40)
|
||||
let buttonHSpacing = ScaleFromIPhone5To7Plus(20, 30)
|
||||
|
||||
let buttonRow = UIView()
|
||||
self.view.addSubview(buttonRow)
|
||||
buttonRow.autoPinWidthToSuperview()
|
||||
buttonRow.autoPinEdge(toSuperviewEdge:.bottom, withInset:buttonBottomMargin)
|
||||
buttonRow.autoPinEdge(.top, to:.bottom, of:attachmentPreviewView, withOffset:buttonTopMargin)
|
||||
|
||||
// We use this invisible subview to ensure that the buttons are centered
|
||||
// horizontally.
|
||||
let buttonSpacer = UIView()
|
||||
buttonRow.addSubview(buttonSpacer)
|
||||
// Vertical positioning of this view doesn't matter.
|
||||
buttonSpacer.autoPinEdge(toSuperviewEdge:.top)
|
||||
buttonSpacer.autoSetDimension(.width, toSize:buttonHSpacing)
|
||||
buttonSpacer.autoHCenterInSuperview()
|
||||
|
||||
let cancelButton = createButton(title: NSLocalizedString("TXT_CANCEL_TITLE",
|
||||
comment: ""),
|
||||
color : UIColor(rgbHex:0xff3B30),
|
||||
action: #selector(cancelPressed))
|
||||
buttonRow.addSubview(cancelButton)
|
||||
cancelButton.autoPinEdge(toSuperviewEdge:.top)
|
||||
cancelButton.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
cancelButton.autoPinEdge(.right, to:.left, of:buttonSpacer)
|
||||
|
||||
let sendButton = createButton(title: NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON",
|
||||
comment: "Label for 'send' button in the 'attachment approval' dialog."),
|
||||
color : UIColor(rgbHex:0x4CD964),
|
||||
action: #selector(sendPressed))
|
||||
buttonRow.addSubview(sendButton)
|
||||
sendButton.autoPinEdge(toSuperviewEdge:.top)
|
||||
sendButton.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
sendButton.autoPinEdge(.left, to:.right, of:buttonSpacer)
|
||||
}
|
||||
|
||||
private func createButton(title: String, color: UIColor, action: Selector) -> UIButton {
|
||||
let buttonFont = UIFont.ows_mediumFont(withSize:ScaleFromIPhone5To7Plus(18, 22))
|
||||
let buttonCornerRadius = ScaleFromIPhone5To7Plus(4, 5)
|
||||
let buttonWidth = ScaleFromIPhone5To7Plus(110, 140)
|
||||
let buttonHeight = ScaleFromIPhone5To7Plus(35, 45)
|
||||
|
||||
let button = UIButton()
|
||||
button.setTitle(title, for:.normal)
|
||||
button.setTitleColor(UIColor.white, for:.normal)
|
||||
button.titleLabel!.font = buttonFont
|
||||
button.backgroundColor = color
|
||||
button.layer.cornerRadius = buttonCornerRadius
|
||||
button.clipsToBounds = true
|
||||
button.addTarget(self, action:action, for:.touchUpInside)
|
||||
button.autoSetDimension(.width, toSize:buttonWidth)
|
||||
button.autoSetDimension(.height, toSize:buttonHeight)
|
||||
return button
|
||||
}
|
||||
|
||||
// MARK: - Event Handlers
|
||||
|
||||
func donePressed(sender: UIButton) {
|
||||
dismiss(animated: true, completion:nil)
|
||||
}
|
||||
|
||||
func cancelPressed(sender: UIButton) {
|
||||
dismiss(animated: true, completion:nil)
|
||||
}
|
||||
|
||||
func sendPressed(sender: UIButton) {
|
||||
let successCompletion = self.successCompletion
|
||||
dismiss(animated: true, completion: {
|
||||
successCompletion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,8 @@
|
||||
//
|
||||
// MessageComposeTableViewController.h
|
||||
//
|
||||
//
|
||||
// Created by Dylan Bourgeois on 02/11/14.
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesComposerTextView.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesInputToolbar.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesToolbarContentView.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesKeyboardController.h>
|
||||
#import "Contact.h"
|
||||
#import "LocalizableText.h"
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
//
|
||||
// MessagesViewController.h
|
||||
// Signal
|
||||
//
|
||||
// Created by Dylan Bourgeois on 28/10/14.
|
||||
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
@ -14,6 +10,24 @@
|
||||
|
||||
extern NSString *const OWSMessagesViewControllerDidAppearNotification;
|
||||
|
||||
@interface OWSMessagesComposerTextView : JSQMessagesComposerTextView
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessagesToolbarContentView : JSQMessagesToolbarContentView
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessagesInputToolbar : JSQMessagesInputToolbar
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface MessagesViewController : JSQMessagesViewController <UIImagePickerControllerDelegate,
|
||||
UINavigationControllerDelegate,
|
||||
UITextViewDelegate,
|
||||
|
||||
@ -74,8 +74,7 @@
|
||||
static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60;
|
||||
|
||||
static NSString *const OWSMessagesViewControllerSegueShowFingerprint = @"fingerprintSegue";
|
||||
static NSString *const OWSMessagesViewControllerSeguePushConversationSettings =
|
||||
@"OWSMessagesViewControllerSeguePushConversationSettings";
|
||||
static NSString *const OWSMessagesViewControllerSeguePushConversationSettings = @"OWSMessagesViewControllerSeguePushConversationSettings";
|
||||
|
||||
NSString *const OWSMessagesViewControllerDidAppearNotification = @"OWSMessagesViewControllerDidAppear";
|
||||
|
||||
@ -84,10 +83,96 @@ typedef enum : NSUInteger {
|
||||
kMediaTypeVideo,
|
||||
} kMediaTypes;
|
||||
|
||||
@interface MessagesViewController () {
|
||||
@protocol OWSTextViewPasteDelegate <NSObject>
|
||||
|
||||
- (void)didPasteAttachment:(SignalAttachment * _Nullable)attachment;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessagesComposerTextView ()
|
||||
|
||||
@property (weak, nonatomic) id<OWSTextViewPasteDelegate> textViewPasteDelegate;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessagesComposerTextView
|
||||
|
||||
- (BOOL)canBecomeFirstResponder {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)pasteBoardHasPossibleAttachment {
|
||||
NSSet *pasteboardUTISet = [NSSet setWithArray:[UIPasteboard generalPasteboard].pasteboardTypes];
|
||||
if ([UIPasteboard generalPasteboard].numberOfItems == 1 &&
|
||||
[[SignalAttachment validInputUTISet] intersectsSet:pasteboardUTISet]) {
|
||||
// We don't want to load/convert images more than once so we
|
||||
// only do a cursory validation pass at this time.
|
||||
return YES;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
|
||||
if (action == @selector(paste:)) {
|
||||
if ([self pasteBoardHasPossibleAttachment]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return [super canPerformAction:action withSender:sender];
|
||||
}
|
||||
|
||||
- (void)paste:(id)sender {
|
||||
if ([self pasteBoardHasPossibleAttachment]) {
|
||||
SignalAttachment *attachment = [SignalAttachment attachmentFromPasteboard];
|
||||
// Note: attachment might be nil or have an error at this point; that's fine.
|
||||
[self.textViewPasteDelegate didPasteAttachment:attachment];
|
||||
return;
|
||||
}
|
||||
|
||||
[super paste:sender];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessagesToolbarContentView
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (UINib *)nib
|
||||
{
|
||||
return [UINib nibWithNibName:NSStringFromClass([OWSMessagesToolbarContentView class])
|
||||
bundle:[NSBundle bundleForClass:[OWSMessagesToolbarContentView class]]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessagesInputToolbar
|
||||
|
||||
- (JSQMessagesToolbarContentView *)loadToolbarContentView {
|
||||
NSArray *views = [[OWSMessagesToolbarContentView nib] instantiateWithOwner:nil
|
||||
options:nil];
|
||||
OWSAssert(views.count == 1);
|
||||
OWSMessagesToolbarContentView *view = views[0];
|
||||
OWSAssert([view isKindOfClass:[OWSMessagesToolbarContentView class]]);
|
||||
return view;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface MessagesViewController () <JSQMessagesComposerTextViewPasteDelegate, OWSTextViewPasteDelegate> {
|
||||
UIImage *tappedImage;
|
||||
BOOL isGroupConversation;
|
||||
|
||||
|
||||
UIView *_unreadContainer;
|
||||
UIImageView *_unreadBackground;
|
||||
UILabel *_unreadLabel;
|
||||
@ -160,7 +245,18 @@ typedef enum : NSUInteger {
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil {
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@ -665,6 +761,11 @@ typedef enum : NSUInteger {
|
||||
// prevent draft from obscuring message history in case user wants to scroll back to refer to something
|
||||
// while composing a long message.
|
||||
self.inputToolbar.maximumHeight = 300;
|
||||
|
||||
OWSAssert(self.inputToolbar.contentView);
|
||||
OWSAssert(self.inputToolbar.contentView.textView);
|
||||
self.inputToolbar.contentView.textView.pasteDelegate = self;
|
||||
((OWSMessagesComposerTextView *) self.inputToolbar.contentView.textView).textViewPasteDelegate = self;
|
||||
}
|
||||
|
||||
- (nullable UILabel *)findNavbarTitleLabel
|
||||
@ -1211,7 +1312,11 @@ typedef enum : NSUInteger {
|
||||
DDLogDebug(@"%@ Ignoring request to show conversation settings, since user left group", self.tag);
|
||||
return;
|
||||
}
|
||||
[self performSegueWithIdentifier:OWSMessagesViewControllerSeguePushConversationSettings sender:self];
|
||||
|
||||
OWSConversationSettingsTableViewController *settingsVC = [[UIStoryboard storyboardWithName:AppDelegateStoryboardMain bundle:NULL]
|
||||
instantiateViewControllerWithIdentifier:@"OWSConversationSettingsTableViewController"];
|
||||
[settingsVC configureWithThread:self.thread];
|
||||
[self.navigationController pushViewController:settingsVC animated:YES];
|
||||
}
|
||||
|
||||
- (void)didTapTitle
|
||||
@ -1726,10 +1831,6 @@ typedef enum : NSUInteger {
|
||||
|
||||
NSString *contactName = [self.contactsManager displayNameForPhoneIdentifier:fingerprint.theirStableId];
|
||||
[vc configureWithThread:self.thread fingerprint:fingerprint contactName:contactName];
|
||||
} else if ([segue.destinationViewController isKindOfClass:[OWSConversationSettingsTableViewController class]]) {
|
||||
OWSConversationSettingsTableViewController *controller
|
||||
= (OWSConversationSettingsTableViewController *)segue.destinationViewController;
|
||||
[controller configureWithThread:self.thread];
|
||||
} else {
|
||||
DDLogDebug(@"%@ Received segue: %@", self.tag, segue.identifier);
|
||||
}
|
||||
@ -1809,7 +1910,18 @@ typedef enum : NSUInteger {
|
||||
|
||||
UIImage *imageFromCamera = [info[UIImagePickerControllerOriginalImage] normalizedImage];
|
||||
if (imageFromCamera) {
|
||||
[self sendMessageAttachment:[self qualityAdjustedAttachmentForImage:imageFromCamera] ofType:@"image/jpeg"];
|
||||
SignalAttachment *attachment = [SignalAttachment imageAttachmentWithImage:imageFromCamera
|
||||
dataUTI:(NSString *) kUTTypeJPEG];
|
||||
if (!attachment ||
|
||||
[attachment hasError]) {
|
||||
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
||||
self.tag,
|
||||
__PRETTY_FUNCTION__,
|
||||
attachment ? [attachment errorMessage] : @"Missing data");
|
||||
failedToPickAttachment(nil);
|
||||
} else {
|
||||
[self sendMessageAttachment:attachment];
|
||||
}
|
||||
} else {
|
||||
failedToPickAttachment(nil);
|
||||
}
|
||||
@ -1827,46 +1939,47 @@ typedef enum : NSUInteger {
|
||||
options.networkAccessAllowed = YES; // iCloud OK
|
||||
options.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; // Don't need quick/dirty version
|
||||
[[PHImageManager defaultManager]
|
||||
requestImageDataForAsset:asset
|
||||
options:options
|
||||
resultHandler:^(NSData *_Nullable imageData,
|
||||
NSString *_Nullable dataUTI,
|
||||
UIImageOrientation orientation,
|
||||
NSDictionary *_Nullable assetInfo) {
|
||||
|
||||
NSError *assetFetchingError = assetInfo[PHImageErrorKey];
|
||||
if (assetFetchingError || !imageData) {
|
||||
return failedToPickAttachment(assetFetchingError);
|
||||
}
|
||||
DDLogVerbose(
|
||||
@"Size in bytes: %lu; detected filetype: %@", (unsigned long)imageData.length, dataUTI);
|
||||
|
||||
if ([dataUTI isEqualToString:(__bridge NSString *)kUTTypeGIF]
|
||||
&& imageData.length <= 5 * 1024 * 1024) {
|
||||
DDLogVerbose(@"Sending raw image/gif to retain any animation");
|
||||
/**
|
||||
* Media Size constraints lifted from Signal-Android
|
||||
* (org/thoughtcrime/securesms/mms/PushMediaConstraints.java)
|
||||
*
|
||||
* GifMaxSize return 5 * MB;
|
||||
* For reference, other media size limits we're not explicitly enforcing:
|
||||
* ImageMaxSize return 420 * KB;
|
||||
* VideoMaxSize return 100 * MB;
|
||||
* getAudioMaxSize 100 * MB;
|
||||
*/
|
||||
[self sendMessageAttachment:imageData ofType:@"image/gif"];
|
||||
} else {
|
||||
DDLogVerbose(@"Compressing attachment as image/jpeg");
|
||||
UIImage *pickedImage = [[UIImage alloc] initWithData:imageData];
|
||||
[self sendMessageAttachment:[self qualityAdjustedAttachmentForImage:pickedImage]
|
||||
ofType:@"image/jpeg"];
|
||||
}
|
||||
}];
|
||||
requestImageDataForAsset:asset
|
||||
options:options
|
||||
resultHandler:^(NSData *_Nullable imageData,
|
||||
NSString *_Nullable dataUTI,
|
||||
UIImageOrientation orientation,
|
||||
NSDictionary *_Nullable assetInfo) {
|
||||
|
||||
NSError *assetFetchingError = assetInfo[PHImageErrorKey];
|
||||
if (assetFetchingError || !imageData) {
|
||||
return failedToPickAttachment(assetFetchingError);
|
||||
}
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
SignalAttachment *attachment = [SignalAttachment imageAttachmentWithData:imageData
|
||||
dataUTI:dataUTI];
|
||||
if (!attachment ||
|
||||
[attachment hasError]) {
|
||||
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
||||
self.tag,
|
||||
__PRETTY_FUNCTION__,
|
||||
attachment ? [attachment errorMessage] : @"Missing data");
|
||||
failedToPickAttachment(nil);
|
||||
} else {
|
||||
[self dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
[self sendMessageAttachment:attachment];
|
||||
}];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sendMessageAttachment:(NSData *)attachmentData ofType:(NSString *)attachmentType
|
||||
- (void)sendMessageAttachment:(SignalAttachment *)attachment
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
// TODO: Should we assume non-nil or should we check for non-nil?
|
||||
OWSAssert(attachment != nil);
|
||||
OWSAssert(![attachment hasError]);
|
||||
OWSAssert([attachment mimeType].length > 0);
|
||||
|
||||
TSOutgoingMessage *message;
|
||||
OWSDisappearingMessagesConfiguration *configuration =
|
||||
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:self.thread.uniqueId];
|
||||
@ -1882,25 +1995,20 @@ typedef enum : NSUInteger {
|
||||
messageBody:nil
|
||||
attachmentIds:[NSMutableArray new]];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
DDLogVerbose(@"Sending attachment. Size in bytes: %lu, contentType: %@",
|
||||
(unsigned long)attachmentData.length,
|
||||
attachmentType);
|
||||
[self.messageSender sendAttachmentData:attachmentData
|
||||
contentType:attachmentType
|
||||
inMessage:message
|
||||
success:^{
|
||||
DDLogDebug(@"%@ Successfully sent message attachment.", self.tag);
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
DDLogError(
|
||||
@"%@ Failed to send message attachment with error: %@", self.tag, error);
|
||||
}];
|
||||
}];
|
||||
});
|
||||
|
||||
DDLogVerbose(@"Sending attachment. Size in bytes: %lu, contentType: %@",
|
||||
(unsigned long)attachment.data.length,
|
||||
[attachment mimeType]);
|
||||
[self.messageSender sendAttachmentData:attachment.data
|
||||
contentType:[attachment mimeType]
|
||||
inMessage:message
|
||||
success:^{
|
||||
DDLogDebug(@"%@ Successfully sent message attachment.", self.tag);
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
DDLogError(
|
||||
@"%@ Failed to send message attachment with error: %@", self.tag, error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSURL *)videoTempFolder {
|
||||
@ -1930,75 +2038,28 @@ typedef enum : NSUInteger {
|
||||
|
||||
exportSession.outputURL = compressedVideoUrl;
|
||||
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
||||
NSError *error;
|
||||
[self sendMessageAttachment:[NSData dataWithContentsOfURL:compressedVideoUrl] ofType:@"video/mp4"];
|
||||
[[NSFileManager defaultManager] removeItemAtURL:compressedVideoUrl error:&error];
|
||||
if (error) {
|
||||
DDLogWarn(@"Failed to remove cached video file: %@", error.debugDescription);
|
||||
}
|
||||
NSData *videoData = [NSData dataWithContentsOfURL:compressedVideoUrl];
|
||||
SignalAttachment *attachment = [SignalAttachment videoAttachmentWithData:videoData
|
||||
dataUTI:(NSString *) kUTTypeMPEG4];
|
||||
if (!attachment ||
|
||||
[attachment hasError]) {
|
||||
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
||||
self.tag,
|
||||
__PRETTY_FUNCTION__,
|
||||
attachment ? [attachment errorMessage] : @"Missing data");
|
||||
// TODO: How should we handle errors here?
|
||||
} else {
|
||||
[self sendMessageAttachment:attachment];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
[[NSFileManager defaultManager] removeItemAtURL:compressedVideoUrl error:&error];
|
||||
if (error) {
|
||||
DDLogWarn(@"Failed to remove cached video file: %@", error.debugDescription);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSData *)qualityAdjustedAttachmentForImage:(UIImage *)image {
|
||||
return UIImageJPEGRepresentation([self adjustedImageSizedForSending:image], [self compressionRate]);
|
||||
}
|
||||
|
||||
- (UIImage *)adjustedImageSizedForSending:(UIImage *)image {
|
||||
CGFloat correctedWidth;
|
||||
switch ([Environment.preferences imageUploadQuality]) {
|
||||
case TSImageQualityUncropped:
|
||||
return image;
|
||||
|
||||
case TSImageQualityHigh:
|
||||
correctedWidth = 2048;
|
||||
break;
|
||||
case TSImageQualityMedium:
|
||||
correctedWidth = 1024;
|
||||
break;
|
||||
case TSImageQualityLow:
|
||||
correctedWidth = 512;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return [self imageScaled:image toMaxSize:correctedWidth];
|
||||
}
|
||||
|
||||
- (UIImage *)imageScaled:(UIImage *)image toMaxSize:(CGFloat)size {
|
||||
CGFloat scaleFactor;
|
||||
CGFloat aspectRatio = image.size.height / image.size.width;
|
||||
|
||||
if (aspectRatio > 1) {
|
||||
scaleFactor = size / image.size.width;
|
||||
} else {
|
||||
scaleFactor = size / image.size.height;
|
||||
}
|
||||
|
||||
CGSize newSize = CGSizeMake(image.size.width * scaleFactor, image.size.height * scaleFactor);
|
||||
|
||||
UIGraphicsBeginImageContext(newSize);
|
||||
[image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
|
||||
UIImage *updatedImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return updatedImage;
|
||||
}
|
||||
|
||||
- (CGFloat)compressionRate {
|
||||
switch ([Environment.preferences imageUploadQuality]) {
|
||||
case TSImageQualityUncropped:
|
||||
return 1;
|
||||
case TSImageQualityHigh:
|
||||
return 0.9f;
|
||||
case TSImageQualityMedium:
|
||||
return 0.5f;
|
||||
case TSImageQualityLow:
|
||||
return 0.3f;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Storage access
|
||||
|
||||
@ -2192,7 +2253,19 @@ typedef enum : NSUInteger {
|
||||
|
||||
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag {
|
||||
if (flag) {
|
||||
[self sendMessageAttachment:[NSData dataWithContentsOfURL:recorder.url] ofType:@"audio/m4a"];
|
||||
NSData *audioData = [NSData dataWithContentsOfURL:recorder.url];
|
||||
SignalAttachment *attachment = [SignalAttachment audioAttachmentWithData:audioData
|
||||
dataUTI:(NSString *) kUTTypeMPEG4Audio];
|
||||
if (!attachment ||
|
||||
[attachment hasError]) {
|
||||
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
||||
self.tag,
|
||||
__PRETTY_FUNCTION__,
|
||||
attachment ? [attachment errorMessage] : @"Missing data");
|
||||
// TODO: How should we handle errors here?
|
||||
} else {
|
||||
[self sendMessageAttachment:attachment];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2408,7 +2481,55 @@ typedef enum : NSUInteger {
|
||||
[self showConversationSettings];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - JSQMessagesComposerTextViewPasteDelegate
|
||||
|
||||
- (BOOL)composerTextView:(JSQMessagesComposerTextView *)textView
|
||||
shouldPasteWithSender:(id)sender {
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - OWSTextViewPasteDelegate
|
||||
|
||||
- (void)didPasteAttachment:(SignalAttachment * _Nullable)attachment {
|
||||
DDLogError(@"%@ %s",
|
||||
self.tag,
|
||||
__PRETTY_FUNCTION__);
|
||||
|
||||
if (attachment == nil ||
|
||||
[attachment hasError]) {
|
||||
DDLogWarn(@"%@ %s Invalid attachment: %@.",
|
||||
self.tag,
|
||||
__PRETTY_FUNCTION__,
|
||||
attachment ? [attachment errorMessage] : @"Missing data");
|
||||
// TODO: Add UI.
|
||||
} else {
|
||||
__weak MessagesViewController *weakSelf = self;
|
||||
UIViewController *viewController = [[AttachmentApprovalViewController alloc] initWithAttachment:attachment
|
||||
successCompletion:^{
|
||||
[weakSelf sendMessageAttachment:attachment];
|
||||
}];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
|
||||
[self.navigationController presentViewController:navigationController
|
||||
animated:YES
|
||||
completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (UINib *)nib
|
||||
{
|
||||
return [UINib nibWithNibName:NSStringFromClass([MessagesViewController class])
|
||||
bundle:[NSBundle bundleForClass:[MessagesViewController class]]];
|
||||
}
|
||||
|
||||
+ (instancetype)messagesViewController
|
||||
{
|
||||
return [[[self class] alloc] initWithNibName:NSStringFromClass([MessagesViewController class])
|
||||
bundle:[NSBundle bundleForClass:[MessagesViewController class]]];
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
|
||||
58
Signal/src/view controllers/MessagesViewController.xib
Normal file
58
Signal/src/view controllers/MessagesViewController.xib
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="15G1217" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MessagesViewController">
|
||||
<connections>
|
||||
<outlet property="collectionView" destination="l9u-2b-4LK" id="bLP-6g-CkO"/>
|
||||
<outlet property="inputToolbar" destination="BoD-Az-3DM" id="w74-g9-1qA"/>
|
||||
<outlet property="toolbarBottomLayoutGuide" destination="rHs-6q-NX4" id="d6h-iu-VMX"/>
|
||||
<outlet property="toolbarHeightConstraint" destination="HIk-02-qcW" id="jE8-xC-1eD"/>
|
||||
<outlet property="view" destination="mUa-cS-ru4" id="nki-T1-RTI"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="mUa-cS-ru4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<collectionView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.0" maximumZoomScale="0.0" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="l9u-2b-4LK" customClass="JSQMessagesCollectionView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<collectionViewLayout key="collectionViewLayout" id="dZl-7C-LHR" customClass="JSQMessagesCollectionViewFlowLayout"/>
|
||||
<cells/>
|
||||
</collectionView>
|
||||
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BoD-Az-3DM" customClass="OWSMessagesInputToolbar">
|
||||
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="HIk-02-qcW"/>
|
||||
</constraints>
|
||||
<items/>
|
||||
</toolbar>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="BoD-Az-3DM" secondAttribute="trailing" id="7xc-Ha-asg"/>
|
||||
<constraint firstItem="l9u-2b-4LK" firstAttribute="leading" secondItem="mUa-cS-ru4" secondAttribute="leading" id="MmF-oh-Y75"/>
|
||||
<constraint firstAttribute="trailing" secondItem="l9u-2b-4LK" secondAttribute="trailing" id="O9u-TA-A0e"/>
|
||||
<constraint firstAttribute="bottom" secondItem="l9u-2b-4LK" secondAttribute="bottom" id="Re7-WW-UmS"/>
|
||||
<constraint firstItem="l9u-2b-4LK" firstAttribute="top" secondItem="mUa-cS-ru4" secondAttribute="top" id="dCQ-DM-Wdj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BoD-Az-3DM" secondAttribute="bottom" id="rHs-6q-NX4"/>
|
||||
<constraint firstItem="BoD-Az-3DM" firstAttribute="leading" secondItem="mUa-cS-ru4" secondAttribute="leading" id="ts7-8f-0lH"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
</view>
|
||||
</objects>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
||||
@ -93,12 +93,7 @@ static NSString *const OWSConversationSettingsTableViewControllerSegueShowGroupM
|
||||
return self;
|
||||
}
|
||||
|
||||
_storageManager = [TSStorageManager sharedManager];
|
||||
_contactsManager = [Environment getCurrent].contactsManager;
|
||||
_messageSender = [[OWSMessageSender alloc] initWithNetworkManager:[Environment getCurrent].networkManager
|
||||
storageManager:_storageManager
|
||||
contactsManager:_contactsManager
|
||||
contactsUpdater:[Environment getCurrent].contactsUpdater];
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
@ -110,14 +105,30 @@ static NSString *const OWSConversationSettingsTableViewControllerSegueShowGroupM
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil {
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
[self commonInit];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
_storageManager = [TSStorageManager sharedManager];
|
||||
_contactsManager = [Environment getCurrent].contactsManager;
|
||||
_messageSender = [[OWSMessageSender alloc] initWithNetworkManager:[Environment getCurrent].networkManager
|
||||
storageManager:_storageManager
|
||||
contactsManager:_contactsManager
|
||||
contactsUpdater:[Environment getCurrent].contactsUpdater];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)configureWithThread:(TSThread *)thread
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="15G1217" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="1" customClass="OWSMessagesToolbarContentView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LEq-G7-jGt" userLabel="Left button container">
|
||||
<rect key="frame" x="8" y="6" width="34" height="32"/>
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="0sE-GV-joM"/>
|
||||
<constraint firstAttribute="width" constant="34" id="eMy-Af-wwH"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Myo-1S-Vg1" userLabel="Right button container">
|
||||
<rect key="frame" x="262" y="6" width="50" height="32"/>
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="NaR-re-dJ4"/>
|
||||
<constraint firstAttribute="width" constant="50" id="yde-S9-dHe"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dm4-NT-mvr" customClass="OWSMessagesComposerTextView">
|
||||
<rect key="frame" x="50" y="7" width="204" height="30"/>
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Myo-1S-Vg1" firstAttribute="leading" secondItem="dm4-NT-mvr" secondAttribute="trailing" constant="8" id="7Ld-5r-Hp3"/>
|
||||
<constraint firstItem="dm4-NT-mvr" firstAttribute="top" secondItem="1" secondAttribute="top" constant="7" id="9Tz-Wq-xIf"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dm4-NT-mvr" secondAttribute="bottom" constant="7" id="CCb-V7-yek"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Myo-1S-Vg1" secondAttribute="bottom" constant="6" id="EaS-Oq-Qp5"/>
|
||||
<constraint firstItem="LEq-G7-jGt" firstAttribute="leading" secondItem="1" secondAttribute="leading" constant="8" id="LAU-fo-GJJ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Myo-1S-Vg1" secondAttribute="trailing" constant="8" id="ds6-61-GNv"/>
|
||||
<constraint firstAttribute="bottom" secondItem="LEq-G7-jGt" secondAttribute="bottom" constant="6" id="oG2-YD-ZZI"/>
|
||||
<constraint firstItem="dm4-NT-mvr" firstAttribute="leading" secondItem="LEq-G7-jGt" secondAttribute="trailing" constant="8" id="owo-gB-gyR"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="leftBarButtonContainerView" destination="LEq-G7-jGt" id="F0V-4N-1Mo"/>
|
||||
<outlet property="leftBarButtonContainerViewWidthConstraint" destination="eMy-Af-wwH" id="FI9-F2-2bN"/>
|
||||
<outlet property="leftHorizontalSpacingConstraint" destination="LAU-fo-GJJ" id="X2c-BI-0Q4"/>
|
||||
<outlet property="rightBarButtonContainerView" destination="Myo-1S-Vg1" id="0SR-cw-EkD"/>
|
||||
<outlet property="rightBarButtonContainerViewWidthConstraint" destination="yde-S9-dHe" id="WGu-df-M3L"/>
|
||||
<outlet property="rightHorizontalSpacingConstraint" destination="ds6-61-GNv" id="ZQh-8M-QFs"/>
|
||||
<outlet property="textView" destination="dm4-NT-mvr" id="PFw-HO-oT8"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="268" y="548"/>
|
||||
</view>
|
||||
</objects>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
||||
530
Signal/src/view controllers/SignalAttachment.swift
Normal file
530
Signal/src/view controllers/SignalAttachment.swift
Normal file
@ -0,0 +1,530 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
|
||||
enum SignalAttachmentError: String {
|
||||
case missingData
|
||||
case fileSizeTooLarge
|
||||
case invalidData
|
||||
case couldNotParseImage
|
||||
case couldNotConvertToJpeg
|
||||
case invalidFileFormat
|
||||
}
|
||||
|
||||
enum TSImageQuality: Int {
|
||||
case uncropped
|
||||
case high
|
||||
case medium
|
||||
case low
|
||||
}
|
||||
|
||||
// Represents a possible attachment to upload.
|
||||
// The attachment may be invalid.
|
||||
//
|
||||
// Signal attachments are subject to validation and
|
||||
// in some cases, file format conversion.
|
||||
//
|
||||
// This class gathers that logic. It offers factory methods
|
||||
// for attachments that do the necessary work.
|
||||
//
|
||||
// The return value for the factory methods will be nil if the input is nil.
|
||||
//
|
||||
// [SignalAttachment hasError] will be true for non-valid attachments.
|
||||
//
|
||||
// TODO: Perhaps do conversion off the main thread?
|
||||
// TODO: Show error on error.
|
||||
// TODO: Show progress on upload.
|
||||
class SignalAttachment: NSObject {
|
||||
|
||||
static let TAG = "[SignalAttachment]"
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let data: Data
|
||||
|
||||
// Attachment types are identified using UTIs.
|
||||
//
|
||||
// See: https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
|
||||
let dataUTI: String
|
||||
|
||||
var error: SignalAttachmentError? {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
assert(oldValue == nil)
|
||||
Logger.verbose("\(SignalAttachment.TAG) Attachment has error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
/**
|
||||
* Media Size constraints from Signal-Android
|
||||
*
|
||||
* https://github.com/WhisperSystems/Signal-Android/blob/master/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
|
||||
*/
|
||||
static let kMaxFileSizeAnimatedImage = 6 * 1024 * 1024
|
||||
static let kMaxFileSizeImage = 6 * 1024 * 1024
|
||||
static let kMaxFileSizeVideo = 100 * 1024 * 1024
|
||||
static let kMaxFileSizeAudio = 100 * 1024 * 1024
|
||||
static let kMaxFileSizeGeneric = 100 * 1024 * 1024
|
||||
|
||||
// MARK: Constructor
|
||||
|
||||
// This method should not be called directly; use the factory
|
||||
// methods instead.
|
||||
internal required init(data: Data, dataUTI: String) {
|
||||
self.data = data
|
||||
self.dataUTI = dataUTI
|
||||
super.init()
|
||||
}
|
||||
|
||||
var hasError: Bool {
|
||||
return error != nil
|
||||
}
|
||||
|
||||
var errorMessage: String? {
|
||||
guard let error = error else {
|
||||
// This method should only be called if there is an error.
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
return "\(error)"
|
||||
}
|
||||
|
||||
// Returns the MIME type for this attachment or nil if no MIME type
|
||||
// can be identified.
|
||||
var mimeType: String? {
|
||||
let mimeType = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassMIMEType)
|
||||
guard mimeType != nil else {
|
||||
return nil
|
||||
}
|
||||
return mimeType?.takeRetainedValue() as? String
|
||||
}
|
||||
|
||||
// Returns the file extension for this attachment or nil if no file extension
|
||||
// can be identified.
|
||||
var fileExtension: String? {
|
||||
guard let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString,
|
||||
kUTTagClassFilenameExtension) else {
|
||||
return nil
|
||||
}
|
||||
return fileExtension.takeRetainedValue() as String
|
||||
}
|
||||
|
||||
private static let allowArbitraryAttachments = false
|
||||
|
||||
// Returns the set of UTIs that correspond to valid _input_ image formats
|
||||
// for Signal attachments.
|
||||
//
|
||||
// Image attachments may be converted to another image format before
|
||||
// being uploaded.
|
||||
private class var inputImageUTISet: Set<String> {
|
||||
return MIMETypeUtil.supportedImageUTITypes()
|
||||
}
|
||||
|
||||
// Returns the set of UTIs that correspond to valid _output_ image formats
|
||||
// for Signal attachments.
|
||||
private class var outputImageUTISet: Set<String> {
|
||||
if allowArbitraryAttachments {
|
||||
return MIMETypeUtil.supportedImageUTITypes()
|
||||
} else {
|
||||
// Until Android client can handle arbitrary attachments,
|
||||
// restrict output.
|
||||
return [
|
||||
kUTTypeJPEG as String,
|
||||
kUTTypeGIF as String,
|
||||
kUTTypePNG as String
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of UTIs that correspond to valid animated image formats
|
||||
// for Signal attachments.
|
||||
private class var animatedImageUTISet: Set<String> {
|
||||
return MIMETypeUtil.supportedAnimatedImageUTITypes()
|
||||
}
|
||||
|
||||
// Returns the set of UTIs that correspond to valid video formats
|
||||
// for Signal attachments.
|
||||
private class var videoUTISet: Set<String> {
|
||||
if allowArbitraryAttachments {
|
||||
return MIMETypeUtil.supportedVideoUTITypes()
|
||||
} else {
|
||||
return [
|
||||
kUTTypeMPEG4 as String
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of UTIs that correspond to valid audio formats
|
||||
// for Signal attachments.
|
||||
private class var audioUTISet: Set<String> {
|
||||
if allowArbitraryAttachments {
|
||||
return MIMETypeUtil.supportedAudioUTITypes()
|
||||
} else {
|
||||
return [
|
||||
kUTTypeMP3 as String,
|
||||
kUTTypeMPEG4Audio as String
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of UTIs that correspond to valid input formats
|
||||
// for Signal attachments.
|
||||
public class var validInputUTISet: Set<String> {
|
||||
return inputImageUTISet.union(videoUTISet.union(audioUTISet))
|
||||
}
|
||||
|
||||
public var isImage: Bool {
|
||||
return SignalAttachment.outputImageUTISet.contains(dataUTI)
|
||||
}
|
||||
|
||||
public var isVideo: Bool {
|
||||
return SignalAttachment.videoUTISet.contains(dataUTI)
|
||||
}
|
||||
|
||||
public var isAudio: Bool {
|
||||
return SignalAttachment.audioUTISet.contains(dataUTI)
|
||||
}
|
||||
|
||||
// Returns an attachment from the pasteboard, or nil if no attachment
|
||||
// can be found.
|
||||
//
|
||||
// NOTE: The attachment returned by this method may not be valid.
|
||||
// Check the attachment's error property.
|
||||
public class func attachmentFromPasteboard() -> SignalAttachment? {
|
||||
guard UIPasteboard.general.numberOfItems >= 1 else {
|
||||
return nil
|
||||
}
|
||||
// If pasteboard contains multiple items, use only the first.
|
||||
let itemSet = IndexSet(integer:0)
|
||||
guard let pasteboardUTITypes = UIPasteboard.general.types(forItemSet:itemSet) else {
|
||||
return nil
|
||||
}
|
||||
let pasteboardUTISet = Set<String>(pasteboardUTITypes[0])
|
||||
for dataUTI in inputImageUTISet {
|
||||
if pasteboardUTISet.contains(dataUTI) {
|
||||
guard let data = dataForFirstPasteboardItem(dataUTI:dataUTI) else {
|
||||
Logger.verbose("\(TAG) Missing expected pasteboard data for UTI: \(dataUTI)")
|
||||
return nil
|
||||
}
|
||||
return imageAttachment(data : data, dataUTI : dataUTI)
|
||||
}
|
||||
}
|
||||
for dataUTI in videoUTISet {
|
||||
if pasteboardUTISet.contains(dataUTI) {
|
||||
guard let data = dataForFirstPasteboardItem(dataUTI:dataUTI) else {
|
||||
Logger.verbose("\(TAG) Missing expected pasteboard data for UTI: \(dataUTI)")
|
||||
return nil
|
||||
}
|
||||
return videoAttachment(data : data, dataUTI : dataUTI)
|
||||
}
|
||||
}
|
||||
for dataUTI in audioUTISet {
|
||||
if pasteboardUTISet.contains(dataUTI) {
|
||||
guard let data = dataForFirstPasteboardItem(dataUTI:dataUTI) else {
|
||||
Logger.verbose("\(TAG) Missing expected pasteboard data for UTI: \(dataUTI)")
|
||||
return nil
|
||||
}
|
||||
return audioAttachment(data : data, dataUTI : dataUTI)
|
||||
}
|
||||
}
|
||||
// TODO: We could handle generic attachments at this point.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This method should only be called for dataUTIs that
|
||||
// are appropriate for the first pasteboard item.
|
||||
private class func dataForFirstPasteboardItem(dataUTI: String) -> Data? {
|
||||
let itemSet = IndexSet(integer:0)
|
||||
guard let datas = UIPasteboard.general.data(forPasteboardType:dataUTI, inItemSet:itemSet) else {
|
||||
Logger.verbose("\(TAG) Missing expected pasteboard data for UTI: \(dataUTI)")
|
||||
return nil
|
||||
}
|
||||
guard datas.count > 0 else {
|
||||
Logger.verbose("\(TAG) Missing expected pasteboard data for UTI: \(dataUTI)")
|
||||
return nil
|
||||
}
|
||||
guard let data = datas[0] as? Data else {
|
||||
Logger.verbose("\(TAG) Missing expected pasteboard data for UTI: \(dataUTI)")
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// MARK: Image Attachments
|
||||
|
||||
// Factory method for an image attachment.
|
||||
//
|
||||
// NOTE: The attachment returned by this method may not be valid.
|
||||
// Check the attachment's error property.
|
||||
public class func imageAttachment(data imageData: Data?, dataUTI: String) -> SignalAttachment {
|
||||
assert(dataUTI.characters.count > 0)
|
||||
|
||||
assert(imageData != nil)
|
||||
guard let imageData = imageData else {
|
||||
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
||||
attachment.error = .missingData
|
||||
return attachment
|
||||
}
|
||||
|
||||
let attachment = SignalAttachment(data : imageData, dataUTI: dataUTI)
|
||||
|
||||
guard inputImageUTISet.contains(dataUTI) else {
|
||||
attachment.error = .invalidFileFormat
|
||||
return attachment
|
||||
}
|
||||
|
||||
guard imageData.count > 0 else {
|
||||
assert(imageData.count > 0)
|
||||
attachment.error = .invalidData
|
||||
return attachment
|
||||
}
|
||||
|
||||
if animatedImageUTISet.contains(dataUTI) {
|
||||
guard imageData.count <= kMaxFileSizeAnimatedImage else {
|
||||
attachment.error = .fileSizeTooLarge
|
||||
return attachment
|
||||
}
|
||||
// Never re-encode animated images (i.e. GIFs) as JPEGs.
|
||||
Logger.verbose("\(TAG) Sending raw \(attachment.mimeType) to retain any animation")
|
||||
return attachment
|
||||
} else {
|
||||
guard let image = UIImage(data:imageData) else {
|
||||
attachment.error = .couldNotParseImage
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private class func defaultImageUploadQuality() -> TSImageQuality {
|
||||
// Currently default to a middling image quality and size.
|
||||
//
|
||||
// TODO: We're likely to change this behavior soon.
|
||||
return .medium
|
||||
}
|
||||
|
||||
// If the proposed attachment already conforms to the
|
||||
// file size and content size limits, don't recompress it.
|
||||
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
|
||||
}
|
||||
guard SignalAttachment.outputImageUTISet.contains(dataUTI) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let maxSize = maxSizeForImage(image: image,
|
||||
imageUploadQuality:defaultImageUploadQuality())
|
||||
if image.size.width <= maxSize &&
|
||||
image.size.height <= maxSize &&
|
||||
imageData.count <= kMaxFileSizeImage {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Factory method for an image attachment.
|
||||
//
|
||||
// NOTE: The attachment returned by this method may nil or not be valid.
|
||||
// Check the attachment's error property.
|
||||
public class func imageAttachment(image: UIImage?, dataUTI: String) -> SignalAttachment {
|
||||
assert(dataUTI.characters.count > 0)
|
||||
|
||||
guard let image = image else {
|
||||
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
||||
attachment.error = .missingData
|
||||
return attachment
|
||||
}
|
||||
|
||||
// Make a placeholder attachment on which to hang errors if necessary.
|
||||
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
||||
attachment.image = image
|
||||
|
||||
Logger.verbose("\(TAG) Writing \(attachment.mimeType) as image/jpeg")
|
||||
return compressImageAsJPEG(image : image, attachment : attachment)
|
||||
}
|
||||
|
||||
private class func compressImageAsJPEG(image: UIImage, attachment: SignalAttachment) -> SignalAttachment {
|
||||
assert(attachment.error == nil)
|
||||
|
||||
var imageUploadQuality = defaultImageUploadQuality()
|
||||
|
||||
while true {
|
||||
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
|
||||
var dstImage: UIImage! = image
|
||||
if image.size.width > maxSize ||
|
||||
image.size.height > maxSize {
|
||||
dstImage = imageScaled(image, toMaxSize: maxSize)
|
||||
}
|
||||
guard let jpgImageData = UIImageJPEGRepresentation(dstImage,
|
||||
jpegCompressionQuality(imageUploadQuality:imageUploadQuality)) else {
|
||||
attachment.error = .couldNotConvertToJpeg
|
||||
return attachment
|
||||
}
|
||||
|
||||
if jpgImageData.count <= kMaxFileSizeImage {
|
||||
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,
|
||||
// continue to try again by progressively reducing the
|
||||
// image upload quality.
|
||||
switch imageUploadQuality {
|
||||
case .uncropped:
|
||||
imageUploadQuality = .high
|
||||
case .high:
|
||||
imageUploadQuality = .medium
|
||||
case .medium:
|
||||
imageUploadQuality = .low
|
||||
case .low:
|
||||
attachment.error = .fileSizeTooLarge
|
||||
return attachment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func imageScaled(_ image: UIImage, toMaxSize size: CGFloat) -> UIImage {
|
||||
var scaleFactor: CGFloat
|
||||
let aspectRatio: CGFloat = image.size.height / image.size.width
|
||||
if aspectRatio > 1 {
|
||||
scaleFactor = size / image.size.width
|
||||
} else {
|
||||
scaleFactor = size / image.size.height
|
||||
}
|
||||
let newSize = CGSize(width: CGFloat(image.size.width * scaleFactor), height: CGFloat(image.size.height * scaleFactor))
|
||||
UIGraphicsBeginImageContext(newSize)
|
||||
image.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(newSize.width), height: CGFloat(newSize.height)))
|
||||
let updatedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return updatedImage!
|
||||
}
|
||||
|
||||
private class func maxSizeForImage(image: UIImage, imageUploadQuality: TSImageQuality) -> CGFloat {
|
||||
switch imageUploadQuality {
|
||||
case .uncropped:
|
||||
return max(image.size.width, image.size.height)
|
||||
case .high:
|
||||
return 2048
|
||||
case .medium:
|
||||
return 1024
|
||||
case .low:
|
||||
return 512
|
||||
}
|
||||
}
|
||||
|
||||
private class func jpegCompressionQuality(imageUploadQuality: TSImageQuality) -> CGFloat {
|
||||
switch imageUploadQuality {
|
||||
case .uncropped:
|
||||
return 1
|
||||
case .high:
|
||||
return 0.9
|
||||
case .medium:
|
||||
return 0.5
|
||||
case .low:
|
||||
return 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Video Attachments
|
||||
|
||||
// Factory method for video attachments.
|
||||
//
|
||||
// NOTE: The attachment returned by this method may not be valid.
|
||||
// Check the attachment's error property.
|
||||
public class func videoAttachment(data: Data?, dataUTI: String) -> SignalAttachment {
|
||||
return newAttachment(data : data,
|
||||
dataUTI : dataUTI,
|
||||
validUTISet : videoUTISet,
|
||||
maxFileSize : kMaxFileSizeVideo)
|
||||
}
|
||||
|
||||
// MARK: Audio Attachments
|
||||
|
||||
// Factory method for audio attachments.
|
||||
//
|
||||
// NOTE: The attachment returned by this method may not be valid.
|
||||
// Check the attachment's error property.
|
||||
public class func audioAttachment(data: Data?, dataUTI: String) -> SignalAttachment {
|
||||
return newAttachment(data : data,
|
||||
dataUTI : dataUTI,
|
||||
validUTISet : audioUTISet,
|
||||
maxFileSize : kMaxFileSizeAudio)
|
||||
}
|
||||
|
||||
// MARK: Generic Attachments
|
||||
|
||||
// Factory method for generic attachments.
|
||||
//
|
||||
// NOTE: The attachment returned by this method may not be valid.
|
||||
// Check the attachment's error property.
|
||||
public class func genericAttachment(data: Data?, dataUTI: String) -> SignalAttachment {
|
||||
return newAttachment(data : data,
|
||||
dataUTI : dataUTI,
|
||||
validUTISet : nil,
|
||||
maxFileSize : kMaxFileSizeGeneric)
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
private class func newAttachment(data: Data?,
|
||||
dataUTI: String,
|
||||
validUTISet: Set<String>?,
|
||||
maxFileSize: Int) -> SignalAttachment {
|
||||
assert(dataUTI.characters.count > 0)
|
||||
|
||||
assert(data != nil)
|
||||
guard let data = data else {
|
||||
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
|
||||
attachment.error = .missingData
|
||||
return attachment
|
||||
}
|
||||
|
||||
let attachment = SignalAttachment(data : data, dataUTI: dataUTI)
|
||||
|
||||
if let validUTISet = validUTISet {
|
||||
guard validUTISet.contains(dataUTI) else {
|
||||
attachment.error = .invalidFileFormat
|
||||
return attachment
|
||||
}
|
||||
}
|
||||
|
||||
guard data.count > 0 else {
|
||||
assert(data.count > 0)
|
||||
attachment.error = .invalidData
|
||||
return attachment
|
||||
}
|
||||
|
||||
guard data.count <= maxFileSize else {
|
||||
attachment.error = .fileSizeTooLarge
|
||||
return attachment
|
||||
}
|
||||
|
||||
// Attachment is valid
|
||||
return attachment
|
||||
}
|
||||
}
|
||||
@ -564,8 +564,8 @@ NSString *const SignalsViewControllerSegueShowIncomingCall = @"ShowIncomingCallS
|
||||
|
||||
- (void)presentThread:(TSThread *)thread keyboardOnViewAppearing:(BOOL)keyboardOnViewAppearing {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
MessagesViewController *mvc = [[UIStoryboard storyboardWithName:AppDelegateStoryboardMain bundle:NULL]
|
||||
instantiateViewControllerWithIdentifier:@"MessagesViewController"];
|
||||
MessagesViewController *mvc = [[MessagesViewController alloc] initWithNibName:@"MessagesViewController"
|
||||
bundle:nil];
|
||||
|
||||
if (self.presentedViewController) {
|
||||
[self.presentedViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
|
||||
@ -52,6 +52,21 @@
|
||||
/* No comment provided by engineer. */
|
||||
"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. */
|
||||
"ATTACHMENT_DOWNLOAD_FAILED" = "Attachment download failed, tap to retry.";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user