diff --git a/Signal/Images.xcassets/file-icon-large.imageset/Contents.json b/Signal/Images.xcassets/file-icon-large.imageset/Contents.json
new file mode 100644
index 000000000..e01e985e3
--- /dev/null
+++ b/Signal/Images.xcassets/file-icon-large.imageset/Contents.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/Signal/Images.xcassets/file-icon-large.imageset/file-icon-large@1x.png b/Signal/Images.xcassets/file-icon-large.imageset/file-icon-large@1x.png
new file mode 100644
index 000000000..7adc6fe64
Binary files /dev/null and b/Signal/Images.xcassets/file-icon-large.imageset/file-icon-large@1x.png differ
diff --git a/Signal/src/Storyboard/Main.storyboard b/Signal/src/Storyboard/Main.storyboard
index 47cff25a3..3308a7042 100644
--- a/Signal/src/Storyboard/Main.storyboard
+++ b/Signal/src/Storyboard/Main.storyboard
@@ -1545,11 +1545,11 @@
-
+
-
+
-
+
@@ -1721,8 +1721,8 @@
-
-
+
+
diff --git a/Signal/src/UIColor+OWS.h b/Signal/src/UIColor+OWS.h
index f60e3672c..2c9d3ab6d 100644
--- a/Signal/src/UIColor+OWS.h
+++ b/Signal/src/UIColor+OWS.h
@@ -18,5 +18,7 @@
+ (UIColor *)ows_errorMessageBorderColor;
+ (UIColor *)ows_infoMessageBorderColor;
+ (UIColor *)backgroundColorForContact:(NSString *)contactIdentifier;
++ (UIColor *)colorWithRGBHex:(unsigned long)value;
++ (UIColor *)colorWithARGBHex:(unsigned long)value;
@end
diff --git a/Signal/src/UIColor+OWS.m b/Signal/src/UIColor+OWS.m
index ef4cebb07..3425c598e 100644
--- a/Signal/src/UIColor+OWS.m
+++ b/Signal/src/UIColor+OWS.m
@@ -107,4 +107,21 @@
return [colors objectAtIndex:(choose % [colors count])];
}
++ (UIColor *)colorWithRGBHex:(unsigned long)value
+{
+ CGFloat red = ((value >> 16) & 0xff) / 255.f;
+ CGFloat green = ((value >> 8) & 0xff) / 255.f;
+ CGFloat blue = ((value >> 0) & 0xff) / 255.f;
+ return [UIColor colorWithRed:red green:green blue:blue alpha:1.f];
+}
+
++ (UIColor *)colorWithARGBHex:(unsigned long)value
+{
+ CGFloat alpha = ((value >> 24) & 0xff) / 255.f;
+ CGFloat red = ((value >> 16) & 0xff) / 255.f;
+ CGFloat green = ((value >> 8) & 0xff) / 255.f;
+ CGFloat blue = ((value >> 0) & 0xff) / 255.f;
+ return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
+}
+
@end
diff --git a/Signal/src/view controllers/AttachmentApprovalViewController.swift b/Signal/src/view controllers/AttachmentApprovalViewController.swift
index 8c49ba8b4..36c6fd58b 100644
--- a/Signal/src/view controllers/AttachmentApprovalViewController.swift
+++ b/Signal/src/view controllers/AttachmentApprovalViewController.swift
@@ -3,989 +3,215 @@
//
import Foundation
-//import WebRTC
-//import PromiseKit
-//// TODO: Add category so that button handlers can be defined where button is created.
-//// TODO: Ensure buttons enabled & disabled as necessary.
-//@objc(OWSAttachmentApprovalViewController)
class AttachmentApprovalViewController: UIViewController {
let TAG = "[AttachmentApprovalViewController]"
- // Dependencies
+ // MARK: Properties
-// let callUIAdapter: CallUIAdapter
-// let contactsManager: OWSContactsManager
+ let attachment: SignalAttachment!
- // MARK: Properties
-
- let attachment : SignalAttachment!
-
-// var thread: TSContactThread!
-// var call: SignalCall!
-// var hasDismissed = false
-
- // MARK: Views
-
-// var hasConstraints = false
-// var blurView: UIVisualEffectView!
-// var dateFormatter: DateFormatter?
-//
-// // MARK: Contact Views
-//
-// var contactNameLabel: UILabel!
-// var contactAvatarView: AvatarImageView!
-// var callStatusLabel: UILabel!
-// var callDurationTimer: Timer?
-//
-// // MARK: Ongoing Call Controls
-//
-// var ongoingCallView: UIView!
-//
-// var hangUpButton: UIButton!
-// var speakerPhoneButton: UIButton!
-// var audioModeMuteButton: UIButton!
-// var audioModeVideoButton: UIButton!
-// var videoModeMuteButton: UIButton!
-// var videoModeVideoButton: UIButton!
-// // TODO: Later, we'll re-enable the text message button
-// // so users can send and read messages during a
-// // call.
-//// var textMessageButton: UIButton!
-//
-// // MARK: Incoming Call Controls
-//
-// var incomingCallView: UIView!
-//
-// var acceptIncomingButton: UIButton!
-// var declineIncomingButton: UIButton!
-//
-// // MARK: Video Views
-//
-// var remoteVideoView: RTCEAGLVideoView!
-// var localVideoView: RTCCameraPreviewView!
-// weak var localVideoTrack: RTCVideoTrack?
-// weak var remoteVideoTrack: RTCVideoTrack?
-// var remoteVideoSize: CGSize! = CGSize.zero
-// var remoteVideoConstraints: [NSLayoutConstraint] = []
-// var localVideoConstraints: [NSLayoutConstraint] = []
-//
-// var shouldRemoteVideoControlsBeHidden = false {
-// didSet {
-// updateCallUI(callState: call.state)
-// }
-// }
-//
-// // MARK: Settings Nag Views
-//
-// var isShowingSettingsNag = false {
-// didSet {
-// if oldValue != isShowingSettingsNag {
-// updateCallUI(callState: call.state)
-// }
-// }
-// }
-// var settingsNagView: UIView!
-// var settingsNagDescriptionLabel: UILabel!
+ var successCompletion : (() -> Void)?
// MARK: Initializers
+ @available(*, deprecated, message:"use attachment: constructor instead.")
required init?(coder aDecoder: NSCoder) {
self.attachment = SignalAttachment.genericAttachment(withData: nil,
dataUTI: kUTTypeContent as String)
-// contactsManager = Environment.getCurrent().contactsManager
-// callUIAdapter = Environment.getCurrent().callUIAdapter
super.init(coder: aDecoder)
- // TODO: How to deprecate constructor in Swift?
assert(false)
-// observeNotifications()
}
- required init(attachment : SignalAttachment!) {
-// let attachmentData : NSData
-// let attachmentType : String
-//
-// contactsManager = Environment.getCurrent().contactsManager
-// callUIAdapter = Environment.getCurrent().callUIAdapter
+ required init(attachment: SignalAttachment!, successCompletion : @escaping () -> Void) {
+ assert(!attachment.hasError())
self.attachment = attachment
+ self.successCompletion = successCompletion
super.init(nibName: nil, bundle: nil)
-// observeNotifications()
}
-// func observeNotifications() {
-// NotificationCenter.default.addObserver(self,
-// selector:#selector(didBecomeActive),
-// name:NSNotification.Name.UIApplicationDidBecomeActive,
-// object:nil)
-// }
-//
-// deinit {
-// NotificationCenter.default.removeObserver(self)
-// }
-//
-// func didBecomeActive() {
-// shouldRemoteVideoControlsBeHidden = false
-// }
-
// MARK: View Lifecycle
-// override func viewDidDisappear(_ animated: Bool) {
-// super.viewDidDisappear(animated)
-//
-// callDurationTimer?.invalidate()
-// callDurationTimer = nil
-// }
-//
-// override func viewWillAppear(_ animated: Bool) {
-// super.viewWillAppear(animated)
-//
-// updateCallUI(callState: call.state)
-// }
-//
-
-// override func loadView() {
-// self.view =
-// }
-
override func viewDidLoad() {
super.viewDidLoad()
-
+
view.backgroundColor = UIColor.black
-//
-// guard let thread = self.thread else {
-// Logger.error("\(TAG) tried to show call call without specifying thread.")
-// showCallFailed(error: OWSErrorMakeAssertionError())
-// return
-// }
- createViews()
+ 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.")
-// contactNameLabel.text = contactsManager.displayName(forPhoneIdentifier: thread.contactIdentifier())
-// contactAvatarView.image = OWSAvatarBuilder.buildImage(for: thread, contactsManager: contactsManager)
-//
-// assert(call != nil)
-// // Subscribe for future call updates
-// call.addObserverAndSyncState(observer: self)
-//
-// Environment.getCurrent().callService.addObserverAndSyncState(observer:self)
+ createViews()
}
// MARK: - Create Views
- func createViews() {
-// self.view.isUserInteractionEnabled = true
-// self.view.addGestureRecognizer(OWSAnyTouchGestureRecognizer(target:self,
-// action:#selector(didTouchRootView)))
-//
-// // Dark blurred background.
-// let blurEffect = UIBlurEffect(style: .dark)
-// blurView = UIVisualEffectView(effect: blurEffect)
-// blurView.isUserInteractionEnabled = false
-// self.view.addSubview(blurView)
-//
-// // Create the video views first, as they are under the other views.
-// createVideoViews()
-//
-// createContactViews()
-// createOngoingCallControls()
-// createIncomingCallControls()
-// createSettingsNagViews()
+ private func createViews() {
+ let previewTopMargin = 30 as CGFloat
+ let previewHMargin = 20 as CGFloat
+
+ 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)
+ }
}
-// func didTouchRootView(sender: UIGestureRecognizer) {
-// if !remoteVideoView.isHidden {
-// shouldRemoteVideoControlsBeHidden = !shouldRemoteVideoControlsBeHidden
-// }
-// }
-//
-// func createVideoViews() {
-// remoteVideoView = RTCEAGLVideoView()
-// remoteVideoView.delegate = self
-// remoteVideoView.isUserInteractionEnabled = false
-// localVideoView = RTCCameraPreviewView()
-// remoteVideoView.isHidden = true
-// localVideoView.isHidden = true
-// self.view.addSubview(remoteVideoView)
-// self.view.addSubview(localVideoView)
-// }
-//
-// func createContactViews() {
-// contactNameLabel = UILabel()
-// contactNameLabel.font = UIFont.ows_lightFont(withSize:ScaleFromIPhone5To7Plus(32, 40))
-// contactNameLabel.textColor = UIColor.white
-// contactNameLabel.layer.shadowOffset = CGSize.zero
-// contactNameLabel.layer.shadowOpacity = 0.35
-// contactNameLabel.layer.shadowRadius = 4
-// self.view.addSubview(contactNameLabel)
-//
-// callStatusLabel = UILabel()
-// callStatusLabel.font = UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(19, 25))
-// callStatusLabel.textColor = UIColor.white
-// callStatusLabel.layer.shadowOffset = CGSize.zero
-// callStatusLabel.layer.shadowOpacity = 0.35
-// callStatusLabel.layer.shadowRadius = 4
-// self.view.addSubview(callStatusLabel)
-//
-// contactAvatarView = AvatarImageView()
-// self.view.addSubview(contactAvatarView)
-// }
-//
-// func createSettingsNagViews() {
-// settingsNagView = UIView()
-// settingsNagView.isHidden = true
-// self.view.addSubview(settingsNagView)
-//
-// let viewStack = UIView()
-// settingsNagView.addSubview(viewStack)
-// viewStack.autoPinWidthToSuperview()
-// viewStack.autoVCenterInSuperview()
-//
-// settingsNagDescriptionLabel = UILabel()
-// settingsNagDescriptionLabel.text = NSLocalizedString("CALL_VIEW_SETTINGS_NAG_DESCRIPTION_ALL",
-// comment: "Reminder to the user of the benefits of enabling CallKit and disabling CallKit privacy.")
-// settingsNagDescriptionLabel.font = UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(16, 18))
-// settingsNagDescriptionLabel.textColor = UIColor.white
-// settingsNagDescriptionLabel.numberOfLines = 0
-// settingsNagDescriptionLabel.lineBreakMode = .byWordWrapping
-// viewStack.addSubview(settingsNagDescriptionLabel)
-// settingsNagDescriptionLabel.autoPinWidthToSuperview()
-// settingsNagDescriptionLabel.autoPinEdge(toSuperviewEdge:.top)
-//
-// let buttonHeight = ScaleFromIPhone5To7Plus(35, 45)
-// let buttonFont = UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(14, 18))
-// let buttonCornerRadius = CGFloat(4)
-// let descriptionVSpacingHeight = ScaleFromIPhone5To7Plus(30, 60)
-//
-// let callSettingsButton = UIButton()
-// callSettingsButton.setTitle(NSLocalizedString("CALL_VIEW_SETTINGS_NAG_SHOW_CALL_SETTINGS",
-// comment: "Label for button that shows the privacy settings"), for:.normal)
-// callSettingsButton.setTitleColor(UIColor.white, for:.normal)
-// callSettingsButton.titleLabel!.font = buttonFont
-// callSettingsButton.addTarget(self, action:#selector(didPressShowCallSettings), for:.touchUpInside)
-// callSettingsButton.backgroundColor = UIColor.ows_signalBrandBlue()
-// callSettingsButton.layer.cornerRadius = buttonCornerRadius
-// callSettingsButton.clipsToBounds = true
-// viewStack.addSubview(callSettingsButton)
-// callSettingsButton.autoSetDimension(.height, toSize:buttonHeight)
-// callSettingsButton.autoPinWidthToSuperview()
-// callSettingsButton.autoPinEdge(.top, to:.bottom, of:settingsNagDescriptionLabel, withOffset:descriptionVSpacingHeight)
-//
-// let notNowButton = UIButton()
-// notNowButton.setTitle(NSLocalizedString("CALL_VIEW_SETTINGS_NAG_NOT_NOW_BUTTON",
-// comment: "Label for button that dismiss the call view's settings nag."), for:.normal)
-// notNowButton.setTitleColor(UIColor.white, for:.normal)
-// notNowButton.titleLabel!.font = buttonFont
-// notNowButton.addTarget(self, action:#selector(didPressDismissNag), for:.touchUpInside)
-// notNowButton.backgroundColor = UIColor.ows_signalBrandBlue()
-// notNowButton.layer.cornerRadius = buttonCornerRadius
-// notNowButton.clipsToBounds = true
-// viewStack.addSubview(notNowButton)
-// notNowButton.autoSetDimension(.height, toSize:buttonHeight)
-// notNowButton.autoPinWidthToSuperview()
-// notNowButton.autoPinEdge(toSuperviewEdge:.bottom)
-// notNowButton.autoPinEdge(.top, to:.bottom, of:callSettingsButton, withOffset:12)
-// }
-//
-// func buttonSize() -> CGFloat {
-// return ScaleFromIPhone5To7Plus(84, 108)
-// }
-//
-// func buttonInset() -> CGFloat {
-// return ScaleFromIPhone5To7Plus(7, 9)
-// }
-//
-// func createOngoingCallControls() {
-//
-//// textMessageButton = createButton(imageName:"message-active-wide",
-//// action:#selector(didPressTextMessage))
-// speakerPhoneButton = createButton(imageName:"speaker-inactive-wide",
-// action:#selector(didPressSpeakerphone))
-// hangUpButton = createButton(imageName:"hangup-active-wide",
-// action:#selector(didPressHangup))
-// audioModeMuteButton = createButton(imageName:"mute-unselected-wide",
-// action:#selector(didPressMute))
-// videoModeMuteButton = createButton(imageName:"video-mute-unselected",
-// action:#selector(didPressMute))
-// audioModeVideoButton = createButton(imageName:"video-inactive-wide",
-// action:#selector(didPressVideo))
-// videoModeVideoButton = createButton(imageName:"video-video-unselected",
-// action:#selector(didPressVideo))
-//
-// setButtonSelectedImage(button: audioModeMuteButton, imageName: "mute-selected-wide")
-// setButtonSelectedImage(button: videoModeMuteButton, imageName: "video-mute-selected")
-// setButtonSelectedImage(button: audioModeVideoButton, imageName: "video-active-wide")
-// setButtonSelectedImage(button: videoModeVideoButton, imageName: "video-video-selected")
-// setButtonSelectedImage(button: speakerPhoneButton, imageName: "speaker-active-wide")
-//
-// ongoingCallView = createContainerForCallControls(controlGroups : [
-// [audioModeMuteButton, speakerPhoneButton, audioModeVideoButton ],
-// [videoModeMuteButton, hangUpButton, videoModeVideoButton ]
-// ])
-// }
-//
-// func setButtonSelectedImage(button: UIButton, imageName: String) {
-// let image = UIImage(named:imageName)
-// assert(image != nil)
-// button.setImage(image, for:.selected)
-// }
-//
-// func createIncomingCallControls() {
-//
-// acceptIncomingButton = createButton(imageName:"call-active-wide",
-// action:#selector(didPressAnswerCall))
-// declineIncomingButton = createButton(imageName:"hangup-active-wide",
-// action:#selector(didPressDeclineCall))
-//
-// incomingCallView = createContainerForCallControls(controlGroups : [
-// [acceptIncomingButton, declineIncomingButton ]
-// ])
-// }
-//
-// func createContainerForCallControls(controlGroups: [[UIView]]) -> UIView {
-// let containerView = UIView()
-// self.view.addSubview(containerView)
-// var rows: [UIView] = []
-// for controlGroup in controlGroups {
-// rows.append(rowWithSubviews(subviews:controlGroup))
-// }
-// let rowspacing = ScaleFromIPhone5To7Plus(6, 7)
-// var prevRow: UIView?
-// for row in rows {
-// containerView.addSubview(row)
-// row.autoHCenterInSuperview()
-// if prevRow != nil {
-// row.autoPinEdge(.top, to:.bottom, of:prevRow!, withOffset:rowspacing)
-// }
-// prevRow = row
-// }
-//
-// containerView.setContentHuggingVerticalHigh()
-// rows.first!.autoPinEdge(toSuperviewEdge:.top)
-// rows.last!.autoPinEdge(toSuperviewEdge:.bottom)
-// return containerView
-// }
-//
-// func createButton(imageName: String, action: Selector) -> UIButton {
-// let image = UIImage(named:imageName)
-// assert(image != nil)
-// let button = UIButton()
-// button.setImage(image, for:.normal)
-// button.imageEdgeInsets = UIEdgeInsets(top: buttonInset(),
-// left: buttonInset(),
-// bottom: buttonInset(),
-// right: buttonInset())
-// button.addTarget(self, action:action, for:.touchUpInside)
-// button.autoSetDimension(.width, toSize:buttonSize())
-// button.autoSetDimension(.height, toSize:buttonSize())
-// return button
-// }
-//
-// // Creates a row containing a given set of subviews.
-// func rowWithSubviews(subviews: [UIView]) -> UIView {
-// let row = UIView()
-// row.setContentHuggingVerticalHigh()
-// row.autoSetDimension(.height, toSize:buttonSize())
-//
-// if subviews.count > 1 {
-// // If there's more than one subview in the row,
-// // space them evenly within the row.
-// var lastSubview: UIView?
-// for subview in subviews {
-// row.addSubview(subview)
-// subview.setContentHuggingHorizontalHigh()
-// subview.autoVCenterInSuperview()
-//
-// if lastSubview != nil {
-// let spacer = UIView()
-// spacer.isHidden = true
-// row.addSubview(spacer)
-// spacer.autoPinEdge(.left, to:.right, of:lastSubview!)
-// spacer.autoPinEdge(.right, to:.left, of:subview)
-// spacer.setContentHuggingHorizontalLow()
-// spacer.autoVCenterInSuperview()
-//
-// if subviews.count == 2 {
-// // special case to hardcode the spacer's size when there is only 1 spacer.
-// spacer.autoSetDimension(.width, toSize: ScaleFromIPhone5To7Plus(46, 60))
-// } else {
-// spacer.autoSetDimension(.width, toSize: ScaleFromIPhone5To7Plus(3, 5))
-// }
-// }
-//
-// lastSubview = subview
-// }
-// subviews.first!.autoPinEdge(toSuperviewEdge:.left)
-// subviews.last!.autoPinEdge(toSuperviewEdge:.right)
-// } else if subviews.count == 1 {
-// // If there's only one subview in this row, center it.
-// let subview = subviews.first!
-// row.addSubview(subview)
-// subview.autoVCenterInSuperview()
-// subview.autoPinWidthToSuperview()
-// }
-//
-// return row
-// }
-//
-// // MARK: - Layout
-//
-// override func updateViewConstraints() {
-// if !hasConstraints {
-// // We only want to create our constraints once.
-// //
-// // Note that constraints are also created elsewhere.
-// // This only creates the constraints for the top-level contents of the view.
-// hasConstraints = true
-//
-// let topMargin = CGFloat(40)
-// let contactHMargin = CGFloat(30)
-// let contactVSpacing = CGFloat(3)
-// let ongoingHMargin = ScaleFromIPhone5To7Plus(46, 72)
-// let incomingHMargin = ScaleFromIPhone5To7Plus(46, 72)
-// let settingsNagHMargin = CGFloat(30)
-// let ongoingBottomMargin = ScaleFromIPhone5To7Plus(23, 41)
-// let incomingBottomMargin = CGFloat(41)
-// let settingsNagBottomMargin = CGFloat(41)
-// let avatarTopSpacing = ScaleFromIPhone5To7Plus(25, 50)
-// // The buttons have built-in 10% margins, so to appear centered
-// // the avatar's bottom spacing should be a bit less.
-// let avatarBottomSpacing = ScaleFromIPhone5To7Plus(18, 41)
-// // Layout of the local video view is a bit unusual because
-// // although the view is square, it will be used
-// let videoPreviewHMargin = CGFloat(0)
-//
-// // Dark blurred background.
-// blurView.autoPinEdgesToSuperviewEdges()
-//
-// localVideoView.autoPinEdge(toSuperviewEdge:.right, withInset:videoPreviewHMargin)
-// localVideoView.autoPinEdge(toSuperviewEdge:.top, withInset:topMargin)
-// let localVideoSize = ScaleFromIPhone5To7Plus(80, 100)
-// localVideoView.autoSetDimension(.width, toSize:localVideoSize)
-// localVideoView.autoSetDimension(.height, toSize:localVideoSize)
-//
-// contactNameLabel.autoPinEdge(toSuperviewEdge:.top, withInset:topMargin)
-// contactNameLabel.autoPinEdge(toSuperviewEdge:.left, withInset:contactHMargin)
-// contactNameLabel.setContentHuggingVerticalHigh()
-//
-// callStatusLabel.autoPinEdge(.top, to:.bottom, of:contactNameLabel, withOffset:contactVSpacing)
-// callStatusLabel.autoPinEdge(toSuperviewEdge:.left, withInset:contactHMargin)
-// callStatusLabel.setContentHuggingVerticalHigh()
-//
-// contactAvatarView.autoPinEdge(.top, to:.bottom, of:callStatusLabel, withOffset:+avatarTopSpacing)
-// contactAvatarView.autoPinEdge(.bottom, to:.top, of:ongoingCallView, withOffset:-avatarBottomSpacing)
-// contactAvatarView.autoHCenterInSuperview()
-// // Stretch that avatar to fill the available space.
-// contactAvatarView.setContentHuggingLow()
-// contactAvatarView.setCompressionResistanceLow()
-// // Preserve square aspect ratio of contact avatar.
-// contactAvatarView.autoMatch(.width, to:.height, of:contactAvatarView)
-//
-// // Ongoing call controls
-// ongoingCallView.autoPinEdge(toSuperviewEdge:.bottom, withInset:ongoingBottomMargin)
-// ongoingCallView.autoPinWidthToSuperview(withMargin:ongoingHMargin)
-// ongoingCallView.setContentHuggingVerticalHigh()
-//
-// // Incoming call controls
-// incomingCallView.autoPinEdge(toSuperviewEdge:.bottom, withInset:incomingBottomMargin)
-// incomingCallView.autoPinWidthToSuperview(withMargin:incomingHMargin)
-// incomingCallView.setContentHuggingVerticalHigh()
-//
-// // Settings nag views
-// settingsNagView.autoPinEdge(toSuperviewEdge:.bottom, withInset:settingsNagBottomMargin)
-// settingsNagView.autoPinWidthToSuperview(withMargin:settingsNagHMargin)
-// settingsNagView.autoPinEdge(.top, to:.bottom, of:callStatusLabel)
-// }
-//
-// updateRemoteVideoLayout()
-// updateLocalVideoLayout()
-//
-// super.updateViewConstraints()
-// }
-//
-// internal func updateRemoteVideoLayout() {
-// NSLayoutConstraint.deactivate(self.remoteVideoConstraints)
-//
-// var constraints: [NSLayoutConstraint] = []
-//
-// // We fill the screen with the remote video. The remote video's
-// // aspect ratio may not (and in fact will very rarely) match the
-// // aspect ratio of the current device, so parts of the remote
-// // video will be hidden offscreen.
-// //
-// // It's better to trim the remote video than to adopt a letterboxed
-// // layout.
-// if remoteVideoSize.width > 0 && remoteVideoSize.height > 0 &&
-// self.view.bounds.size.width > 0 && self.view.bounds.size.height > 0 {
-//
-// var remoteVideoWidth = self.view.bounds.size.width
-// var remoteVideoHeight = self.view.bounds.size.height
-// if remoteVideoSize.width / self.view.bounds.size.width > remoteVideoSize.height / self.view.bounds.size.height {
-// remoteVideoWidth = round(self.view.bounds.size.height * remoteVideoSize.width / remoteVideoSize.height)
-// } else {
-// remoteVideoHeight = round(self.view.bounds.size.width * remoteVideoSize.height / remoteVideoSize.width)
-// }
-// constraints.append(remoteVideoView.autoSetDimension(.width, toSize:remoteVideoWidth))
-// constraints.append(remoteVideoView.autoSetDimension(.height, toSize:remoteVideoHeight))
-// constraints += remoteVideoView.autoCenterInSuperview()
-//
-// remoteVideoView.frame = CGRect(origin:CGPoint.zero,
-// size:CGSize(width:remoteVideoWidth,
-// height:remoteVideoHeight))
-//
-// remoteVideoView.isHidden = false
-// } else {
-// constraints += remoteVideoView.autoPinEdgesToSuperviewEdges()
-// remoteVideoView.isHidden = true
-// }
-//
-// self.remoteVideoConstraints = constraints
-//
-// // We need to force relayout to occur immediately (and not
-// // wait for a UIKit layout/render pass) or the remoteVideoView
-// // (which presumably is updating its CALayer directly) will
-// // ocassionally appear to have bad frames.
-// remoteVideoView.setNeedsLayout()
-// remoteVideoView.superview?.setNeedsLayout()
-// remoteVideoView.layoutIfNeeded()
-// remoteVideoView.superview?.layoutIfNeeded()
-//
-// updateCallUI(callState: call.state)
-// }
-//
-// internal func updateLocalVideoLayout() {
-//
-// NSLayoutConstraint.deactivate(self.localVideoConstraints)
-//
-// var constraints: [NSLayoutConstraint] = []
-//
-// if localVideoView.isHidden {
-// let contactHMargin = CGFloat(30)
-// constraints.append(contactNameLabel.autoPinEdge(toSuperviewEdge:.right, withInset:contactHMargin))
-// constraints.append(callStatusLabel.autoPinEdge(toSuperviewEdge:.right, withInset:contactHMargin))
-// } else {
-// let spacing = CGFloat(10)
-// constraints.append(contactNameLabel.autoPinEdge(.right, to:.left, of:localVideoView, withOffset:-spacing))
-// constraints.append(callStatusLabel.autoPinEdge(.right, to:.left, of:localVideoView, withOffset:-spacing))
-// }
-//
-// self.localVideoConstraints = constraints
-// updateCallUI(callState: call.state)
-// }
-//
-// // MARK: - Methods
-//
-// func showCallFailed(error: Error) {
-// // TODO Show something in UI.
-// Logger.error("\(TAG) call failed with error: \(error)")
-// }
-//
-// // MARK: - View State
-//
-// func localizedTextForCallState(_ callState: CallState) -> String {
-// assert(Thread.isMainThread)
-//
-// switch callState {
-// case .idle, .remoteHangup, .localHangup:
-// return NSLocalizedString("IN_CALL_TERMINATED", comment: "Call setup status label")
-// case .dialing:
-// return NSLocalizedString("IN_CALL_CONNECTING", comment: "Call setup status label")
-// case .remoteRinging, .localRinging:
-// return NSLocalizedString("IN_CALL_RINGING", comment: "Call setup status label")
-// case .answering:
-// return NSLocalizedString("IN_CALL_SECURING", comment: "Call setup status label")
-// case .connected:
-// if let call = self.call {
-// let callDuration = call.connectionDuration()
-// let callDurationDate = Date(timeIntervalSinceReferenceDate:callDuration)
-// if dateFormatter == nil {
-// dateFormatter = DateFormatter()
-// dateFormatter!.dateFormat = "HH:mm:ss"
-// dateFormatter!.timeZone = TimeZone(identifier:"UTC")!
-// }
-// var formattedDate = dateFormatter!.string(from: callDurationDate)
-// if formattedDate.hasPrefix("00:") {
-// // Don't show the "hours" portion of the date format unless the
-// // call duration is at least 1 hour.
-// formattedDate = formattedDate.substring(from: formattedDate.index(formattedDate.startIndex, offsetBy: 3))
-// } else {
-// // If showing the "hours" portion of the date format, strip any leading
-// // zeroes.
-// if formattedDate.hasPrefix("0") {
-// formattedDate = formattedDate.substring(from: formattedDate.index(formattedDate.startIndex, offsetBy: 1))
-// }
-// }
-// return formattedDate
-// } else {
-// return NSLocalizedString("IN_CALL_TALKING", comment: "Call setup status label")
-// }
-// case .remoteBusy:
-// return NSLocalizedString("END_CALL_RESPONDER_IS_BUSY", comment: "Call setup status label")
-// case .localFailure:
-// if let error = call.error {
-// switch error {
-// case .timeout(description: _):
-// if self.call.direction == .outgoing {
-// return NSLocalizedString("CALL_SCREEN_STATUS_NO_ANSWER", comment: "Call setup status label after outgoing call times out")
-// }
-// default:
-// break
-// }
-// }
-//
-// return NSLocalizedString("END_CALL_UNCATEGORIZED_FAILURE", comment: "Call setup status label")
-// }
-// }
-//
-// func updateCallStatusLabel(callState: CallState) {
-// assert(Thread.isMainThread)
-//
-// let text = String(format: CallStrings.callStatusFormat,
-// localizedTextForCallState(callState))
-// self.callStatusLabel.text = text
-// }
-//
-// func updateCallUI(callState: CallState) {
-// assert(Thread.isMainThread)
-// updateCallStatusLabel(callState: callState)
-//
-// if isShowingSettingsNag {
-// settingsNagView.isHidden = false
-// contactAvatarView.isHidden = true
-// ongoingCallView.isHidden = true
-// return
-// }
-//
-// audioModeMuteButton.isSelected = call.isMuted
-// videoModeMuteButton.isSelected = call.isMuted
-// audioModeVideoButton.isSelected = call.hasLocalVideo
-// videoModeVideoButton.isSelected = call.hasLocalVideo
-// speakerPhoneButton.isSelected = call.isSpeakerphoneEnabled
-//
-// // Show Incoming vs. Ongoing call controls
-// let isRinging = callState == .localRinging
-// incomingCallView.isHidden = !isRinging
-// incomingCallView.isUserInteractionEnabled = isRinging
-// ongoingCallView.isHidden = isRinging
-// ongoingCallView.isUserInteractionEnabled = !isRinging
-//
-// // Rework control state if remote video is available.
-// let hasRemoteVideo = !remoteVideoView.isHidden
-// contactAvatarView.isHidden = hasRemoteVideo
-//
-// // Rework control state if local video is available.
-// let hasLocalVideo = !localVideoView.isHidden
-// for subview in [speakerPhoneButton, audioModeMuteButton, audioModeVideoButton] {
-// subview?.isHidden = hasLocalVideo
-// }
-// for subview in [videoModeMuteButton, videoModeVideoButton] {
-// subview?.isHidden = !hasLocalVideo
-// }
-//
-// // Also hide other controls if user has tapped to hide them.
-// if shouldRemoteVideoControlsBeHidden && !remoteVideoView.isHidden {
-// contactNameLabel.isHidden = true
-// callStatusLabel.isHidden = true
-// ongoingCallView.isHidden = true
-// } else {
-// contactNameLabel.isHidden = false
-// callStatusLabel.isHidden = false
-// }
-//
-// // Dismiss Handling
-// switch callState {
-// case .remoteHangup, .remoteBusy, .localFailure:
-// Logger.debug("\(TAG) dismissing after delay because new state is \(callState)")
-// dismissIfPossible(shouldDelay:true)
-// case .localHangup:
-// Logger.debug("\(TAG) dismissing immediately from local hangup")
-// dismissIfPossible(shouldDelay:false)
-// default: break
-// }
-//
-// if callState == .connected {
-// if callDurationTimer == nil {
-// let kDurationUpdateFrequencySeconds = 1 / 20.0
-// callDurationTimer = Timer.scheduledTimer(timeInterval: kDurationUpdateFrequencySeconds,
-// target:self,
-// selector:#selector(updateCallDuration),
-// userInfo:nil,
-// repeats:true)
-// }
-// } else {
-// callDurationTimer?.invalidate()
-// callDurationTimer = nil
-// }
-// }
-//
-// func updateCallDuration(timer: Timer?) {
-// updateCallStatusLabel(callState: call.state)
-// }
-//
-// // MARK: - Actions
-//
-// /**
-// * Ends a connected call. Do not confuse with `didPressDeclineCall`.
-// */
-// func didPressHangup(sender: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-// if let call = self.call {
-// callUIAdapter.localHangupCall(call)
-// } else {
-// Logger.warn("\(TAG) hung up, but call was unexpectedly nil")
-// }
-//
-// dismissIfPossible(shouldDelay:false)
-// }
-//
-// func didPressMute(sender muteButton: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-// muteButton.isSelected = !muteButton.isSelected
-// if let call = self.call {
-// callUIAdapter.setIsMuted(call: call, isMuted: muteButton.isSelected)
-// } else {
-// Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil")
-// }
-// }
-//
-// func didPressSpeakerphone(sender speakerphoneButton: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-// speakerphoneButton.isSelected = !speakerphoneButton.isSelected
-// if let call = self.call {
-// callUIAdapter.setIsSpeakerphoneEnabled(call: call, isEnabled: speakerphoneButton.isSelected)
-// } else {
-// Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil")
-// }
-// }
-//
-// func didPressTextMessage(sender speakerphoneButton: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-//
-// dismissIfPossible(shouldDelay:false)
-// }
-//
-// func didPressAnswerCall(sender: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-//
-// guard let call = self.call else {
-// Logger.error("\(TAG) call was unexpectedly nil. Terminating call.")
-//
-// let text = String(format: CallStrings.callStatusFormat,
-// NSLocalizedString("END_CALL_UNCATEGORIZED_FAILURE", comment: "Call setup status label"))
-// self.callStatusLabel.text = text
-//
-// dismissIfPossible(shouldDelay:true)
-// return
-// }
-//
-// callUIAdapter.answerCall(call)
-// }
-//
-// func didPressVideo(sender: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-// let hasLocalVideo = !sender.isSelected
-// if let call = self.call {
-// callUIAdapter.setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo)
-// } else {
-// Logger.warn("\(TAG) pressed video, but call was unexpectedly nil")
-// }
-// }
-//
-// /**
-// * Denies an incoming not-yet-connected call, Do not confuse with `didPressHangup`.
-// */
-// func didPressDeclineCall(sender: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-//
-// if let call = self.call {
-// callUIAdapter.declineCall(call)
-// } else {
-// Logger.warn("\(TAG) denied call, but call was unexpectedly nil")
-// }
-//
-// dismissIfPossible(shouldDelay:false)
-// }
-//
-// func didPressShowCallSettings(sender: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-//
-// markSettingsNagAsComplete()
-//
-// dismissIfPossible(shouldDelay: false, ignoreNag: true, completion: {
-// // Find the frontmost presented UIViewController from which to present the
-// // settings views.
-// let fromViewController = UIApplication.shared.frontmostViewController
-// assert(fromViewController != nil)
-//
-// // Construct the "settings" view & push the "privacy settings" view.
-// let navigationController = UIStoryboard.main.instantiateViewController(withIdentifier: "SettingsNavigationController") as! UINavigationController
-// assert(navigationController.viewControllers.count == 1)
-// let privacySettingsViewController = PrivacySettingsTableViewController()
-// navigationController.pushViewController(privacySettingsViewController, animated:false)
-//
-// fromViewController?.present(navigationController, animated: true, completion: nil)
-// })
-// }
-//
-// func didPressDismissNag(sender: UIButton) {
-// Logger.info("\(TAG) called \(#function)")
-//
-// markSettingsNagAsComplete()
-//
-// dismissIfPossible(shouldDelay: false, ignoreNag: true)
-// }
-//
-// // We only show the "blocking" settings nag until the user has chosen
-// // to view the privacy settings _or_ dismissed the nag at least once.
-// //
-// // In either case, we set the "CallKit enabled" and "CallKit privacy enabled"
-// // settings to their default values to indicate that the user has reviewed
-// // them.
-// private func markSettingsNagAsComplete() {
-// Logger.info("\(TAG) called \(#function)")
-//
-// let preferences = Environment.getCurrent().preferences!
-//
-// preferences.setIsCallKitEnabled(preferences.isCallKitEnabled())
-// preferences.setIsCallKitPrivacyEnabled(preferences.isCallKitPrivacyEnabled())
-// }
-//
-// // MARK: - CallObserver
-//
-// internal func stateDidChange(call: SignalCall, state: CallState) {
-// AssertIsOnMainThread()
-// Logger.info("\(self.TAG) new call status: \(state)")
-// self.updateCallUI(callState: state)
-// }
-//
-// internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) {
-// AssertIsOnMainThread()
-// self.updateCallUI(callState: call.state)
-// }
-//
-// internal func muteDidChange(call: SignalCall, isMuted: Bool) {
-// AssertIsOnMainThread()
-// self.updateCallUI(callState: call.state)
-// }
-//
-// internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) {
-// AssertIsOnMainThread()
-// self.updateCallUI(callState: call.state)
-// }
-//
-// // MARK: - Video
-//
-// internal func updateLocalVideoTrack(localVideoTrack: RTCVideoTrack?) {
-// AssertIsOnMainThread()
-// guard self.localVideoTrack != localVideoTrack else {
-// return
-// }
-//
-// self.localVideoTrack = localVideoTrack
-//
-// var source: RTCAVFoundationVideoSource?
-// if localVideoTrack?.source is RTCAVFoundationVideoSource {
-// source = localVideoTrack?.source as! RTCAVFoundationVideoSource
-// }
-// localVideoView.captureSession = source?.captureSession
-// let isHidden = source == nil
-// Logger.info("\(TAG) \(#function) isHidden: \(isHidden)")
-// localVideoView.isHidden = isHidden
-//
-// updateLocalVideoLayout()
-// }
-//
-// internal func updateRemoteVideoTrack(remoteVideoTrack: RTCVideoTrack?) {
-// AssertIsOnMainThread()
-// guard self.remoteVideoTrack != remoteVideoTrack else {
-// return
-// }
-//
-// self.remoteVideoTrack?.remove(remoteVideoView)
-// self.remoteVideoTrack = nil
-// remoteVideoView.renderFrame(nil)
-// self.remoteVideoTrack = remoteVideoTrack
-// self.remoteVideoTrack?.add(remoteVideoView)
-// shouldRemoteVideoControlsBeHidden = false
-//
-// if remoteVideoTrack == nil {
-// remoteVideoSize = CGSize.zero
-// }
-//
-// updateRemoteVideoLayout()
-// }
-//
-// internal func dismissIfPossible(shouldDelay: Bool, ignoreNag: Bool = false, completion: (() -> Swift.Void)? = nil) {
-// if hasDismissed {
-// // Don't dismiss twice.
-// return
-// } else if !ignoreNag &&
-// call.direction == .incoming &&
-// UIDevice.current.supportsCallKit &&
-// (!Environment.getCurrent().preferences.isCallKitEnabled() ||
-// Environment.getCurrent().preferences.isCallKitPrivacyEnabled()) {
-//
-// isShowingSettingsNag = true
-//
-// // Update the nag view's copy to reflect the settings state.
-// if Environment.getCurrent().preferences.isCallKitEnabled() {
-// settingsNagDescriptionLabel.text = NSLocalizedString("CALL_VIEW_SETTINGS_NAG_DESCRIPTION_PRIVACY",
-// comment: "Reminder to the user of the benefits of disabling CallKit privacy.")
-// } else {
-// settingsNagDescriptionLabel.text = NSLocalizedString("CALL_VIEW_SETTINGS_NAG_DESCRIPTION_ALL",
-// comment: "Reminder to the user of the benefits of enabling CallKit and disabling CallKit privacy.")
-// }
-// settingsNagDescriptionLabel.superview?.setNeedsLayout()
-//
-// if Environment.getCurrent().preferences.isCallKitEnabledSet() ||
-// Environment.getCurrent().preferences.isCallKitPrivacySet() {
-// // User has already touched these preferences, only show
-// // the "fleeting" nag, not the "blocking" nag.
-//
-// // Show nag for N seconds.
-// DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
-// guard let strongSelf = self else { return }
-// strongSelf.dismissIfPossible(shouldDelay: false, ignoreNag: true)
-// }
-// }
-// } else if shouldDelay {
-// hasDismissed = true
-// DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
-// guard let strongSelf = self else { return }
-// strongSelf.dismiss(animated: true, completion:completion)
-// }
-// } else {
-// hasDismissed = true
-// self.dismiss(animated: false, completion:completion)
-// }
-// }
-//
-// // MARK: - CallServiceObserver
-//
-// internal func didUpdateCall(call: SignalCall?) {
-// // Do nothing.
-// }
-//
-// internal func didUpdateVideoTracks(localVideoTrack: RTCVideoTrack?,
-// remoteVideoTrack: RTCVideoTrack?) {
-// AssertIsOnMainThread()
-//
-// updateLocalVideoTrack(localVideoTrack:localVideoTrack)
-// updateRemoteVideoTrack(remoteVideoTrack:remoteVideoTrack)
-// }
-//
-// // MARK: - RTCEAGLVideoViewDelegate
-//
-// internal func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
-// AssertIsOnMainThread()
-//
-// if videoView != remoteVideoView {
-// return
-// }
-//
-// Logger.info("\(TAG) \(#function): \(size)")
-//
-// remoteVideoSize = size
-// updateRemoteVideoLayout()
-// }
+ 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 attachment.fileExtension() != nil {
+ let fileExtensionLabel = UILabel()
+ fileExtensionLabel.text = String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT",
+ comment: "Format string for file extension label in call interstitial view"),
+ attachment.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("ATTACHMENT_APPROVAL_CANCEL_BUTTON",
+ comment: "Label for 'cancel' button in the 'attachment approval' dialog."),
+ 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: {
+ if successCompletion != nil {
+ successCompletion!()
+ }
+ })
+ }
}
diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m
index 1c1996887..6ef9dc290 100644
--- a/Signal/src/view controllers/MessagesViewController.m
+++ b/Signal/src/view controllers/MessagesViewController.m
@@ -2496,11 +2496,15 @@ typedef enum : NSUInteger {
[attachment hasError]) {
// TODO: Add UI.
} else {
-
- // TODO: Add UI.
- UIViewController *viewController = [[AttachmentApprovalViewController alloc] initWithAttachment:attachment];
- // [self tryToSendMessageAttachmentWithData:imageData
- // dataUTI:dataUTI];
+ __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];
}
}
diff --git a/Signal/src/view controllers/SignalAttachment.swift b/Signal/src/view controllers/SignalAttachment.swift
index 7bea57db4..b9dfe55dd 100644
--- a/Signal/src/view controllers/SignalAttachment.swift
+++ b/Signal/src/view controllers/SignalAttachment.swift
@@ -30,50 +30,55 @@ enum SignalAttachmentError: String {
// TODO: Show error on error.
// TODO: Show progress on upload.
class SignalAttachment: NSObject {
-
+
static let TAG = "[SignalAttachment]"
-
+
// MARK: Properties
-
- let data : Data!
-
+
+ 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? {
+ 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
* (org/thoughtcrime/securesms/mms/PushMediaConstraints.java)
*/
- static let kMaxFileSize_Gif = 5 * 1024 * 1024
- static let kMaxFileSize_Image = 420 * 1024
- static let kMaxFileSize_Video = 100 * 1024 * 1024
- static let kMaxFileSize_Audio = 100 * 1024 * 1024
+ static let kMaxFileSizeGif = 5 * 1024 * 1024
+ static let kMaxFileSizeImage = 420 * 1024
+ static let kMaxFileSizeVideo = 100 * 1024 * 1024
+ static let kMaxFileSizeAudio = 100 * 1024 * 1024
// TODO: What should the max file size on "other" attachments be?
- static let kMaxFileSize_Generic = 25 * 1024 * 1024
-
+ static let kMaxFileSizeGeneric = 100 * 1024 * 1024
+
// MARK: Constructor
-
+
// This method should not be called directly; use the factory
// methods instead.
- internal required init(data : Data!, dataUTI : String!) {
+ internal required init(data: Data!, dataUTI: String!) {
self.data = data
self.dataUTI = dataUTI
super.init()
}
-
+
public func hasError() -> Bool {
return error != nil
}
@@ -87,7 +92,17 @@ class SignalAttachment: NSObject {
}
return mimeType?.takeRetainedValue() as? String
}
-
+
+ // Returns the file extension for this attachment or nil if no file extension
+ // can be identified.
+ public func fileExtension() -> String? {
+ let fileExtension = UTTypeCopyPreferredTagWithClass(dataUTI as CFString, kUTTagClassFilenameExtension)
+ guard fileExtension != nil else {
+ return nil
+ }
+ return fileExtension?.takeRetainedValue() as? String
+ }
+
// Returns the set of UTIs that correspond to valid _input_ image formats
// for Signal attachments.
//
@@ -99,10 +114,10 @@ class SignalAttachment: NSObject {
return [
kUTTypeJPEG as String,
kUTTypeGIF as String,
- kUTTypePNG as String,
+ kUTTypePNG as String
]
}
-
+
// Returns the set of UTIs that correspond to valid _output_ image formats
// for Signal attachments.
//
@@ -111,20 +126,20 @@ class SignalAttachment: NSObject {
return [
kUTTypeJPEG as String,
kUTTypeGIF as String,
- kUTTypePNG as String,
+ kUTTypePNG as String
]
}
-
+
// Returns the set of UTIs that correspond to valid video formats
// for Signal attachments.
//
// TODO: We need to finalize which formats we support.
private class func videoUTISet() -> Set! {
return [
- kUTTypeMPEG4 as String,
+ kUTTypeMPEG4 as String
]
}
-
+
// Returns the set of UTIs that correspond to valid audio formats
// for Signal attachments.
//
@@ -132,16 +147,28 @@ class SignalAttachment: NSObject {
private class func audioUTISet() -> Set! {
return [
kUTTypeMP3 as String,
- kUTTypeMPEG4Audio as String,
+ kUTTypeMPEG4Audio as String
]
}
-
+
// Returns the set of UTIs that correspond to valid input formats
// for Signal attachments.
public class func validInputUTISet() -> Set! {
return inputImageUTISet().union(videoUTISet().union(audioUTISet()))
}
-
+
+ public func isImage() -> Bool {
+ return SignalAttachment.outputImageUTISet().contains(dataUTI)
+ }
+
+ public func isVideo() -> Bool {
+ return SignalAttachment.videoUTISet().contains(dataUTI)
+ }
+
+ public func isAudio() -> Bool {
+ return SignalAttachment.audioUTISet().contains(dataUTI)
+ }
+
// Returns an attachment from the pasteboard, or nil if no attachment
// can be found.
//
@@ -174,39 +201,39 @@ class SignalAttachment: NSObject {
}
}
// TODO: We could handle generic attachments at this point.
-
+
return nil
}
-
+
// 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(withData imageData : Data?, dataUTI : String!) -> SignalAttachment! {
+ public class func imageAttachment(withData imageData: Data?, dataUTI: String!) -> SignalAttachment! {
assert(dataUTI.characters.count > 0)
assert(imageData != nil)
guard let imageData = imageData else {
return nil
}
-
+
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 dataUTI == kUTTypeGIF as String {
- guard imageData.count <= kMaxFileSize_Gif else {
+ guard imageData.count <= kMaxFileSizeGif else {
attachment.error = .fileSizeTooLarge
return attachment
}
@@ -214,71 +241,91 @@ class SignalAttachment: NSObject {
// animated.
//
// TODO: Consider re-encoding non-animated GIFs as JPEG?
- Logger.verbose("\(TAG) Sending raw image/gif to retain any animation")
+ Logger.verbose("\(TAG) Sending raw \(attachment.mimeType()) to retain any animation")
return attachment
} else {
guard let image = UIImage(data:imageData) else {
attachment.error = .couldNotParseImage
return attachment
}
-
- // If the proposed attachment already is a JPEG,
- // and already conforms to the file size and
- // content size limits, don't recompress it.
- //
- // TODO: Should non-JPEGs always be converted to JPEG?
- if dataUTI == kUTTypeJPEG as String {
- let imageUploadQuality = Environment.preferences().imageUploadQuality()
- let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
- if (image.size.width <= maxSize &&
- image.size.height <= maxSize &&
- imageData.count <= kMaxFileSize_Image) {
- Logger.verbose("\(TAG) Sending raw image/jpeg")
- 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) Converting attachment to image/jpeg")
- return recompressImageAsJPEG(image : image, attachment : attachment)
+
+ Logger.verbose("\(TAG) Compressing attachment as image/jpeg")
+ return compressImageAsJPEG(image : image, attachment : attachment)
}
}
-
+
+ // If the proposed attachment already is a JPEG, and already conforms to the
+ // file size and content size limits, don't recompress it.
+ //
+ // TODO: Should non-JPEGs always be converted to JPEG?
+ private class func isInputImageValidOutputImage(image: UIImage?, imageData: Data?, dataUTI: String!) -> Bool {
+ guard let image = image else {
+ return false
+ }
+ guard let imageData = imageData else {
+ return false
+ }
+ if dataUTI == kUTTypeJPEG as String {
+ let imageUploadQuality = Environment.preferences().imageUploadQuality()
+ let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
+ 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(withImage image : UIImage?, dataUTI : String!) -> SignalAttachment! {
+ public class func imageAttachment(withImage image: UIImage?, dataUTI: String!) -> SignalAttachment! {
assert(dataUTI.characters.count > 0)
-
+
guard let image = image else {
return nil
}
-
+
// Make a placeholder attachment on which to hang errors if necessary.
let attachment = SignalAttachment(data : Data(), dataUTI: dataUTI)
-
- Logger.verbose("\(TAG) Converting attachment to image/jpeg")
- return recompressImageAsJPEG(image : image, attachment : attachment)
+ attachment.image = image
+
+ Logger.verbose("\(TAG) Writing \(attachment.mimeType()) as image/jpeg")
+ return compressImageAsJPEG(image : image, attachment : attachment)
}
- private class func recompressImageAsJPEG(image : UIImage!, attachment : SignalAttachment!) -> SignalAttachment! {
+ private class func compressImageAsJPEG(image: UIImage!, attachment: SignalAttachment!) -> SignalAttachment! {
assert(attachment.error == nil)
-
+
var imageUploadQuality = Environment.preferences().imageUploadQuality()
-
+
while true {
let maxSize = maxSizeForImage(image: image, imageUploadQuality:imageUploadQuality)
- let resizedImage = imageScaled(image, toMaxSize: maxSize)
- guard let jpgImageData = UIImageJPEGRepresentation(resizedImage,
+ 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 <= kMaxFileSize_Image {
- return SignalAttachment(data : jpgImageData, dataUTI: kUTTypeJPEG as String)
+
+ 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.
@@ -295,14 +342,13 @@ class SignalAttachment: NSObject {
}
}
}
-
+
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 {
+ } else {
scaleFactor = size / image.size.height
}
let newSize = CGSize(width: CGFloat(image.size.width * scaleFactor), height: CGFloat(image.size.height * scaleFactor))
@@ -312,7 +358,7 @@ class SignalAttachment: NSObject {
UIGraphicsEndImageContext()
return updatedImage!
}
-
+
private class func maxSizeForImage(image: UIImage, imageUploadQuality: TSImageQuality) -> CGFloat {
switch imageUploadQuality {
case .uncropped:
@@ -325,7 +371,7 @@ class SignalAttachment: NSObject {
return 512
}
}
-
+
private class func jpegCompressionQuality(imageUploadQuality: TSImageQuality) -> CGFloat {
switch imageUploadQuality {
case .uncropped:
@@ -338,79 +384,79 @@ class SignalAttachment: NSObject {
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(withData data : Data?, dataUTI : String!) -> SignalAttachment! {
+ public class func videoAttachment(withData data: Data?, dataUTI: String!) -> SignalAttachment! {
return newAttachment(withData : data,
dataUTI : dataUTI,
validUTISet : videoUTISet(),
- maxFileSize : kMaxFileSize_Video)
+ 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(withData data : Data?, dataUTI : String!) -> SignalAttachment! {
+ public class func audioAttachment(withData data: Data?, dataUTI: String!) -> SignalAttachment! {
return newAttachment(withData : data,
dataUTI : dataUTI,
validUTISet : audioUTISet(),
- maxFileSize : kMaxFileSize_Audio)
+ 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(withData data : Data?, dataUTI : String!) -> SignalAttachment! {
+ public class func genericAttachment(withData data: Data?, dataUTI: String!) -> SignalAttachment! {
return newAttachment(withData : data,
dataUTI : dataUTI,
validUTISet : nil,
- maxFileSize : kMaxFileSize_Generic)
+ maxFileSize : kMaxFileSizeGeneric)
}
-
+
// MARK: Helper Methods
-
- private class func newAttachment(withData data : Data?,
- dataUTI : String!,
- validUTISet : Set?,
- maxFileSize : Int) -> SignalAttachment! {
+
+ private class func newAttachment(withData data: Data?,
+ dataUTI: String!,
+ validUTISet: Set?,
+ maxFileSize: Int) -> SignalAttachment! {
assert(dataUTI.characters.count > 0)
-
+
assert(data != nil)
guard let data = data else {
return nil
}
-
+
let attachment = SignalAttachment(data : data, dataUTI: dataUTI)
-
+
if validUTISet != nil {
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
}
diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings
index 3e98c54b5..5ce57cb56 100644
--- a/Signal/translations/en.lproj/Localizable.strings
+++ b/Signal/translations/en.lproj/Localizable.strings
@@ -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.";