//
// C o p y r i g h t ( c ) 2 0 1 9 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import SignalUtilitiesKit
import SignalUtilitiesKit
@objc
enum MessageMetadataViewMode : UInt {
case focusOnMessage
case focusOnMetadata
}
@objc
protocol MessageDetailViewDelegate : AnyObject {
func detailViewMessageWasDeleted ( _ messageDetailViewController : MessageDetailViewController )
}
@objc
class MessageDetailViewController : OWSViewController , MediaGalleryDataSourceDelegate , OWSMessageBubbleViewDelegate {
@objc
weak var delegate : MessageDetailViewDelegate ?
// MARK: P r o p e r t i e s
let uiDatabaseConnection : YapDatabaseConnection
var bubbleView : UIView ?
let mode : MessageMetadataViewMode
let viewItem : ConversationViewItem
var message : TSMessage
var wasDeleted : Bool = false
var messageBubbleView : OWSMessageBubbleView ?
var messageBubbleViewWidthLayoutConstraint : NSLayoutConstraint ?
var messageBubbleViewHeightLayoutConstraint : NSLayoutConstraint ?
var scrollView : UIScrollView !
var contentView : UIView ?
var attachment : TSAttachment ?
var dataSource : DataSource ?
var attachmentStream : TSAttachmentStream ?
var messageBody : String ?
lazy var shouldShowUD : Bool = {
return self . preferences . shouldShowUnidentifiedDeliveryIndicators ( )
} ( )
var conversationStyle : ConversationStyle
// MARK: D e p e n d e n c i e s
var preferences : OWSPreferences {
return Environment . shared . preferences
}
// MARK: I n i t i a l i z e r s
@ available ( * , unavailable , message : " use other constructor instead. " )
required init ? ( coder aDecoder : NSCoder ) {
notImplemented ( )
}
@objc
required init ( viewItem : ConversationViewItem , message : TSMessage , thread : TSThread , mode : MessageMetadataViewMode ) {
self . viewItem = viewItem
self . message = message
self . mode = mode
self . uiDatabaseConnection = OWSPrimaryStorage . shared ( ) . uiDatabaseConnection
self . conversationStyle = ConversationStyle ( thread : thread )
super . init ( nibName : nil , bundle : nil )
}
// MARK: V i e w L i f e c y c l e
override func viewDidLoad ( ) {
super . viewDidLoad ( )
do {
try updateMessageToLatest ( )
} catch DetailViewError . messageWasDeleted {
self . delegate ? . detailViewMessageWasDeleted ( self )
} catch {
owsFailDebug ( " unexpected error " )
}
self . conversationStyle . viewWidth = view . width ( )
ViewControllerUtilities . setUpDefaultSessionStyle ( for : self , title : NSLocalizedString ( " MESSAGE_METADATA_VIEW_TITLE " , comment : " Title for the 'message metadata' view. " ) , hasCustomBackButton : false )
createViews ( )
self . view . layoutIfNeeded ( )
NotificationCenter . default . addObserver ( self ,
selector : #selector ( uiDatabaseDidUpdate ) ,
name : . OWSUIDatabaseConnectionDidUpdate ,
object : OWSPrimaryStorage . shared ( ) . dbNotificationObject )
}
override public func viewWillTransition ( to size : CGSize , with coordinator : UIViewControllerTransitionCoordinator ) {
Logger . debug ( " " )
super . viewWillTransition ( to : size , with : coordinator )
self . conversationStyle . viewWidth = size . width
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
updateMessageBubbleViewLayout ( )
if mode = = . focusOnMetadata {
if let bubbleView = self . bubbleView {
// F o r c e l a y o u t .
view . setNeedsLayout ( )
view . layoutIfNeeded ( )
let contentHeight = scrollView . contentSize . height
let scrollViewHeight = scrollView . frame . size . height
guard contentHeight >= scrollViewHeight else {
// A l l c o n t e n t i s v i s i b l e w i t h i n t h e s c r o l l v i e w . N o n e e d t o o f f s e t .
return
}
// W e w a n t t o i n c l u d e a t l e a s t a l i t t l e p o r t i o n o f t h e m e s s a g e , b u t s c r o l l n o f a r t h e r t h a n n e c e s s a r y .
let showAtLeast : CGFloat = 50
let bubbleViewBottom = bubbleView . superview ! . convert ( bubbleView . frame , to : scrollView ) . maxY
let maxOffset = bubbleViewBottom - showAtLeast
let lastPage = contentHeight - scrollViewHeight
let offset = CGPoint ( x : 0 , y : min ( maxOffset , lastPage ) )
scrollView . setContentOffset ( offset , animated : false )
}
}
}
// MARK: - C r e a t e V i e w s
private func createViews ( ) {
view . backgroundColor = . clear
let scrollView = UIScrollView ( )
self . scrollView = scrollView
view . addSubview ( scrollView )
scrollView . autoPinWidthToSuperview ( withMargin : 0 )
if scrollView . applyInsetsFix ( ) {
scrollView . autoPinEdge ( . top , to : . top , of : view )
} else {
scrollView . autoPinEdge ( toSuperviewEdge : . top )
}
let contentView = UIView . container ( )
self . contentView = contentView
scrollView . addSubview ( contentView )
contentView . autoPinLeadingToSuperviewMargin ( )
contentView . autoPinTrailingToSuperviewMargin ( )
contentView . autoPinEdge ( toSuperviewEdge : . top )
contentView . autoPinEdge ( toSuperviewEdge : . bottom )
scrollView . layoutMargins = UIEdgeInsets ( top : 0 , left : 0 , bottom : 0 , right : 0 )
scrollView . contentInset = UIEdgeInsets ( top : 20 , left : 0 , bottom : 20 , right : 0 )
if hasMediaAttachment {
let footer = UIToolbar ( )
view . addSubview ( footer )
footer . autoPinWidthToSuperview ( withMargin : 0 )
footer . autoPinEdge ( . top , to : . bottom , of : scrollView )
footer . autoPinEdge ( . bottom , to : . bottom , of : view )
footer . items = [
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil ) ,
UIBarButtonItem ( barButtonSystemItem : . action , target : self , action : #selector ( shareButtonPressed ) ) ,
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil )
]
} else {
scrollView . autoPinEdge ( toSuperviewEdge : . bottom )
}
updateContent ( )
}
lazy var thread : TSThread = {
var thread : TSThread ?
self . uiDatabaseConnection . read { transaction in
thread = self . message . thread ( with : transaction )
}
return thread !
} ( )
private func updateContent ( ) {
guard let contentView = contentView else {
owsFailDebug ( " Missing contentView " )
return
}
// R e m o v e a n y e x i s t i n g c o n t e n t v i e w s .
for subview in contentView . subviews {
subview . removeFromSuperview ( )
}
var rows = [ UIView ] ( )
// C o n t e n t
rows += contentRows ( )
// S e n d e r ?
if let incomingMessage = message as ? TSIncomingMessage {
let senderId = incomingMessage . authorId
let threadID = thread . uniqueId !
var senderName : String !
Storage . writeSync { transaction in
senderName = DisplayNameUtilities2 . getDisplayName ( for : senderId , inThreadWithID : threadID , using : transaction )
}
rows . append ( valueRow ( name : NSLocalizedString ( " MESSAGE_METADATA_VIEW_SENDER " ,
comment : " Label for the 'sender' field of the 'message metadata' view. " ) ,
value : senderName ) )
}
// R e c i p i e n t ( s )
if let outgoingMessage = message as ? TSOutgoingMessage {
func getSeparator ( ) -> UIView {
let result = UIView ( )
result . set ( . height , to : Values . separatorThickness )
result . backgroundColor = Colors . separator
return result
}
if ! outgoingMessage . recipientIds ( ) . isEmpty {
rows += [ getSeparator ( ) ]
}
rows += outgoingMessage . recipientIds ( ) . flatMap { publicKey -> [ UIView ] in
// W e u s e C o n t a c t C e l l V i e w , n o t C o n t a c t T a b l e V i e w C e l l .
// T a b l e v i e w c e l l s d o n ' t l a y o u t p r o p e r l y o u t s i d e t h e
// c o n t e x t o f a t a b l e v i e w .
let cellView = ContactCellView ( )
cellView . configure ( withRecipientId : publicKey )
let wrapper = UIView ( )
wrapper . layoutMargins = UIEdgeInsets ( top : 8 , left : 20 , bottom : 8 , right : 20 )
wrapper . addSubview ( cellView )
cellView . autoPinEdgesToSuperviewMargins ( )
return [ wrapper , getSeparator ( ) ]
}
if ! outgoingMessage . recipientIds ( ) . isEmpty {
rows += [ UIView . vSpacer ( 10 ) ]
}
}
let sentText = DateUtil . formatPastTimestampRelativeToNow ( message . timestamp )
let sentRow : UIStackView = valueRow ( name : NSLocalizedString ( " MESSAGE_METADATA_VIEW_SENT_DATE_TIME " ,
comment : " Label for the 'sent date & time' field of the 'message metadata' view. " ) ,
value : sentText )
if let incomingMessage = message as ? TSIncomingMessage {
if self . shouldShowUD , incomingMessage . wasReceivedByUD {
let icon = # imageLiteral ( resourceName : " ic_secret_sender_indicator " ) . withRenderingMode ( . alwaysTemplate )
let iconView = UIImageView ( image : icon )
iconView . tintColor = Theme . secondaryColor
iconView . setContentHuggingHigh ( )
sentRow . addArrangedSubview ( iconView )
// k e e p t h e i c o n c l o s e t o t h e l a b e l .
let spacerView = UIView ( )
spacerView . setContentHuggingLow ( )
sentRow . addArrangedSubview ( spacerView )
}
}
sentRow . isUserInteractionEnabled = true
sentRow . addGestureRecognizer ( UILongPressGestureRecognizer ( target : self , action : #selector ( didLongPressSent ) ) )
rows . append ( sentRow )
if message is TSIncomingMessage {
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 : DateUtil . formatPastTimestampRelativeToNow ( message . receivedAtTimestamp ) ) )
}
rows += addAttachmentMetadataRows ( )
// TODO: W e c o u l d i n c l u d e t h e " d i s a p p e a r i n g m e s s a g e s " s t a t e h e r e .
let rowStack = UIStackView ( arrangedSubviews : rows )
rowStack . axis = . vertical
rowStack . spacing = 5
contentView . addSubview ( rowStack )
rowStack . autoPinEdgesToSuperviewMargins ( )
contentView . layoutIfNeeded ( )
updateMessageBubbleViewLayout ( )
}
private func displayableTextIfText ( ) -> String ? {
guard viewItem . hasBodyText else {
return nil
}
guard let displayableText = viewItem . displayableBodyText else {
return nil
}
let messageBody = displayableText . fullText
guard messageBody . count > 0 else {
return nil
}
return messageBody
}
let bubbleViewHMargin : CGFloat = 10
private func contentRows ( ) -> [ UIView ] {
var rows = [ UIView ] ( )
let messageBubbleView = OWSMessageBubbleView ( frame : CGRect . zero )
messageBubbleView . delegate = self
messageBubbleView . addTapGestureHandler ( )
self . messageBubbleView = messageBubbleView
messageBubbleView . viewItem = viewItem
messageBubbleView . cellMediaCache = NSCache ( )
messageBubbleView . conversationStyle = conversationStyle
messageBubbleView . configureViews ( )
messageBubbleView . loadContent ( )
assert ( messageBubbleView . isUserInteractionEnabled )
let row = UIView ( )
row . addSubview ( messageBubbleView )
messageBubbleView . autoPinHeightToSuperview ( )
let isIncoming = self . message as ? TSIncomingMessage != nil
messageBubbleView . autoPinEdge ( toSuperviewEdge : isIncoming ? . leading : . trailing , withInset : bubbleViewHMargin )
self . messageBubbleViewWidthLayoutConstraint = messageBubbleView . autoSetDimension ( . width , toSize : 0 )
self . messageBubbleViewHeightLayoutConstraint = messageBubbleView . autoSetDimension ( . height , toSize : 0 )
rows . append ( row )
if rows . isEmpty {
// N e i t h e r a t t a c h m e n t n o r b o d y .
owsFailDebug ( " Message has 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 : " " ) )
}
let spacer = UIView ( )
spacer . autoSetDimension ( . height , toSize : 15 )
rows . append ( spacer )
return rows
}
private func fetchAttachment ( transaction : YapDatabaseReadTransaction ) -> TSAttachment ? {
// TODO: S u p p o r t m u l t i - i m a g e m e s s a g e s .
guard let attachmentId = message . attachmentIds . firstObject as ? String else {
return nil
}
guard let attachment = TSAttachment . fetch ( uniqueId : attachmentId , transaction : transaction ) else {
Logger . warn ( " Missing attachment. Was it deleted? " )
return nil
}
return attachment
}
var hasMediaAttachment : Bool {
guard let attachment = self . attachment else {
return false
}
guard attachment . contentType != OWSMimeTypeOversizeTextMessage else {
// t o t h e u s e r , o v e r s i z e d t e x t a t t a c h m e n t s s h o u l d b e h a v e
// j u s t l i k e r e g u l a r t e x t m e s s a g e s .
return false
}
return true
}
private func addAttachmentMetadataRows ( ) -> [ UIView ] {
guard hasMediaAttachment else {
return [ ]
}
var rows = [ UIView ] ( )
if let attachment = self . attachment {
// O n l y s h o w M I M E t y p e s i n D E B U G b u i l d s .
if _isDebugAssertConfiguration ( ) {
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 ) )
}
}
if let dataSource = self . dataSource {
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 : OWSFormat . formatFileSize ( UInt ( fileSize ) ) ) )
}
return rows
}
private func buildUDAccessoryView ( text : String ) -> UIView {
let label = UILabel ( )
label . textColor = Theme . secondaryColor
label . text = text
label . textAlignment = . right
label . font = UIFont . ows_mediumFont ( withSize : 13 )
let image = # imageLiteral ( resourceName : " ic_secret_sender_indicator " ) . withRenderingMode ( . alwaysTemplate )
let imageView = UIImageView ( image : image )
imageView . tintColor = Theme . middleGrayColor
let hStack = UIStackView ( arrangedSubviews : [ imageView , label ] )
hStack . axis = . horizontal
hStack . spacing = 8
return hStack
}
private func nameLabel ( text : String ) -> UILabel {
let label = UILabel ( )
label . textColor = Theme . primaryColor
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 = Theme . primaryColor
label . font = UIFont . ows_regularFont ( withSize : 14 )
label . text = text
label . setContentHuggingHorizontalLow ( )
return label
}
private func valueRow ( name : String , value : String , subtitle : String = " " ) -> UIStackView {
let nameLabel = self . nameLabel ( text : name )
let valueLabel = self . valueLabel ( text : value )
let hStackView = UIStackView ( arrangedSubviews : [ nameLabel , valueLabel ] )
hStackView . axis = . horizontal
hStackView . spacing = 10
hStackView . layoutMargins = UIEdgeInsets ( top : 0 , left : 20 , bottom : 0 , right : 20 )
hStackView . isLayoutMarginsRelativeArrangement = true
if subtitle . count > 0 {
let subtitleLabel = self . valueLabel ( text : subtitle )
subtitleLabel . textColor = Theme . secondaryColor
hStackView . addArrangedSubview ( subtitleLabel )
}
return hStackView
}
// MARK: - A c t i o n s
@objc func shareButtonPressed ( ) {
guard let attachmentStream = attachmentStream else {
Logger . error ( " Share button should only be shown with attachment, but no attachment found. " )
return
}
AttachmentSharing . showShareUI ( forAttachment : attachmentStream )
}
// MARK: - A c t i o n s
enum DetailViewError : Error {
case messageWasDeleted
}
// T h i s m e t h o d s h o u l d b e c a l l e d a f t e r s e l f . d a t a b a s e C o n n e c t i o n . b e g i n L o n g L i v e d R e a d T r a n s a c t i o n ( ) .
private func updateMessageToLatest ( ) throws {
AssertIsOnMainThread ( )
try self . uiDatabaseConnection . read { transaction in
guard let uniqueId = self . message . uniqueId else {
Logger . error ( " Message is missing uniqueId. " )
return
}
guard let newMessage = TSInteraction . fetch ( uniqueId : uniqueId , transaction : transaction ) as ? TSMessage else {
Logger . error ( " Message was deleted " )
throw DetailViewError . messageWasDeleted
}
self . message = newMessage
self . attachment = self . fetchAttachment ( transaction : transaction )
self . attachmentStream = self . attachment as ? TSAttachmentStream
}
}
@objc internal func uiDatabaseDidUpdate ( notification : NSNotification ) {
AssertIsOnMainThread ( )
guard ! wasDeleted else {
// I t e m w a s d e l e t e d i n t h e t i l e v i e w g a l l e r y .
// D o n ' t b o t h e r r e - r e n d e r i n g , i t w i l l f a i l a n d w e ' l l s o o n b e d i s m i s s e d .
return
}
guard let notifications = notification . userInfo ? [ OWSUIDatabaseConnectionNotificationsKey ] as ? [ Notification ] else {
owsFailDebug ( " notifications was unexpectedly nil " )
return
}
guard let uniqueId = self . message . uniqueId else {
Logger . error ( " Message is missing uniqueId. " )
return
}
guard self . uiDatabaseConnection . hasChange ( forKey : uniqueId ,
inCollection : TSInteraction . collection ( ) ,
in : notifications ) else {
Logger . debug ( " No relevant changes. " )
return
}
do {
try updateMessageToLatest ( )
} catch DetailViewError . messageWasDeleted {
DispatchQueue . main . async {
self . delegate ? . detailViewMessageWasDeleted ( self )
}
return
} catch {
owsFailDebug ( " unexpected error: \( error ) " )
}
updateContent ( )
}
private func string ( for messageReceiptStatus : MessageReceiptStatus ) -> String {
switch messageReceiptStatus {
case . uploading :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING " ,
comment : " Status label for messages which are uploading. " )
case . sending :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SENDING " ,
comment : " Status label for messages which are sending. " )
case . sent :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SENT " ,
comment : " Status label for messages which are sent. " )
case . delivered :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_DELIVERED " ,
comment : " Status label for messages which are delivered. " )
case . read :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_READ " ,
comment : " Status label for messages which are read. " )
case . failed :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_FAILED " ,
comment : " Status label for messages which are failed. " )
case . skipped :
return NSLocalizedString ( " MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SKIPPED " ,
comment : " Status label for messages which were skipped. " )
}
}
// MARK: - M e s s a g e B u b b l e L a y o u t
private func updateMessageBubbleViewLayout ( ) {
guard let messageBubbleView = messageBubbleView else {
return
}
guard let messageBubbleViewWidthLayoutConstraint = messageBubbleViewWidthLayoutConstraint else {
return
}
guard let messageBubbleViewHeightLayoutConstraint = messageBubbleViewHeightLayoutConstraint else {
return
}
let messageBubbleSize = messageBubbleView . measureSize ( )
messageBubbleViewWidthLayoutConstraint . constant = messageBubbleSize . width
messageBubbleViewHeightLayoutConstraint . constant = messageBubbleSize . height
}
// MARK: O W S M e s s a g e B u b b l e V i e w D e l e g a t e
func didTapImageViewItem ( _ viewItem : ConversationViewItem , attachmentStream : TSAttachmentStream , imageView : UIView ) {
let mediaGallery = MediaGallery ( thread : self . thread )
mediaGallery . addDataSourceDelegate ( self )
mediaGallery . presentDetailView ( fromViewController : self , mediaAttachment : attachmentStream , replacingView : imageView )
}
func didTapVideoViewItem ( _ viewItem : ConversationViewItem , attachmentStream : TSAttachmentStream , imageView : UIView ) {
let mediaGallery = MediaGallery ( thread : self . thread )
mediaGallery . addDataSourceDelegate ( self )
mediaGallery . presentDetailView ( fromViewController : self , mediaAttachment : attachmentStream , replacingView : imageView )
}
var audioAttachmentPlayer : OWSAudioPlayer ?
func didTapAudioViewItem ( _ viewItem : ConversationViewItem , attachmentStream : TSAttachmentStream ) {
AssertIsOnMainThread ( )
guard let mediaURL = attachmentStream . originalMediaURL else {
owsFailDebug ( " mediaURL was unexpectedly nil for attachment: \( attachmentStream ) " )
return
}
guard FileManager . default . fileExists ( atPath : mediaURL . path ) else {
owsFailDebug ( " audio file missing at path: \( mediaURL ) " )
return
}
if let audioAttachmentPlayer = self . audioAttachmentPlayer {
// I s t h i s p l a y e r a s s o c i a t e d w i t h t h i s m e d i a a d a p t e r ?
if audioAttachmentPlayer . owner = = = viewItem {
// T a p t o p a u s e & u n p a u s e .
audioAttachmentPlayer . togglePlayState ( )
return
}
audioAttachmentPlayer . stop ( )
self . audioAttachmentPlayer = nil
}
let audioAttachmentPlayer = OWSAudioPlayer ( mediaUrl : mediaURL , audioBehavior : . audioMessagePlayback , delegate : viewItem )
self . audioAttachmentPlayer = audioAttachmentPlayer
// A s s o c i a t e t h e p l a y e r w i t h t h i s m e d i a a d a p t e r .
audioAttachmentPlayer . owner = viewItem
audioAttachmentPlayer . play ( )
}
func didPanAudioViewItem ( toCurrentTime currentTime : TimeInterval ) {
// TODO: I m p l e m e n t
}
func didTapTruncatedTextMessage ( _ conversationItem : ConversationViewItem ) {
guard let navigationController = self . navigationController else {
owsFailDebug ( " navigationController was unexpectedly nil " )
return
}
let viewController = LongTextViewController ( viewItem : viewItem )
viewController . delegate = self
navigationController . pushViewController ( viewController , animated : true )
}
func didTapFailedIncomingAttachment ( _ viewItem : ConversationViewItem ) {
// n o - o p
}
func didTapFailedOutgoingMessage ( _ message : TSOutgoingMessage ) {
// n o - o p
}
func didTapConversationItem ( _ viewItem : ConversationViewItem , quotedReply : OWSQuotedReplyModel ) {
// n o - o p
}
func didTapConversationItem ( _ viewItem : ConversationViewItem , quotedReply : OWSQuotedReplyModel , failedThumbnailDownloadAttachmentPointer attachmentPointer : TSAttachmentPointer ) {
// n o - o p
}
func didTapConversationItem ( _ viewItem : ConversationViewItem , linkPreview : OWSLinkPreview ) {
guard let urlString = linkPreview . urlString else {
owsFailDebug ( " Missing url. " )
return
}
guard let url = URL ( string : urlString ) else {
owsFailDebug ( " Invalid url: \( urlString ) . " )
return
}
UIApplication . shared . openURL ( url )
}
@objc func didLongPressSent ( sender : UIGestureRecognizer ) {
guard sender . state = = . began else {
return
}
let messageTimestamp = " \( message . timestamp ) "
UIPasteboard . general . string = messageTimestamp
}
var lastSearchedText : String ? {
return nil
}
// M e d i a G a l l e r y D a t a S o u r c e D e l e g a t e
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , willDelete items : [ MediaGalleryItem ] , initiatedBy : AnyObject ) {
Logger . info ( " " )
guard ( items . map ( { $0 . message } ) = = [ self . message ] ) else {
// S h o u l d o n l y b e o n e m e s s a g e w e c a n d e l e t e w h e n v i e w i n g m e s s a g e d e t a i l s
owsFailDebug ( " Unexpectedly informed of irrelevant message deletion " )
return
}
self . wasDeleted = true
}
func mediaGalleryDataSource ( _ mediaGalleryDataSource : MediaGalleryDataSource , deletedSections : IndexSet , deletedItems : [ IndexPath ] ) {
self . dismiss ( animated : true ) {
self . navigationController ? . popViewController ( animated : true )
}
}
// MARK: - C o n t a c t S h a r e V i e w H e l p e r D e l e g a t e
public func didCreateOrEditContact ( ) {
updateContent ( )
self . dismiss ( animated : true )
}
}
extension MessageDetailViewController : LongTextViewDelegate {
func longTextViewMessageWasDeleted ( _ longTextViewController : LongTextViewController ) {
self . delegate ? . detailViewMessageWasDeleted ( self )
}
}