Merge branch 'charlesmchen/paste'

pull/1/head
Matthew Chen 8 years ago
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 */,

@ -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

@ -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"];
}
}];
}
}
- (void)sendMessageAttachment:(NSData *)attachmentData ofType:(NSString *)attachmentType
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:(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

@ -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>

@ -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…
Cancel
Save