mirror of https://github.com/oxen-io/session-ios
Sketch out message metadata view.
* Show message metadata view from conversation view. * Pull out MediaMessageView class. * Track recipient read timestamps. * Add per-recipient status to message metadata view. * Add share button to message metadata view. // FREEBIEpull/1/head
parent
3bb8f4aad5
commit
9f9ac746d1
@ -0,0 +1,364 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
||||
|
||||
let TAG = "[MediaMessageView]"
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let attachment: SignalAttachment
|
||||
|
||||
var videoPlayer: MPMoviePlayerController?
|
||||
|
||||
var audioPlayer: OWSAudioAttachmentPlayer?
|
||||
var audioStatusLabel: UILabel?
|
||||
var audioPlayButton: UIButton?
|
||||
var isAudioPlayingFlag = false
|
||||
var isAudioPaused = false
|
||||
var audioProgressSeconds: CGFloat = 0
|
||||
var audioDurationSeconds: CGFloat = 0
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"use attachment: constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
self.attachment = SignalAttachment.empty()
|
||||
super.init(coder: aDecoder)
|
||||
owsFail("\(self.TAG) invalid constructor")
|
||||
|
||||
createViews()
|
||||
}
|
||||
|
||||
required init(attachment: SignalAttachment) {
|
||||
assert(!attachment.hasError)
|
||||
self.attachment = attachment
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
createViews()
|
||||
}
|
||||
|
||||
// MARK: View Lifecycle
|
||||
|
||||
func viewWillAppear(_ animated: Bool) {
|
||||
ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(true)
|
||||
}
|
||||
|
||||
func viewWillDisappear(_ animated: Bool) {
|
||||
ViewControllerUtils.setAudioIgnoresHardwareMuteSwitch(false)
|
||||
}
|
||||
|
||||
// MARK: - Create Views
|
||||
|
||||
private func createViews() {
|
||||
self.backgroundColor = UIColor.white
|
||||
|
||||
if attachment.isAnimatedImage {
|
||||
createAnimatedPreview()
|
||||
} else if attachment.isImage {
|
||||
createImagePreview()
|
||||
} else if attachment.isVideo {
|
||||
createVideoPreview()
|
||||
} else if attachment.isAudio {
|
||||
createAudioPreview()
|
||||
} else {
|
||||
createGenericPreview()
|
||||
}
|
||||
}
|
||||
|
||||
private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView {
|
||||
assert(subviews.count > 0)
|
||||
|
||||
let stackView = UIView()
|
||||
|
||||
var lastView: UIView?
|
||||
for subview in subviews {
|
||||
|
||||
stackView.addSubview(subview)
|
||||
subview.autoHCenterInSuperview()
|
||||
|
||||
if lastView == nil {
|
||||
subview.autoPinEdge(toSuperviewEdge:.top)
|
||||
} else {
|
||||
subview.autoPinEdge(.top, to:.bottom, of:lastView!, withOffset:10)
|
||||
}
|
||||
|
||||
lastView = subview
|
||||
}
|
||||
|
||||
lastView?.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
|
||||
return stackView
|
||||
}
|
||||
|
||||
private func createAudioPreview() {
|
||||
guard let dataUrl = attachment.dataUrl else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
|
||||
audioPlayer = OWSAudioAttachmentPlayer(mediaUrl: dataUrl, delegate: self)
|
||||
|
||||
var subviews = [UIView]()
|
||||
|
||||
let audioPlayButton = UIButton()
|
||||
self.audioPlayButton = audioPlayButton
|
||||
setAudioIconToPlay()
|
||||
audioPlayButton.imageView?.layer.minificationFilter = kCAFilterTrilinear
|
||||
audioPlayButton.imageView?.layer.magnificationFilter = kCAFilterTrilinear
|
||||
audioPlayButton.addTarget(self, action:#selector(audioPlayButtonPressed), for:.touchUpInside)
|
||||
let buttonSize = createHeroViewSize()
|
||||
audioPlayButton.autoSetDimension(.width, toSize:buttonSize)
|
||||
audioPlayButton.autoSetDimension(.height, toSize:buttonSize)
|
||||
subviews.append(audioPlayButton)
|
||||
|
||||
let fileNameLabel = createFileNameLabel()
|
||||
if let fileNameLabel = fileNameLabel {
|
||||
subviews.append(fileNameLabel)
|
||||
}
|
||||
|
||||
let fileSizeLabel = createFileSizeLabel()
|
||||
subviews.append(fileSizeLabel)
|
||||
|
||||
let audioStatusLabel = createAudioStatusLabel()
|
||||
self.audioStatusLabel = audioStatusLabel
|
||||
updateAudioStatusLabel()
|
||||
subviews.append(audioStatusLabel)
|
||||
|
||||
let stackView = wrapViewsInVerticalStack(subviews:subviews)
|
||||
self.addSubview(stackView)
|
||||
fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
|
||||
stackView.autoPinWidthToSuperview()
|
||||
stackView.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
private func createAnimatedPreview() {
|
||||
guard attachment.isValidImage else {
|
||||
return
|
||||
}
|
||||
let data = attachment.data
|
||||
// Use Flipboard FLAnimatedImage library to display gifs
|
||||
guard let animatedImage = FLAnimatedImage(gifData:data) else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
let animatedImageView = FLAnimatedImageView()
|
||||
animatedImageView.animatedImage = animatedImage
|
||||
animatedImageView.contentMode = .scaleAspectFit
|
||||
self.addSubview(animatedImageView)
|
||||
animatedImageView.autoPinWidthToSuperview()
|
||||
animatedImageView.autoPinHeightToSuperview()
|
||||
}
|
||||
|
||||
private func createImagePreview() {
|
||||
var image = attachment.image
|
||||
if image == nil {
|
||||
image = UIImage(data:attachment.data)
|
||||
}
|
||||
guard image != nil else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
|
||||
let imageView = UIImageView(image:image)
|
||||
imageView.layer.minificationFilter = kCAFilterTrilinear
|
||||
imageView.layer.magnificationFilter = kCAFilterTrilinear
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
self.addSubview(imageView)
|
||||
imageView.autoPinWidthToSuperview()
|
||||
imageView.autoPinHeightToSuperview()
|
||||
}
|
||||
|
||||
private func createVideoPreview() {
|
||||
guard let dataUrl = attachment.dataUrl else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
guard let videoPlayer = MPMoviePlayerController(contentURL:dataUrl) else {
|
||||
createGenericPreview()
|
||||
return
|
||||
}
|
||||
videoPlayer.prepareToPlay()
|
||||
|
||||
videoPlayer.controlStyle = .default
|
||||
videoPlayer.shouldAutoplay = false
|
||||
|
||||
self.addSubview(videoPlayer.view)
|
||||
self.videoPlayer = videoPlayer
|
||||
videoPlayer.view.autoPinWidthToSuperview()
|
||||
videoPlayer.view.autoPinHeightToSuperview()
|
||||
}
|
||||
|
||||
private func createGenericPreview() {
|
||||
var subviews = [UIView]()
|
||||
|
||||
let imageView = createHeroImageView(imageName: "file-thin-black-filled-large")
|
||||
subviews.append(imageView)
|
||||
|
||||
let fileNameLabel = createFileNameLabel()
|
||||
if let fileNameLabel = fileNameLabel {
|
||||
subviews.append(fileNameLabel)
|
||||
}
|
||||
|
||||
let fileSizeLabel = createFileSizeLabel()
|
||||
subviews.append(fileSizeLabel)
|
||||
|
||||
let stackView = wrapViewsInVerticalStack(subviews:subviews)
|
||||
self.addSubview(stackView)
|
||||
fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
|
||||
stackView.autoPinWidthToSuperview()
|
||||
stackView.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
private func createHeroViewSize() -> CGFloat {
|
||||
return ScaleFromIPhone5To7Plus(175, 225)
|
||||
}
|
||||
|
||||
private func createHeroImageView(imageName: String) -> UIView {
|
||||
let imageSize = createHeroViewSize()
|
||||
let image = UIImage(named:imageName)
|
||||
assert(image != nil)
|
||||
let imageView = UIImageView(image:image)
|
||||
imageView.layer.minificationFilter = kCAFilterTrilinear
|
||||
imageView.layer.magnificationFilter = kCAFilterTrilinear
|
||||
imageView.layer.shadowColor = UIColor.black.cgColor
|
||||
let shadowScaling = 5.0
|
||||
imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling)
|
||||
imageView.layer.shadowOpacity = 0.25
|
||||
imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling)
|
||||
imageView.autoSetDimension(.width, toSize:imageSize)
|
||||
imageView.autoSetDimension(.height, toSize:imageSize)
|
||||
|
||||
return imageView
|
||||
}
|
||||
|
||||
private func labelFont() -> UIFont {
|
||||
return UIFont.ows_regularFont(withSize:ScaleFromIPhone5To7Plus(18, 24))
|
||||
}
|
||||
|
||||
private func formattedFileExtension() -> String? {
|
||||
guard let fileExtension = attachment.fileExtension else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT",
|
||||
comment: "Format string for file extension label in call interstitial view"),
|
||||
fileExtension.uppercased())
|
||||
}
|
||||
|
||||
public func formattedFileName() -> String? {
|
||||
guard let sourceFilename = attachment.sourceFilename else {
|
||||
return nil
|
||||
}
|
||||
let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
guard filename.characters.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
private func createFileNameLabel() -> UIView? {
|
||||
let filename = formattedFileName() ?? formattedFileExtension()
|
||||
|
||||
guard filename != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let label = UILabel()
|
||||
label.text = filename
|
||||
label.textColor = UIColor.ows_materialBlue()
|
||||
label.font = labelFont()
|
||||
label.textAlignment = .center
|
||||
label.lineBreakMode = .byTruncatingMiddle
|
||||
return label
|
||||
}
|
||||
|
||||
private func createFileSizeLabel() -> UIView {
|
||||
let label = UILabel()
|
||||
let fileSize = attachment.dataLength
|
||||
label.text = String(format:NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT",
|
||||
comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}."),
|
||||
ViewControllerUtils.formatFileSize(UInt(fileSize)))
|
||||
|
||||
label.textColor = UIColor.ows_materialBlue()
|
||||
label.font = labelFont()
|
||||
label.textAlignment = .center
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
private func createAudioStatusLabel() -> UILabel {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor.ows_materialBlue()
|
||||
label.font = labelFont()
|
||||
label.textAlignment = .center
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
// MARK: - Event Handlers
|
||||
|
||||
func audioPlayButtonPressed(sender: UIButton) {
|
||||
audioPlayer?.togglePlayState()
|
||||
}
|
||||
|
||||
// MARK: - OWSAudioAttachmentPlayerDelegate
|
||||
|
||||
public func isAudioPlaying() -> Bool {
|
||||
return isAudioPlayingFlag
|
||||
}
|
||||
|
||||
public func setIsAudioPlaying(_ isAudioPlaying: Bool) {
|
||||
isAudioPlayingFlag = isAudioPlaying
|
||||
|
||||
updateAudioStatusLabel()
|
||||
}
|
||||
|
||||
public func isPaused() -> Bool {
|
||||
return isAudioPaused
|
||||
}
|
||||
|
||||
public func setIsPaused(_ isPaused: Bool) {
|
||||
isAudioPaused = isPaused
|
||||
}
|
||||
|
||||
public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
|
||||
audioProgressSeconds = progress
|
||||
audioDurationSeconds = duration
|
||||
|
||||
updateAudioStatusLabel()
|
||||
}
|
||||
|
||||
private func updateAudioStatusLabel() {
|
||||
guard let audioStatusLabel = self.audioStatusLabel else {
|
||||
owsFail("Missing audio status label")
|
||||
return
|
||||
}
|
||||
|
||||
if isAudioPlayingFlag && audioProgressSeconds > 0 && audioDurationSeconds > 0 {
|
||||
audioStatusLabel.text = String(format:"%@ / %@",
|
||||
ViewControllerUtils.formatDurationSeconds(Int(round(self.audioProgressSeconds))),
|
||||
ViewControllerUtils.formatDurationSeconds(Int(round(self.audioDurationSeconds))))
|
||||
} else {
|
||||
audioStatusLabel.text = " "
|
||||
}
|
||||
}
|
||||
|
||||
public func setAudioIconToPlay() {
|
||||
let image = UIImage(named:"audio_play_black_large")?.withRenderingMode(.alwaysTemplate)
|
||||
assert(image != nil)
|
||||
audioPlayButton?.setImage(image, for:.normal)
|
||||
audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue()
|
||||
}
|
||||
|
||||
public func setAudioIconToPause() {
|
||||
let image = UIImage(named:"audio_pause_black_large")?.withRenderingMode(.alwaysTemplate)
|
||||
assert(image != nil)
|
||||
audioPlayButton?.setImage(image, for:.normal)
|
||||
audioPlayButton?.imageView?.tintColor = UIColor.ows_materialBlue()
|
||||
}
|
||||
}
|
@ -0,0 +1,393 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class MessageMetadataViewController: OWSViewController {
|
||||
|
||||
let TAG = "[MessageMetadataViewController]"
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let message: TSMessage
|
||||
|
||||
var mediaMessageView: MediaMessageView?
|
||||
|
||||
var scrollView: UIScrollView?
|
||||
var contentView: UIView?
|
||||
|
||||
var dataSource: DataSource?
|
||||
var attachmentStream: TSAttachmentStream?
|
||||
var messageBody: String?
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"use message: constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
self.message = TSMessage()
|
||||
super.init(coder: aDecoder)
|
||||
owsFail("\(self.TAG) invalid constructor")
|
||||
}
|
||||
|
||||
required init(message: TSMessage) {
|
||||
self.message = message
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
// MARK: View Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.navigationItem.title = NSLocalizedString("MESSAGE_METADATA_VIEW_TITLE",
|
||||
comment: "Title for the 'message metadata' view.")
|
||||
|
||||
createViews()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
mediaMessageView?.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
mediaMessageView?.viewWillDisappear(animated)
|
||||
}
|
||||
|
||||
// MARK: - Create Views
|
||||
|
||||
private func createViews() {
|
||||
view.backgroundColor = UIColor.white
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
self.scrollView = scrollView
|
||||
view.addSubview(scrollView)
|
||||
scrollView.autoPinWidthToSuperview(withMargin:0)
|
||||
scrollView.autoPin(toTopLayoutGuideOf: self, withInset:0)
|
||||
|
||||
let footer = UIToolbar()
|
||||
footer.barTintColor = UIColor.ows_materialBlue()
|
||||
view.addSubview(footer)
|
||||
footer.autoPinWidthToSuperview(withMargin:0)
|
||||
footer.autoPinEdge(.top, to:.bottom, of:scrollView)
|
||||
footer.autoPin(toBottomLayoutGuideOf: self, withInset:0)
|
||||
|
||||
footer.items = [
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
|
||||
UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareButtonPressed)),
|
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
]
|
||||
|
||||
// See notes on how to use UIScrollView with iOS Auto Layout:
|
||||
//
|
||||
// https://developer.apple.com/library/content/releasenotes/General/RN-iOSSDK-6_0/
|
||||
let contentView = UIView.container()
|
||||
self.contentView = contentView
|
||||
scrollView.addSubview(contentView)
|
||||
contentView.autoPinLeadingToSuperView()
|
||||
contentView.autoPinTrailingToSuperView()
|
||||
contentView.autoPinEdge(toSuperviewEdge:.top)
|
||||
contentView.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
|
||||
var rows = [UIView]()
|
||||
|
||||
let contactsManager = Environment.getCurrent().contactsManager!
|
||||
|
||||
// Group?
|
||||
let thread = message.thread
|
||||
if let groupThread = thread as? TSGroupThread {
|
||||
var groupName = groupThread.name()
|
||||
if groupName.characters.count < 1 {
|
||||
groupName = NSLocalizedString("NEW_GROUP_DEFAULT_TITLE", comment: "")
|
||||
}
|
||||
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_GROUP_NAME",
|
||||
comment: "Label for the 'group name' field of the 'message metadata' view."),
|
||||
value:groupName))
|
||||
}
|
||||
|
||||
// Sender?
|
||||
if let incomingMessage = message as? TSIncomingMessage {
|
||||
let senderId = incomingMessage.authorId
|
||||
let senderName = contactsManager.contactOrProfileName(forPhoneIdentifier:senderId)
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENDER",
|
||||
comment: "Label for the 'sender' field of the 'message metadata' view."),
|
||||
value:senderName))
|
||||
}
|
||||
|
||||
// Recipient(s)
|
||||
if let outgoingMessage = message as? TSOutgoingMessage {
|
||||
for recipientId in thread.recipientIdentifiers {
|
||||
let recipientName = contactsManager.contactOrProfileName(forPhoneIdentifier:recipientId)
|
||||
let recipientStatus = self.recipientStatus(forOutgoingMessage: outgoingMessage, recipientId: recipientId)
|
||||
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_RECIPIENT",
|
||||
comment: "Label for the 'recipient' field of the 'message metadata' view."),
|
||||
value:recipientName,
|
||||
subtitle:recipientStatus))
|
||||
}
|
||||
}
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .long
|
||||
|
||||
let sentDate = NSDate.ows_date(withMillisecondsSince1970:message.timestamp)
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENT_DATE_TIME",
|
||||
comment: "Label for the 'sent date & time' field of the 'message metadata' view."),
|
||||
value:dateFormatter.string(from:sentDate)))
|
||||
|
||||
if let _ = message as? TSIncomingMessage {
|
||||
let receivedDate = message.dateForSorting()
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_RECEIVED_DATE_TIME",
|
||||
comment: "Label for the 'received date & time' field of the 'message metadata' view."),
|
||||
value:dateFormatter.string(from:receivedDate)))
|
||||
}
|
||||
|
||||
// TODO: We could include the "disappearing messages" state here.
|
||||
|
||||
if message.attachmentIds.count > 0 {
|
||||
rows += addAttachmentRows()
|
||||
} else if let messageBody = message.body {
|
||||
// TODO: We should also display "oversize text messages" in a
|
||||
// similar way.
|
||||
if messageBody.characters.count > 0 {
|
||||
self.messageBody = messageBody
|
||||
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_BODY_LABEL",
|
||||
comment: "Label for the message body in the 'message metadata' view."),
|
||||
value:""))
|
||||
|
||||
let bodyLabel = UILabel()
|
||||
bodyLabel.textColor = UIColor.black
|
||||
bodyLabel.font = UIFont.ows_regularFont(withSize:14)
|
||||
bodyLabel.text = messageBody
|
||||
bodyLabel.numberOfLines = 0
|
||||
bodyLabel.lineBreakMode = .byWordWrapping
|
||||
rows.append(bodyLabel)
|
||||
} else {
|
||||
// Neither attachment nor body.
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_NO_ATTACHMENT_OR_BODY",
|
||||
comment: "Label for messages without a body or attachment in the 'message metadata' view."),
|
||||
value:""))
|
||||
}
|
||||
}
|
||||
|
||||
var lastRow: UIView?
|
||||
for row in rows {
|
||||
contentView.addSubview(row)
|
||||
row.autoPinLeadingToSuperView()
|
||||
row.autoPinTrailingToSuperView()
|
||||
|
||||
if let lastRow = lastRow {
|
||||
row.autoPinEdge(.top, to:.bottom, of:lastRow, withOffset:5)
|
||||
} else {
|
||||
row.autoPinEdge(toSuperviewEdge:.top, withInset:20)
|
||||
}
|
||||
|
||||
lastRow = row
|
||||
}
|
||||
if let lastRow = lastRow {
|
||||
lastRow.autoPinEdge(toSuperviewEdge:.bottom, withInset:20)
|
||||
}
|
||||
|
||||
if let mediaMessageView = mediaMessageView {
|
||||
mediaMessageView.autoPinToSquareAspectRatio()
|
||||
}
|
||||
|
||||
// TODO: We might want to add a footer with share/save/copy/etc.
|
||||
}
|
||||
|
||||
private func addAttachmentRows() -> [UIView] {
|
||||
var rows = [UIView]()
|
||||
|
||||
guard let attachmentId = message.attachmentIds[0] as? String else {
|
||||
owsFail("Invalid attachment")
|
||||
return rows
|
||||
}
|
||||
|
||||
guard let attachment = TSAttachment.fetch(uniqueId:attachmentId) else {
|
||||
owsFail("Missing attachment")
|
||||
return rows
|
||||
}
|
||||
|
||||
let contentType = attachment.contentType
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_MIME_TYPE",
|
||||
comment: "Label for the MIME type of attachments in the 'message metadata' view."),
|
||||
value:contentType))
|
||||
|
||||
if let sourceFilename = attachment.sourceFilename {
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SOURCE_FILENAME",
|
||||
comment: "Label for the original filename of any attachment in the 'message metadata' view."),
|
||||
value:sourceFilename))
|
||||
}
|
||||
|
||||
guard let attachmentStream = attachment as? TSAttachmentStream else {
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_NOT_YET_DOWNLOADED",
|
||||
comment: "Label for 'not yet downloaded' attachments in the 'message metadata' view."),
|
||||
value:""))
|
||||
return rows
|
||||
}
|
||||
self.attachmentStream = attachmentStream
|
||||
|
||||
if let filePath = attachmentStream.filePath() {
|
||||
dataSource = DataSourcePath.dataSource(withFilePath:filePath)
|
||||
}
|
||||
|
||||
guard let dataSource = dataSource else {
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_MISSING_FILE",
|
||||
comment: "Label for 'missing' attachments in the 'message metadata' view."),
|
||||
value:""))
|
||||
return rows
|
||||
}
|
||||
|
||||
let fileSize = dataSource.dataLength()
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_FILE_SIZE",
|
||||
comment: "Label for file size of attachments in the 'message metadata' view."),
|
||||
value:ViewControllerUtils.formatFileSize(UInt(fileSize))))
|
||||
|
||||
if let dataUTI = MIMETypeUtil.utiType(forMIMEType:contentType) {
|
||||
if attachment.isVoiceMessage() {
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_VOICE_MESSAGE",
|
||||
comment: "Label for voice messages of the 'message metadata' view."),
|
||||
value:""))
|
||||
} else {
|
||||
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_MEDIA",
|
||||
comment: "Label for media messages of the 'message metadata' view."),
|
||||
value:""))
|
||||
}
|
||||
let attachment = SignalAttachment(dataSource : dataSource, dataUTI: dataUTI)
|
||||
let mediaMessageView = MediaMessageView(attachment:attachment)
|
||||
self.mediaMessageView = mediaMessageView
|
||||
rows.append(mediaMessageView)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
private func recipientStatus(forOutgoingMessage message: TSOutgoingMessage, recipientId: String) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .long
|
||||
|
||||
let recipientReadMap = message.recipientReadMap
|
||||
if let readTimestamp = recipientReadMap[recipientId] {
|
||||
assert(message.messageState == .sentToService)
|
||||
let readDate = NSDate.ows_date(withMillisecondsSince1970:readTimestamp.uint64Value)
|
||||
return String(format:NSLocalizedString("MESSAGE_STATUS_READ_WITH_TIMESTAMP_FORMAT",
|
||||
comment: "message status for messages read by the recipient. Embeds: {{the date and time the message was read}}."),
|
||||
dateFormatter.string(from:readDate))
|
||||
}
|
||||
|
||||
// TODO: We don't currently track delivery state on a per-recipient basis.
|
||||
// We should.
|
||||
if message.wasDelivered {
|
||||
return NSLocalizedString("MESSAGE_STATUS_DELIVERED",
|
||||
comment:"message status for message delivered to their recipient.")
|
||||
}
|
||||
|
||||
if message.messageState == .unsent {
|
||||
return NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages")
|
||||
} else if (message.messageState == .sentToService ||
|
||||
message.wasSent(toRecipient:recipientId)) {
|
||||
return
|
||||
NSLocalizedString("MESSAGE_STATUS_SENT",
|
||||
comment:"message footer for sent messages")
|
||||
} else if message.hasAttachments() {
|
||||
return NSLocalizedString("MESSAGE_STATUS_UPLOADING",
|
||||
comment:"message footer while attachment is uploading")
|
||||
} else {
|
||||
assert(message.messageState == .attemptingOut)
|
||||
|
||||
return NSLocalizedString("MESSAGE_STATUS_SENDING",
|
||||
comment:"message status while message is sending.")
|
||||
}
|
||||
}
|
||||
|
||||
private func nameLabel(text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor.black
|
||||
label.font = UIFont.ows_mediumFont(withSize:14)
|
||||
label.text = text
|
||||
label.setContentHuggingHorizontalHigh()
|
||||
return label
|
||||
}
|
||||
|
||||
private func valueLabel(text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor.black
|
||||
label.font = UIFont.ows_regularFont(withSize:14)
|
||||
label.text = text
|
||||
label.setContentHuggingHorizontalLow()
|
||||
return label
|
||||
}
|
||||
|
||||
private func valueRow(name: String, value: String, subtitle: String = "") -> UIView {
|
||||
let row = UIView.container()
|
||||
let nameLabel = self.nameLabel(text:name)
|
||||
let valueLabel = self.valueLabel(text:value)
|
||||
row.addSubview(nameLabel)
|
||||
row.addSubview(valueLabel)
|
||||
nameLabel.autoPinLeadingToSuperView()
|
||||
valueLabel.autoPinTrailingToSuperView()
|
||||
valueLabel.autoPinLeading(toTrailingOf:nameLabel, margin: 10)
|
||||
nameLabel.autoPinEdge(toSuperviewEdge:.top)
|
||||
valueLabel.autoPinEdge(toSuperviewEdge:.top)
|
||||
|
||||
if subtitle.characters.count > 0 {
|
||||
let subtitleLabel = self.valueLabel(text:subtitle)
|
||||
subtitleLabel.textColor = UIColor.ows_darkGray()
|
||||
row.addSubview(subtitleLabel)
|
||||
subtitleLabel.autoPinTrailingToSuperView()
|
||||
subtitleLabel.autoPinLeading(toTrailingOf:nameLabel, margin: 10)
|
||||
subtitleLabel.autoPinEdge(.top, to:.bottom, of:valueLabel, withOffset:1)
|
||||
subtitleLabel.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
} else if value.characters.count > 0 {
|
||||
valueLabel.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
} else {
|
||||
nameLabel.autoPinEdge(toSuperviewEdge:.bottom)
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func shareButtonPressed() {
|
||||
if let messageBody = messageBody {
|
||||
UIPasteboard.general.string = messageBody
|
||||
return
|
||||
}
|
||||
|
||||
guard let attachmentStream = attachmentStream else {
|
||||
Logger.error("\(TAG) Message has neither attachment nor message body.")
|
||||
return
|
||||
}
|
||||
AttachmentSharing.showShareUI(forAttachment:attachmentStream)
|
||||
}
|
||||
|
||||
func copyToPasteboard() {
|
||||
if let messageBody = messageBody {
|
||||
UIPasteboard.general.string = messageBody
|
||||
return
|
||||
}
|
||||
|
||||
guard let attachmentStream = attachmentStream else {
|
||||
Logger.error("\(TAG) Message has neither attachment nor message body.")
|
||||
return
|
||||
}
|
||||
guard let utiType = MIMETypeUtil.utiType(forMIMEType:attachmentStream.contentType) else {
|
||||
Logger.error("\(TAG) Attachment has invalid MIME type: \(attachmentStream.contentType).")
|
||||
return
|
||||
}
|
||||
guard let dataSource = dataSource else {
|
||||
Logger.error("\(TAG) Attachment missing data source.")
|
||||
return
|
||||
}
|
||||
let data = dataSource.data()
|
||||
UIPasteboard.general.setData(data, forPasteboardType:utiType)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSDate (millisecondTimeStamp)
|
||||
|
||||
+ (uint64_t)ows_millisecondTimeStamp;
|
||||
+ (NSDate *)ows_dateWithMillisecondsSince1970:(uint64_t)milliseconds;
|
||||
+ (uint64_t)ows_millisecondsSince1970ForDate:(NSDate *)date;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
Loading…
Reference in New Issue