parent
796be18c56
commit
fb408f980c
|
@ -12,28 +12,19 @@
|
|||
3400C7991EAFB772008A8584 /* ThreadViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 3400C7981EAFB772008A8584 /* ThreadViewHelper.m */; };
|
||||
340CB2241EAC155C0001CAA1 /* ContactsViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 340CB2231EAC155C0001CAA1 /* ContactsViewHelper.m */; };
|
||||
340CB2271EAC25820001CAA1 /* UpdateGroupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340CB2261EAC25820001CAA1 /* UpdateGroupViewController.m */; };
|
||||
341207271EE19F6A00463194 /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 341207261EE19F6A00463194 /* OWSSystemMessageCell.m */; };
|
||||
341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */; };
|
||||
341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */ = {isa = PBXBuildFile; fileRef = 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */; };
|
||||
342FCE6B1EF9C375002690AD /* OWS105AttachmentFilePaths.m in Sources */ = {isa = PBXBuildFile; fileRef = 342FCE6A1EF9C375002690AD /* OWS105AttachmentFilePaths.m */; };
|
||||
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
|
||||
34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; };
|
||||
34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; };
|
||||
34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; };
|
||||
34330A611E788EA900DF2FB9 /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330A601E788EA900DF2FB9 /* AttachmentUploadView.m */; };
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
|
||||
343D3D9B1E9283F100165CA4 /* BlockListUIUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */; };
|
||||
3448BFCC1EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3448BFC21EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.m */; };
|
||||
3448BFCD1EDF0EA7005B2D69 /* OWSMessagesInputToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = 3448BFC41EDF0EA7005B2D69 /* OWSMessagesInputToolbar.m */; };
|
||||
3448BFCF1EDF0EA7005B2D69 /* OWSMessagesComposerTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3448BFC81EDF0EA7005B2D69 /* OWSMessagesComposerTextView.m */; };
|
||||
3448BFD01EDF0EA7005B2D69 /* ConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3448BFCA1EDF0EA7005B2D69 /* ConversationViewController.m */; };
|
||||
3448BFD11EDF0EA7005B2D69 /* ConversationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3448BFCB1EDF0EA7005B2D69 /* ConversationViewController.xib */; };
|
||||
344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F2F661E57A932000D9322 /* UIViewController+OWS.m */; };
|
||||
34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */; };
|
||||
34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34535D811E256BE9008A4747 /* UIView+OWS.m */; };
|
||||
3453D8EA1EC0D4ED003F9E6F /* OWSAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3453D8E91EC0D4ED003F9E6F /* OWSAlerts.swift */; };
|
||||
345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671001E89A5F1006EE662 /* ThreadUtil.m */; };
|
||||
3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */; };
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
|
||||
3471B1DA1EB7C63600F6AEC8 /* NewNonContactConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3471B1D91EB7C63600F6AEC8 /* NewNonContactConversationViewController.m */; };
|
||||
3472229F1EB22FFE00E53955 /* AddToGroupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3472229E1EB22FFE00E53955 /* AddToGroupViewController.m */; };
|
||||
|
@ -63,7 +54,6 @@
|
|||
34B3F8891E8DF1700035BE1A /* OWSConversationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F85C1E8DF1700035BE1A /* OWSConversationSettingsViewController.m */; };
|
||||
34B3F88A1E8DF1700035BE1A /* OWSLinkDeviceViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F85E1E8DF1700035BE1A /* OWSLinkDeviceViewController.m */; };
|
||||
34B3F88B1E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8601E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.m */; };
|
||||
34B3F88C1E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 34B3F8611E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib */; };
|
||||
34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8631E8DF1700035BE1A /* OWSQRCodeScanningViewController.m */; };
|
||||
34B3F88E1E8DF1700035BE1A /* PrivacySettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8651E8DF1700035BE1A /* PrivacySettingsTableViewController.m */; };
|
||||
34B3F88F1E8DF1710035BE1A /* RegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8671E8DF1700035BE1A /* RegistrationViewController.m */; };
|
||||
|
@ -80,7 +70,6 @@
|
|||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */; };
|
||||
34C04D801F6195E6004308B3 /* OWSFlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C04D7F1F6195E6004308B3 /* OWSFlatButton.swift */; };
|
||||
34C42D5B1F45F7A80072EC04 /* OWSNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */; };
|
||||
34C42D611F4734CA0072EC04 /* OWSContactOffersCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D601F4734CA0072EC04 /* OWSContactOffersCell.m */; };
|
||||
34C42D661F4734ED0072EC04 /* OWSContactOffersInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D631F4734ED0072EC04 /* OWSContactOffersInteraction.m */; };
|
||||
34C42D671F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; };
|
||||
34CA1C251F706B5400E51C51 /* NSAttributedString+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CA1C241F706B5400E51C51 /* NSAttributedString+OWS.m */; };
|
||||
|
@ -93,6 +82,25 @@
|
|||
34CE88ED1F3237260098030F /* ProfileFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88EB1F3237260098030F /* ProfileFetcherJob.swift */; };
|
||||
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */; };
|
||||
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */; };
|
||||
34D1F0821F8678AA0066283D /* ConversationHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0661F8678AA0066283D /* ConversationHeaderView.m */; };
|
||||
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0681F8678AA0066283D /* ConversationInputTextView.m */; };
|
||||
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F06A1F8678AA0066283D /* ConversationInputToolbar.m */; };
|
||||
34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F06E1F8678AA0066283D /* ConversationViewController.m */; };
|
||||
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0701F8678AA0066283D /* ConversationViewItem.m */; };
|
||||
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0721F8678AA0066283D /* ConversationViewLayout.m */; };
|
||||
34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0971F867BFC0066283D /* ConversationViewCell.m */; };
|
||||
34D1F0AA1F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0991F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.m */; };
|
||||
34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */; };
|
||||
34D1F0AC1F867BFC0066283D /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */; };
|
||||
34D1F0AD1F867BFC0066283D /* OWSIncomingMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A01F867BFC0066283D /* OWSIncomingMessageCell.m */; };
|
||||
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */; };
|
||||
34D1F0AF1F867BFC0066283D /* OWSOutgoingMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A41F867BFC0066283D /* OWSOutgoingMessageCell.m */; };
|
||||
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */; };
|
||||
34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */; };
|
||||
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */; };
|
||||
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */; };
|
||||
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; };
|
||||
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; };
|
||||
34D5CC961EA6AFAD005515DB /* OWSContactsSyncing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */; };
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
|
||||
34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCB01EAE7E7F005515DB /* SelectRecipientViewController.m */; };
|
||||
|
@ -109,7 +117,6 @@
|
|||
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */; };
|
||||
34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */; };
|
||||
34E8BF381EE9E2FD00F5F4CA /* FingerprintViewScanController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E8BF371EE9E2FD00F5F4CA /* FingerprintViewScanController.m */; };
|
||||
34F3089F1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */; };
|
||||
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
|
||||
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; };
|
||||
450449391F45EE7D002D1ADA /* NSString+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 450449381F45EE7D002D1ADA /* NSString+OWS.m */; };
|
||||
|
@ -119,10 +126,6 @@
|
|||
4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; };
|
||||
4505C2C21E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */; };
|
||||
4505C2C31E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */; };
|
||||
450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; };
|
||||
450873C41D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */; };
|
||||
450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; };
|
||||
450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */; };
|
||||
4509E79A1DD653700025A59F /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4509E7991DD653700025A59F /* WebRTC.framework */; };
|
||||
450D19131F85236600970622 /* RemoteVideoView.m in Sources */ = {isa = PBXBuildFile; fileRef = 450D19121F85236600970622 /* RemoteVideoView.m */; };
|
||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2041E0D74AC003D14BE /* Platform.swift */; };
|
||||
|
@ -150,16 +153,12 @@
|
|||
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
|
||||
452C46901E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; };
|
||||
452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; };
|
||||
452EA0971EA662330078744B /* AttachmentPointerAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */; };
|
||||
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; };
|
||||
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
||||
452ECA4E1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
||||
4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */; };
|
||||
45387B041E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 45387B031E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m */; };
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
|
||||
4539B5871F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
|
||||
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
|
||||
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */; };
|
||||
4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */; };
|
||||
4542F0961EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */; };
|
||||
4542F0971EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */; };
|
||||
|
@ -243,9 +242,6 @@
|
|||
45F170BC1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; };
|
||||
45F170CC1E310E22003FC1F2 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */; };
|
||||
45F170D61E315310003FC1F2 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; };
|
||||
45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */; };
|
||||
45F2B1971D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */; };
|
||||
45F2B1981D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */; };
|
||||
45F3AEB61DFDE7900080CE33 /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F3AEB51DFDE7900080CE33 /* AvatarImageView.swift */; };
|
||||
45F3AEB71DFDE7900080CE33 /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F3AEB51DFDE7900080CE33 /* AvatarImageView.swift */; };
|
||||
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F659721E1BD99C00444429 /* CallKitCallUIAdaptee.swift */; };
|
||||
|
@ -255,7 +251,6 @@
|
|||
45FBC5C91DF8575700E9B410 /* CallKitCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */; };
|
||||
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; };
|
||||
45FBC5D21DF8592E00E9B410 /* SignalCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBC5D01DF8592E00E9B410 /* SignalCall.swift */; };
|
||||
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */; };
|
||||
56EAA22E1901718F78C6DBB4 /* libPods-SignalTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B5E7D6C9007F5E5761D79DD /* libPods-SignalTests.a */; };
|
||||
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; };
|
||||
7038632718F70C0700D4A43F /* CryptoTools.m in Sources */ = {isa = PBXBuildFile; fileRef = 7038632418F70C0700D4A43F /* CryptoTools.m */; };
|
||||
|
@ -284,7 +279,6 @@
|
|||
A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4F17A06537000A904E /* AddressBookUI.framework */; };
|
||||
A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4D17A0652C000A904E /* AddressBook.framework */; };
|
||||
A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5509EC91A69AB8B00ABA4BC /* Main.storyboard */; };
|
||||
A5E9D4BB1A65FAD800E4481C /* TSVideoAttachmentAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = A5E9D4B91A65FAD800E4481C /* TSVideoAttachmentAdapter.m */; };
|
||||
AD41D7B51A6F6F0600241130 /* play_button.png in Resources */ = {isa = PBXBuildFile; fileRef = AD41D7B31A6F6F0600241130 /* play_button.png */; };
|
||||
AD41D7B61A6F6F0600241130 /* play_button@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AD41D7B41A6F6F0600241130 /* play_button@2x.png */; };
|
||||
AD83FF3F1A73426500B5C81A /* audio_pause_button_blue.png in Resources */ = {isa = PBXBuildFile; fileRef = AD83FF381A73426500B5C81A /* audio_pause_button_blue.png */; };
|
||||
|
@ -304,7 +298,6 @@
|
|||
B60EDE041A05A01700D73516 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B60EDE031A05A01700D73516 /* AudioToolbox.framework */; };
|
||||
B6258B331C29E2E60014138E /* NotificationsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B6258B321C29E2E60014138E /* NotificationsManager.m */; };
|
||||
B625CD561ABB589C00E8B23C /* NewMessage.aifc in Resources */ = {isa = PBXBuildFile; fileRef = B625CD551ABB589C00E8B23C /* NewMessage.aifc */; };
|
||||
B62D53F71A23CCAD009AAF82 /* TSMessageAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = B62D53F61A23CCAD009AAF82 /* TSMessageAdapter.m */; };
|
||||
B633C5861A1D190B0059AC12 /* call@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C5041A1D190B0059AC12 /* call@2x.png */; };
|
||||
B633C58D1A1D190B0059AC12 /* contact_default_feed.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C50B1A1D190B0059AC12 /* contact_default_feed.png */; };
|
||||
B633C59D1A1D190B0059AC12 /* endcall@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C51B1A1D190B0059AC12 /* endcall@2x.png */; };
|
||||
|
@ -343,7 +336,6 @@
|
|||
B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B67EBF5C19194AC60084CCFD /* Settings.bundle */; };
|
||||
B68112EA1A4D9EC400BA82FF /* UIImage+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = B68112E91A4D9EC400BA82FF /* UIImage+OWS.m */; };
|
||||
B69CD25119773E79005CE69A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B69CD25019773E79005CE69A /* XCTest.framework */; };
|
||||
B6A3EB4B1A423B3800B2236B /* TSPhotoAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A3EB4A1A423B3800B2236B /* TSPhotoAdapter.m */; };
|
||||
B6B1013C196D213F007E3930 /* SignalKeyingStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B1013B196D213F007E3930 /* SignalKeyingStorage.m */; };
|
||||
B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6B226961BE4B7D200860F4D /* ContactsUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9ECFB198B31BA00C620D3 /* PushManager.m */; };
|
||||
|
@ -413,10 +405,6 @@
|
|||
340CB2231EAC155C0001CAA1 /* ContactsViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactsViewHelper.m; sourceTree = "<group>"; };
|
||||
340CB2251EAC25820001CAA1 /* UpdateGroupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UpdateGroupViewController.h; sourceTree = "<group>"; };
|
||||
340CB2261EAC25820001CAA1 /* UpdateGroupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UpdateGroupViewController.m; sourceTree = "<group>"; };
|
||||
341207251EE19F6A00463194 /* OWSSystemMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OWSSystemMessageCell.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
|
||||
341207261EE19F6A00463194 /* OWSSystemMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OWSSystemMessageCell.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
|
||||
341BB7471DB727EE001E2975 /* JSQMediaItem+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMediaItem+OWS.h"; sourceTree = "<group>"; };
|
||||
341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMediaItem+OWS.m"; sourceTree = "<group>"; };
|
||||
341F2C0D1F2B8AE700D07D6B /* DebugUIMisc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMisc.h; sourceTree = "<group>"; };
|
||||
341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMisc.m; sourceTree = "<group>"; };
|
||||
342FCE691EF9C375002690AD /* OWS105AttachmentFilePaths.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWS105AttachmentFilePaths.h; path = Migrations/OWS105AttachmentFilePaths.h; sourceTree = "<group>"; };
|
||||
|
@ -425,21 +413,10 @@
|
|||
34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = "<group>"; };
|
||||
34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dripicons-v2.ttf"; sourceTree = "<group>"; };
|
||||
34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ElegantIcons.ttf; sourceTree = "<group>"; };
|
||||
34330A5F1E788EA900DF2FB9 /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = "<group>"; };
|
||||
34330A601E788EA900DF2FB9 /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = "<group>"; };
|
||||
34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = "<group>"; };
|
||||
34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = "<group>"; };
|
||||
343D3D991E9283F100165CA4 /* BlockListUIUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlockListUIUtils.h; sourceTree = "<group>"; };
|
||||
343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlockListUIUtils.m; sourceTree = "<group>"; };
|
||||
3448BFC11EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesToolbarContentView.h; sourceTree = "<group>"; };
|
||||
3448BFC21EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesToolbarContentView.m; sourceTree = "<group>"; };
|
||||
3448BFC31EDF0EA7005B2D69 /* OWSMessagesInputToolbar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesInputToolbar.h; sourceTree = "<group>"; };
|
||||
3448BFC41EDF0EA7005B2D69 /* OWSMessagesInputToolbar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesInputToolbar.m; sourceTree = "<group>"; };
|
||||
3448BFC71EDF0EA7005B2D69 /* OWSMessagesComposerTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesComposerTextView.h; sourceTree = "<group>"; };
|
||||
3448BFC81EDF0EA7005B2D69 /* OWSMessagesComposerTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessagesComposerTextView.m; sourceTree = "<group>"; };
|
||||
3448BFC91EDF0EA7005B2D69 /* ConversationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewController.h; sourceTree = "<group>"; };
|
||||
3448BFCA1EDF0EA7005B2D69 /* ConversationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewController.m; sourceTree = "<group>"; };
|
||||
3448BFCB1EDF0EA7005B2D69 /* ConversationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ConversationViewController.xib; sourceTree = "<group>"; };
|
||||
344F2F651E57A932000D9322 /* UIViewController+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+OWS.h"; path = "util/UIViewController+OWS.h"; sourceTree = "<group>"; };
|
||||
344F2F661E57A932000D9322 /* UIViewController+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+OWS.m"; path = "util/UIViewController+OWS.m"; sourceTree = "<group>"; };
|
||||
34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioAttachmentPlayer.h; sourceTree = "<group>"; };
|
||||
|
@ -449,8 +426,6 @@
|
|||
3453D8E91EC0D4ED003F9E6F /* OWSAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAlerts.swift; sourceTree = "<group>"; };
|
||||
345670FF1E89A5F1006EE662 /* ThreadUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadUtil.h; sourceTree = "<group>"; };
|
||||
345671001E89A5F1006EE662 /* ThreadUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadUtil.m; sourceTree = "<group>"; };
|
||||
345671081E8A9F5D006EE662 /* TSGenericAttachmentAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGenericAttachmentAdapter.h; sourceTree = "<group>"; };
|
||||
345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSGenericAttachmentAdapter.m; sourceTree = "<group>"; };
|
||||
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
|
||||
3471B1D81EB7C63600F6AEC8 /* NewNonContactConversationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewNonContactConversationViewController.h; sourceTree = "<group>"; };
|
||||
3471B1D91EB7C63600F6AEC8 /* NewNonContactConversationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewNonContactConversationViewController.m; sourceTree = "<group>"; };
|
||||
|
@ -501,7 +476,6 @@
|
|||
34B3F85E1E8DF1700035BE1A /* OWSLinkDeviceViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLinkDeviceViewController.m; sourceTree = "<group>"; };
|
||||
34B3F85F1E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSLinkedDevicesTableViewController.h; sourceTree = "<group>"; };
|
||||
34B3F8601E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLinkedDevicesTableViewController.m; sourceTree = "<group>"; };
|
||||
34B3F8611E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSMessagesToolbarContentView.xib; sourceTree = "<group>"; };
|
||||
34B3F8621E8DF1700035BE1A /* OWSQRCodeScanningViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQRCodeScanningViewController.h; sourceTree = "<group>"; };
|
||||
34B3F8631E8DF1700035BE1A /* OWSQRCodeScanningViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQRCodeScanningViewController.m; sourceTree = "<group>"; };
|
||||
34B3F8641E8DF1700035BE1A /* PrivacySettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivacySettingsTableViewController.h; sourceTree = "<group>"; };
|
||||
|
@ -530,8 +504,6 @@
|
|||
34C04D7F1F6195E6004308B3 /* OWSFlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSFlatButton.swift; sourceTree = "<group>"; };
|
||||
34C42D591F45F7A80072EC04 /* OWSNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSNavigationController.h; sourceTree = "<group>"; };
|
||||
34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSNavigationController.m; sourceTree = "<group>"; };
|
||||
34C42D5F1F4734CA0072EC04 /* OWSContactOffersCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactOffersCell.h; sourceTree = "<group>"; };
|
||||
34C42D601F4734CA0072EC04 /* OWSContactOffersCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactOffersCell.m; sourceTree = "<group>"; };
|
||||
34C42D621F4734ED0072EC04 /* OWSContactOffersInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactOffersInteraction.h; sourceTree = "<group>"; };
|
||||
34C42D631F4734ED0072EC04 /* OWSContactOffersInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactOffersInteraction.m; sourceTree = "<group>"; };
|
||||
34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSUnreadIndicatorInteraction.h; sourceTree = "<group>"; };
|
||||
|
@ -551,10 +523,47 @@
|
|||
34CE88EB1F3237260098030F /* ProfileFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileFetcherJob.swift; sourceTree = "<group>"; };
|
||||
34D1F04F1F7D45A60066283D /* GifPickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerCell.swift; sourceTree = "<group>"; };
|
||||
34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyDownloader.swift; sourceTree = "<group>"; };
|
||||
34D1F0651F8678AA0066283D /* ConversationHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationHeaderView.h; sourceTree = "<group>"; };
|
||||
34D1F0661F8678AA0066283D /* ConversationHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationHeaderView.m; sourceTree = "<group>"; };
|
||||
34D1F0671F8678AA0066283D /* ConversationInputTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationInputTextView.h; sourceTree = "<group>"; };
|
||||
34D1F0681F8678AA0066283D /* ConversationInputTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationInputTextView.m; sourceTree = "<group>"; };
|
||||
34D1F0691F8678AA0066283D /* ConversationInputToolbar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationInputToolbar.h; sourceTree = "<group>"; };
|
||||
34D1F06A1F8678AA0066283D /* ConversationInputToolbar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationInputToolbar.m; sourceTree = "<group>"; };
|
||||
34D1F06D1F8678AA0066283D /* ConversationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewController.h; sourceTree = "<group>"; };
|
||||
34D1F06E1F8678AA0066283D /* ConversationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewController.m; sourceTree = "<group>"; };
|
||||
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewItem.h; sourceTree = "<group>"; };
|
||||
34D1F0701F8678AA0066283D /* ConversationViewItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewItem.m; sourceTree = "<group>"; };
|
||||
34D1F0711F8678AA0066283D /* ConversationViewLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewLayout.h; sourceTree = "<group>"; };
|
||||
34D1F0721F8678AA0066283D /* ConversationViewLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewLayout.m; sourceTree = "<group>"; };
|
||||
34D1F0961F867BFC0066283D /* ConversationViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewCell.h; sourceTree = "<group>"; };
|
||||
34D1F0971F867BFC0066283D /* ConversationViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewCell.m; sourceTree = "<group>"; };
|
||||
34D1F0981F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMessagesCollectionViewCell+OWS.h"; sourceTree = "<group>"; };
|
||||
34D1F0991F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMessagesCollectionViewCell+OWS.m"; sourceTree = "<group>"; };
|
||||
34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactOffersCell.h; sourceTree = "<group>"; };
|
||||
34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactOffersCell.m; sourceTree = "<group>"; };
|
||||
34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = "<group>"; };
|
||||
34D1F09D1F867BFC0066283D /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = "<group>"; };
|
||||
34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSExpirationTimerView.m; sourceTree = "<group>"; };
|
||||
34D1F09F1F867BFC0066283D /* OWSIncomingMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIncomingMessageCell.h; sourceTree = "<group>"; };
|
||||
34D1F0A01F867BFC0066283D /* OWSIncomingMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIncomingMessageCell.m; sourceTree = "<group>"; };
|
||||
34D1F0A11F867BFC0066283D /* OWSMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageCell.h; sourceTree = "<group>"; };
|
||||
34D1F0A21F867BFC0066283D /* OWSMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageCell.m; sourceTree = "<group>"; };
|
||||
34D1F0A31F867BFC0066283D /* OWSOutgoingMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCell.h; sourceTree = "<group>"; };
|
||||
34D1F0A41F867BFC0066283D /* OWSOutgoingMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCell.m; sourceTree = "<group>"; };
|
||||
34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSystemMessageCell.h; sourceTree = "<group>"; };
|
||||
34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSystemMessageCell.m; sourceTree = "<group>"; };
|
||||
34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicatorCell.h; sourceTree = "<group>"; };
|
||||
34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicatorCell.m; sourceTree = "<group>"; };
|
||||
34D1F0B21F86D31D0066283D /* ConversationCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationCollectionView.h; sourceTree = "<group>"; };
|
||||
34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationCollectionView.m; sourceTree = "<group>"; };
|
||||
34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGenericAttachmentView.h; sourceTree = "<group>"; };
|
||||
34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSGenericAttachmentView.m; sourceTree = "<group>"; };
|
||||
34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioMessageView.h; sourceTree = "<group>"; };
|
||||
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioMessageView.m; sourceTree = "<group>"; };
|
||||
34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = "<group>"; };
|
||||
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = "<group>"; };
|
||||
34D5CC941EA6AFAD005515DB /* OWSContactsSyncing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSyncing.h; sourceTree = "<group>"; };
|
||||
34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSyncing.m; sourceTree = "<group>"; };
|
||||
34D5CC981EA6EB79005515DB /* OWSMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageCollectionViewCell.h; sourceTree = "<group>"; };
|
||||
34D5CC9B1EA6ED17005515DB /* OWSMessageMediaAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageMediaAdapter.h; sourceTree = "<group>"; };
|
||||
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
|
||||
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = "<group>"; };
|
||||
34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsViewDelegate.h; sourceTree = "<group>"; };
|
||||
|
@ -581,8 +590,6 @@
|
|||
34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIPage.m; sourceTree = "<group>"; };
|
||||
34E8BF361EE9E2FD00F5F4CA /* FingerprintViewScanController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FingerprintViewScanController.h; sourceTree = "<group>"; };
|
||||
34E8BF371EE9E2FD00F5F4CA /* FingerprintViewScanController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FingerprintViewScanController.m; sourceTree = "<group>"; };
|
||||
34F3089D1ECA580B00BB7697 /* OWSUnreadIndicatorCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OWSUnreadIndicatorCell.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
|
||||
34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OWSUnreadIndicatorCell.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
|
||||
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = "<group>"; };
|
||||
34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = "<group>"; };
|
||||
34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = "<group>"; };
|
||||
|
@ -593,11 +600,6 @@
|
|||
450573FD1E78A06D00615BB4 /* OWS103EnableVideoCalling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWS103EnableVideoCalling.m; path = Migrations/OWS103EnableVideoCalling.m; sourceTree = "<group>"; };
|
||||
4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExperienceUpgrade.swift; path = ExperienceUpgrades/ExperienceUpgrade.swift; sourceTree = "<group>"; };
|
||||
4505C2C11E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeFinder.swift; sourceTree = "<group>"; };
|
||||
450873C11D9D5149006B54F2 /* OWSExpirationTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirationTimerView.h; sourceTree = "<group>"; };
|
||||
450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSExpirationTimerView.m; sourceTree = "<group>"; };
|
||||
450873C51D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIncomingMessageCollectionViewCell.h; sourceTree = "<group>"; };
|
||||
450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIncomingMessageCollectionViewCell.m; sourceTree = "<group>"; };
|
||||
450873C91D9D86F4006B54F2 /* OWSExpirableMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSExpirableMessageView.h; sourceTree = "<group>"; };
|
||||
4509E7991DD653700025A59F /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = Carthage/Build/iOS/WebRTC.framework; sourceTree = "<group>"; };
|
||||
450D19111F85236600970622 /* RemoteVideoView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RemoteVideoView.h; sourceTree = "<group>"; };
|
||||
450D19121F85236600970622 /* RemoteVideoView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RemoteVideoView.m; sourceTree = "<group>"; };
|
||||
|
@ -616,20 +618,14 @@
|
|||
4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; };
|
||||
4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldHelper.swift; sourceTree = "<group>"; };
|
||||
4523149B1F7D7F81003A428C /* OWSMessagesBubbleImageFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSMessagesBubbleImageFactory.swift; sourceTree = "<group>"; };
|
||||
4526BD481CA61C8D00166BC8 /* OWSMessageEditing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageEditing.h; sourceTree = "<group>"; };
|
||||
452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = "<group>"; };
|
||||
452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = "<group>"; };
|
||||
452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerAdapter.swift; sourceTree = "<group>"; };
|
||||
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = "<group>"; };
|
||||
452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageFetcherJob.swift; path = Jobs/MessageFetcherJob.swift; sourceTree = "<group>"; };
|
||||
4531C9C21DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMessagesCollectionViewCell+OWS.h"; sourceTree = "<group>"; };
|
||||
4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMessagesCollectionViewCell+OWS.m"; sourceTree = "<group>"; };
|
||||
45387B021E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWS102MoveLoggingPreferenceToUserDefaults.h; path = Migrations/OWS102MoveLoggingPreferenceToUserDefaults.h; sourceTree = "<group>"; };
|
||||
45387B031E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWS102MoveLoggingPreferenceToUserDefaults.m; path = Migrations/OWS102MoveLoggingPreferenceToUserDefaults.m; sourceTree = "<group>"; };
|
||||
4539B5851F79348F007141FF /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = "<group>"; };
|
||||
453CC0361D08E1A60040EBA3 /* sn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sn; path = translations/sn.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessagesBubblesSizeCalculator.h; sourceTree = "<group>"; };
|
||||
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OWSMessagesBubblesSizeCalculator.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
|
||||
4542F0931EB9372700C7EE92 /* SystemContactsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContactsFetcher.swift; sourceTree = "<group>"; };
|
||||
4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+retainUntilComplete.swift"; sourceTree = "<group>"; };
|
||||
45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = "<group>"; };
|
||||
|
@ -646,7 +642,6 @@
|
|||
45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAvatarBuilder.m; sourceTree = "<group>"; };
|
||||
45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGroupAvatarBuilder.h; sourceTree = "<group>"; };
|
||||
45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSGroupAvatarBuilder.m; sourceTree = "<group>"; };
|
||||
45666ECE1D995B94008FE134 /* OWSMessageData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageData.h; sourceTree = "<group>"; };
|
||||
45666F541D9B2827008FE134 /* OWSScrubbingLogFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScrubbingLogFormatter.h; sourceTree = "<group>"; };
|
||||
45666F551D9B2827008FE134 /* OWSScrubbingLogFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScrubbingLogFormatter.m; sourceTree = "<group>"; };
|
||||
45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScrubbingLogFormatterTest.m; sourceTree = "<group>"; };
|
||||
|
@ -713,17 +708,11 @@
|
|||
45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = "<group>"; };
|
||||
45F170CB1E310E22003FC1F2 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = "<group>"; };
|
||||
45F170D51E315310003FC1F2 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||
45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCollectionViewCell.h; sourceTree = "<group>"; };
|
||||
45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCollectionViewCell.m; sourceTree = "<group>"; };
|
||||
45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSIncomingMessageCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSOutgoingMessageCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
45F3AEB51DFDE7900080CE33 /* AvatarImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = "<group>"; };
|
||||
45F659721E1BD99C00444429 /* CallKitCallUIAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallUIAdaptee.swift; sourceTree = "<group>"; };
|
||||
45F659811E1BE77000444429 /* NonCallKitCallUIAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonCallKitCallUIAdaptee.swift; sourceTree = "<group>"; };
|
||||
45FBC59A1DF8575700E9B410 /* CallKitCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallKitCallManager.swift; sourceTree = "<group>"; };
|
||||
45FBC5D01DF8592E00E9B410 /* SignalCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalCall.swift; sourceTree = "<group>"; };
|
||||
4CE0E3751B95453C007210CF /* TSAnimatedAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSAnimatedAdapter.h; sourceTree = "<group>"; };
|
||||
4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAnimatedAdapter.m; sourceTree = "<group>"; };
|
||||
70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
|
||||
7038632318F70C0700D4A43F /* CryptoTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CryptoTools.h; sourceTree = "<group>"; };
|
||||
7038632418F70C0700D4A43F /* CryptoTools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CryptoTools.m; sourceTree = "<group>"; };
|
||||
|
@ -760,8 +749,6 @@
|
|||
A1C32D4F17A06537000A904E /* AddressBookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBookUI.framework; path = System/Library/Frameworks/AddressBookUI.framework; sourceTree = SDKROOT; };
|
||||
A1FDCBEE16DAA6C300868894 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
A5509EC91A69AB8B00ABA4BC /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Storyboard/Main.storyboard; sourceTree = "<group>"; };
|
||||
A5E9D4B91A65FAD800E4481C /* TSVideoAttachmentAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSVideoAttachmentAdapter.m; sourceTree = "<group>"; };
|
||||
A5E9D4BA1A65FAD800E4481C /* TSVideoAttachmentAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSVideoAttachmentAdapter.h; sourceTree = "<group>"; };
|
||||
AD2AB1207E8888E4262D781B /* Pods-SignalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AD41D7B31A6F6F0600241130 /* play_button.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = play_button.png; sourceTree = "<group>"; };
|
||||
AD41D7B41A6F6F0600241130 /* play_button@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play_button@2x.png"; sourceTree = "<group>"; };
|
||||
|
@ -786,8 +773,6 @@
|
|||
B6258B311C29E2E60014138E /* NotificationsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationsManager.h; sourceTree = "<group>"; };
|
||||
B6258B321C29E2E60014138E /* NotificationsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = NotificationsManager.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
|
||||
B625CD551ABB589C00E8B23C /* NewMessage.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = NewMessage.aifc; sourceTree = "<group>"; };
|
||||
B62D53F51A23CCAD009AAF82 /* TSMessageAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSMessageAdapter.h; sourceTree = "<group>"; };
|
||||
B62D53F61A23CCAD009AAF82 /* TSMessageAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSMessageAdapter.m; sourceTree = "<group>"; };
|
||||
B633C5041A1D190B0059AC12 /* call@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "call@2x.png"; sourceTree = "<group>"; };
|
||||
B633C50B1A1D190B0059AC12 /* contact_default_feed.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = contact_default_feed.png; sourceTree = "<group>"; };
|
||||
B633C51B1A1D190B0059AC12 /* endcall@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "endcall@2x.png"; sourceTree = "<group>"; };
|
||||
|
@ -843,8 +828,6 @@
|
|||
B69C2D1A1AA5447600A640C2 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = translations/ca.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B69C2D1B1AA5448300A640C2 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = translations/cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B69CD25019773E79005CE69A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
|
||||
B6A3EB491A423B3800B2236B /* TSPhotoAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSPhotoAdapter.h; sourceTree = "<group>"; };
|
||||
B6A3EB4A1A423B3800B2236B /* TSPhotoAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSPhotoAdapter.m; sourceTree = "<group>"; };
|
||||
B6B1013A196D213F007E3930 /* SignalKeyingStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalKeyingStorage.h; sourceTree = "<group>"; };
|
||||
B6B1013B196D213F007E3930 /* SignalKeyingStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalKeyingStorage.m; sourceTree = "<group>"; };
|
||||
B6B226961BE4B7D200860F4D /* ContactsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ContactsUI.framework; path = System/Library/Frameworks/ContactsUI.framework; sourceTree = SDKROOT; };
|
||||
|
@ -854,7 +837,6 @@
|
|||
B6C6AE531A305ED1006BAF8F /* redphone.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = redphone.cer; sourceTree = "<group>"; };
|
||||
B6C93C4C199567AD00EDF894 /* DebugLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugLogger.h; sourceTree = "<group>"; };
|
||||
B6C93C4D199567AD00EDF894 /* DebugLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugLogger.m; sourceTree = "<group>"; };
|
||||
B6D3CBCE1C1376BE00C039DF /* TSContentAdapters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSContentAdapters.h; sourceTree = "<group>"; };
|
||||
B6DA6B051B8A2F9A00CA6F98 /* AppStoreRating.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppStoreRating.h; sourceTree = "<group>"; };
|
||||
B6DA6B061B8A2F9A00CA6F98 /* AppStoreRating.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppStoreRating.m; sourceTree = "<group>"; };
|
||||
B6F509961AA53F760068F56A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = translations/en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -977,15 +959,21 @@
|
|||
3448BFC01EDF0EA7005B2D69 /* ConversationView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3448BFC91EDF0EA7005B2D69 /* ConversationViewController.h */,
|
||||
3448BFCA1EDF0EA7005B2D69 /* ConversationViewController.m */,
|
||||
3448BFCB1EDF0EA7005B2D69 /* ConversationViewController.xib */,
|
||||
3448BFC71EDF0EA7005B2D69 /* OWSMessagesComposerTextView.h */,
|
||||
3448BFC81EDF0EA7005B2D69 /* OWSMessagesComposerTextView.m */,
|
||||
3448BFC31EDF0EA7005B2D69 /* OWSMessagesInputToolbar.h */,
|
||||
3448BFC41EDF0EA7005B2D69 /* OWSMessagesInputToolbar.m */,
|
||||
3448BFC11EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.h */,
|
||||
3448BFC21EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.m */,
|
||||
34D1F0951F867BFC0066283D /* Cells */,
|
||||
34D1F0B21F86D31D0066283D /* ConversationCollectionView.h */,
|
||||
34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */,
|
||||
34D1F0651F8678AA0066283D /* ConversationHeaderView.h */,
|
||||
34D1F0661F8678AA0066283D /* ConversationHeaderView.m */,
|
||||
34D1F0671F8678AA0066283D /* ConversationInputTextView.h */,
|
||||
34D1F0681F8678AA0066283D /* ConversationInputTextView.m */,
|
||||
34D1F0691F8678AA0066283D /* ConversationInputToolbar.h */,
|
||||
34D1F06A1F8678AA0066283D /* ConversationInputToolbar.m */,
|
||||
34D1F06D1F8678AA0066283D /* ConversationViewController.h */,
|
||||
34D1F06E1F8678AA0066283D /* ConversationViewController.m */,
|
||||
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
|
||||
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
|
||||
34D1F0711F8678AA0066283D /* ConversationViewLayout.h */,
|
||||
34D1F0721F8678AA0066283D /* ConversationViewLayout.m */,
|
||||
);
|
||||
path = ConversationView;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1068,7 +1056,6 @@
|
|||
34B3F85E1E8DF1700035BE1A /* OWSLinkDeviceViewController.m */,
|
||||
34B3F85F1E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.h */,
|
||||
34B3F8601E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.m */,
|
||||
34B3F8611E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib */,
|
||||
34C42D591F45F7A80072EC04 /* OWSNavigationController.h */,
|
||||
34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */,
|
||||
34B3F8621E8DF1700035BE1A /* OWSQRCodeScanningViewController.h */,
|
||||
|
@ -1148,6 +1135,38 @@
|
|||
path = Profiles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
34D1F0951F867BFC0066283D /* Cells */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */,
|
||||
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */,
|
||||
34D1F0961F867BFC0066283D /* ConversationViewCell.h */,
|
||||
34D1F0971F867BFC0066283D /* ConversationViewCell.m */,
|
||||
34D1F0981F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.h */,
|
||||
34D1F0991F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.m */,
|
||||
34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */,
|
||||
34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */,
|
||||
34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */,
|
||||
34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */,
|
||||
34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */,
|
||||
34D1F09D1F867BFC0066283D /* OWSExpirationTimerView.h */,
|
||||
34D1F09E1F867BFC0066283D /* OWSExpirationTimerView.m */,
|
||||
34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */,
|
||||
34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */,
|
||||
34D1F09F1F867BFC0066283D /* OWSIncomingMessageCell.h */,
|
||||
34D1F0A01F867BFC0066283D /* OWSIncomingMessageCell.m */,
|
||||
34D1F0A11F867BFC0066283D /* OWSMessageCell.h */,
|
||||
34D1F0A21F867BFC0066283D /* OWSMessageCell.m */,
|
||||
34D1F0A31F867BFC0066283D /* OWSOutgoingMessageCell.h */,
|
||||
34D1F0A41F867BFC0066283D /* OWSOutgoingMessageCell.m */,
|
||||
34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */,
|
||||
34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */,
|
||||
34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */,
|
||||
34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */,
|
||||
);
|
||||
path = Cells;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
34D8C0221ED3673300188D7C /* DebugUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1269,10 +1288,7 @@
|
|||
458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */,
|
||||
45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */,
|
||||
45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */,
|
||||
453D28B81D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.h */,
|
||||
453D28B91D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m */,
|
||||
34D913491F62D4A500722898 /* SignalAttachment.swift */,
|
||||
B62D53F41A23CC8B009AAF82 /* TSMessageAdapters */,
|
||||
34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */,
|
||||
34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */,
|
||||
);
|
||||
|
@ -1492,32 +1508,14 @@
|
|||
451764281DE939FD00EDB8B9 /* ContactCell.xib */,
|
||||
76EB052E18170B33006006FC /* ContactTableViewCell.h */,
|
||||
76EB052F18170B33006006FC /* ContactTableViewCell.m */,
|
||||
4531C9C21DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.h */,
|
||||
4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */,
|
||||
3453D8E91EC0D4ED003F9E6F /* OWSAlerts.swift */,
|
||||
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */,
|
||||
34F308A11ECB469700BB7697 /* OWSBezierPathView.m */,
|
||||
34C42D5F1F4734CA0072EC04 /* OWSContactOffersCell.h */,
|
||||
34C42D601F4734CA0072EC04 /* OWSContactOffersCell.m */,
|
||||
459311FA1D75C948008DD4F0 /* OWSDeviceTableViewCell.h */,
|
||||
459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */,
|
||||
450873C91D9D86F4006B54F2 /* OWSExpirableMessageView.h */,
|
||||
450873C11D9D5149006B54F2 /* OWSExpirationTimerView.h */,
|
||||
450873C21D9D5149006B54F2 /* OWSExpirationTimerView.m */,
|
||||
34C04D7F1F6195E6004308B3 /* OWSFlatButton.swift */,
|
||||
450873C51D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.h */,
|
||||
450873C61D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m */,
|
||||
45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */,
|
||||
34D5CC981EA6EB79005515DB /* OWSMessageCollectionViewCell.h */,
|
||||
45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */,
|
||||
45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */,
|
||||
45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */,
|
||||
34330AA11E79686200DF2FB9 /* OWSProgressView.h */,
|
||||
34330AA21E79686200DF2FB9 /* OWSProgressView.m */,
|
||||
341207251EE19F6A00463194 /* OWSSystemMessageCell.h */,
|
||||
341207261EE19F6A00463194 /* OWSSystemMessageCell.m */,
|
||||
34F3089D1ECA580B00BB7697 /* OWSUnreadIndicatorCell.h */,
|
||||
34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */,
|
||||
45A6DAD51EBBF85500893231 /* ReminderView.swift */,
|
||||
450D19111F85236600970622 /* RemoteVideoView.h */,
|
||||
450D19121F85236600970622 /* RemoteVideoView.m */,
|
||||
|
@ -1548,33 +1546,6 @@
|
|||
name = rating;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B62D53F41A23CC8B009AAF82 /* TSMessageAdapters */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
34330A5F1E788EA900DF2FB9 /* AttachmentUploadView.h */,
|
||||
34330A601E788EA900DF2FB9 /* AttachmentUploadView.m */,
|
||||
341BB7471DB727EE001E2975 /* JSQMediaItem+OWS.h */,
|
||||
341BB7481DB727EE001E2975 /* JSQMediaItem+OWS.m */,
|
||||
45666ECE1D995B94008FE134 /* OWSMessageData.h */,
|
||||
4526BD481CA61C8D00166BC8 /* OWSMessageEditing.h */,
|
||||
34D5CC9B1EA6ED17005515DB /* OWSMessageMediaAdapter.h */,
|
||||
4CE0E3751B95453C007210CF /* TSAnimatedAdapter.h */,
|
||||
4CE0E3761B954546007210CF /* TSAnimatedAdapter.m */,
|
||||
B6D3CBCE1C1376BE00C039DF /* TSContentAdapters.h */,
|
||||
345671081E8A9F5D006EE662 /* TSGenericAttachmentAdapter.h */,
|
||||
345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */,
|
||||
B62D53F51A23CCAD009AAF82 /* TSMessageAdapter.h */,
|
||||
B62D53F61A23CCAD009AAF82 /* TSMessageAdapter.m */,
|
||||
B6A3EB491A423B3800B2236B /* TSPhotoAdapter.h */,
|
||||
B6A3EB4A1A423B3800B2236B /* TSPhotoAdapter.m */,
|
||||
A5E9D4BA1A65FAD800E4481C /* TSVideoAttachmentAdapter.h */,
|
||||
A5E9D4B91A65FAD800E4481C /* TSVideoAttachmentAdapter.m */,
|
||||
452EA0961EA662330078744B /* AttachmentPointerAdapter.swift */,
|
||||
);
|
||||
name = TSMessageAdapters;
|
||||
path = TSMessageAdapaters;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B633C4FD1A1D190B0059AC12 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1984,9 +1955,7 @@
|
|||
AD83FF3F1A73426500B5C81A /* audio_pause_button_blue.png in Resources */,
|
||||
34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */,
|
||||
A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */,
|
||||
45F2B1971D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib in Resources */,
|
||||
AD83FF421A73426500B5C81A /* audio_play_button.png in Resources */,
|
||||
45F2B1981D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib in Resources */,
|
||||
34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */,
|
||||
B633C5C41A1D190B0059AC12 /* mute_on@2x.png in Resources */,
|
||||
B633C5CE1A1D190B0059AC12 /* quit@2x.png in Resources */,
|
||||
|
@ -1996,7 +1965,6 @@
|
|||
B633C59D1A1D190B0059AC12 /* endcall@2x.png in Resources */,
|
||||
FC5CDF391A3393DD00B47253 /* error_white@2x.png in Resources */,
|
||||
B633C5D21A1D190B0059AC12 /* savephoto@2x.png in Resources */,
|
||||
3448BFD11EDF0EA7005B2D69 /* ConversationViewController.xib in Resources */,
|
||||
B10C9B611A7049EC00ECA2BF /* play_icon.png in Resources */,
|
||||
AD83FF401A73426500B5C81A /* audio_pause_button_blue@2x.png in Resources */,
|
||||
B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */,
|
||||
|
@ -2019,7 +1987,6 @@
|
|||
B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */,
|
||||
E1370BE418A0686C00826894 /* outring.mp3 in Resources */,
|
||||
B10C9B601A7049EC00ECA2BF /* pause_icon@2x.png in Resources */,
|
||||
34B3F88C1E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib in Resources */,
|
||||
B6C6AE551A305ED1006BAF8F /* redphone.cer in Resources */,
|
||||
E1370BE518A0686C00826894 /* r.caf in Resources */,
|
||||
FC9120411A39EFB70074545C /* qr@2x.png in Resources */,
|
||||
|
@ -2207,6 +2174,8 @@
|
|||
files = (
|
||||
34B3F8761E8DF1700035BE1A /* CodeVerificationViewController.m in Sources */,
|
||||
76EB063E18170B33006006FC /* Operation.m in Sources */,
|
||||
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */,
|
||||
34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */,
|
||||
34B3F8741E8DF1700035BE1A /* AttachmentSharing.m in Sources */,
|
||||
34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */,
|
||||
45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */,
|
||||
|
@ -2217,13 +2186,15 @@
|
|||
4505C2C21E648F7A00CEBF41 /* ExperienceUpgradeFinder.swift in Sources */,
|
||||
341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */,
|
||||
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */,
|
||||
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */,
|
||||
344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */,
|
||||
B6DA6B071B8A2F9A00CA6F98 /* AppStoreRating.m in Sources */,
|
||||
455AC69B1F4F79E500134004 /* ImageCache.swift in Sources */,
|
||||
451A13B11E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */,
|
||||
3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */,
|
||||
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */,
|
||||
34D1F0821F8678AA0066283D /* ConversationHeaderView.m in Sources */,
|
||||
340CB2241EAC155C0001CAA1 /* ContactsViewHelper.m in Sources */,
|
||||
34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */,
|
||||
34CE88EC1F3237260098030F /* OWSProfileManager.m in Sources */,
|
||||
45CD81F21DC03A22004C9430 /* OWSLogger.m in Sources */,
|
||||
4542F0941EB9372700C7EE92 /* SystemContactsFetcher.swift in Sources */,
|
||||
|
@ -2231,9 +2202,8 @@
|
|||
B97940271832BD2400BD66CB /* UIUtil.m in Sources */,
|
||||
34CE88ED1F3237260098030F /* ProfileFetcherJob.swift in Sources */,
|
||||
34B3F8791E8DF1700035BE1A /* CountryCodeViewController.m in Sources */,
|
||||
4CE0E3771B954546007210CF /* TSAnimatedAdapter.m in Sources */,
|
||||
34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */,
|
||||
34CA1C251F706B5400E51C51 /* NSAttributedString+OWS.m in Sources */,
|
||||
4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */,
|
||||
4542F0961EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */,
|
||||
4516E3FF1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */,
|
||||
4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */,
|
||||
|
@ -2249,30 +2219,28 @@
|
|||
45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */,
|
||||
34C04D801F6195E6004308B3 /* OWSFlatButton.swift in Sources */,
|
||||
458DE9D91DEE7B360071BB03 /* OWSWebRTCDataProtos.pb.m in Sources */,
|
||||
B62D53F71A23CCAD009AAF82 /* TSMessageAdapter.m in Sources */,
|
||||
76EB063C18170B33006006FC /* NumberUtil.m in Sources */,
|
||||
34CA1C291F7164F700E51C51 /* MediaMessageView.swift in Sources */,
|
||||
B6A3EB4B1A423B3800B2236B /* TSPhotoAdapter.m in Sources */,
|
||||
3400C7961EAF99F4008A8584 /* SelectThreadViewController.m in Sources */,
|
||||
34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */,
|
||||
34B3F88F1E8DF1710035BE1A /* RegistrationViewController.m in Sources */,
|
||||
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */,
|
||||
3448BFCD1EDF0EA7005B2D69 /* OWSMessagesInputToolbar.m in Sources */,
|
||||
34B3F8901E8DF1710035BE1A /* AppSettingsViewController.m in Sources */,
|
||||
34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */,
|
||||
343D3D9B1E9283F100165CA4 /* BlockListUIUtils.m in Sources */,
|
||||
34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */,
|
||||
34D1F0AC1F867BFC0066283D /* OWSExpirationTimerView.m in Sources */,
|
||||
76EB063A18170B33006006FC /* FunctionalUtil.m in Sources */,
|
||||
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */,
|
||||
34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */,
|
||||
348F2EAE1F0D21BC00D4ECE0 /* DeviceSleepManager.swift in Sources */,
|
||||
34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */,
|
||||
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */,
|
||||
451686AB1F520CDA00AC3D4B /* MultiDeviceProfileKeyUpdateJob.swift in Sources */,
|
||||
76EB058A18170B33006006FC /* Release.m in Sources */,
|
||||
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
|
||||
34D9134D1F66DB7C00722898 /* ModalActivityIndicatorViewController.swift in Sources */,
|
||||
4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */,
|
||||
450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */,
|
||||
76EB057A18170B33006006FC /* OWSContactsManager.m in Sources */,
|
||||
76EB064218170B33006006FC /* StringUtil.m in Sources */,
|
||||
452037D11EE84975004E4CDF /* DebugUISessionState.m in Sources */,
|
||||
|
@ -2282,7 +2250,6 @@
|
|||
345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */,
|
||||
4585C4601ED4FD0400896AEA /* OWS104CreateRecipientIdentities.m in Sources */,
|
||||
45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
|
||||
450873C31D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */,
|
||||
B6258B331C29E2E60014138E /* NotificationsManager.m in Sources */,
|
||||
34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */,
|
||||
34533F181EA8D2070006114F /* OWSAudioAttachmentPlayer.m in Sources */,
|
||||
|
@ -2296,25 +2263,23 @@
|
|||
450449391F45EE7D002D1ADA /* NSString+OWS.m in Sources */,
|
||||
45464DBC1DFA041F001D3FD6 /* DataChannelMessage.swift in Sources */,
|
||||
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */,
|
||||
3448BFCF1EDF0EA7005B2D69 /* OWSMessagesComposerTextView.m in Sources */,
|
||||
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */,
|
||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
3472229F1EB22FFE00E53955 /* AddToGroupViewController.m in Sources */,
|
||||
45666F561D9B2827008FE134 /* OWSScrubbingLogFormatter.m in Sources */,
|
||||
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
|
||||
34F3089F1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m in Sources */,
|
||||
34B3F8861E8DF1700035BE1A /* NotificationSettingsOptionsViewController.m in Sources */,
|
||||
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */,
|
||||
4556FA681F54AA9500AF40DD /* DebugUIProfile.swift in Sources */,
|
||||
452EA0971EA662330078744B /* AttachmentPointerAdapter.swift in Sources */,
|
||||
34DFCB851E8E04B500053165 /* AddToBlockListViewController.m in Sources */,
|
||||
45855F371D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */,
|
||||
34D1F0AD1F867BFC0066283D /* OWSIncomingMessageCell.m in Sources */,
|
||||
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
|
||||
45666EC61D99483D008FE134 /* OWSAvatarBuilder.m in Sources */,
|
||||
45E615161E8C590B0018AD52 /* DisplayableTextFilter.swift in Sources */,
|
||||
34C42D611F4734CA0072EC04 /* OWSContactOffersCell.m in Sources */,
|
||||
34B3F88A1E8DF1700035BE1A /* OWSLinkDeviceViewController.m in Sources */,
|
||||
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
|
||||
76EB068618170B34006006FC /* ContactTableViewCell.m in Sources */,
|
||||
3497DBEF1ECE2E4700DB2605 /* DomainFrontingCountryViewController.m in Sources */,
|
||||
34B3F8881E8DF1700035BE1A /* OversizeTextMessageViewController.swift in Sources */,
|
||||
|
@ -2322,12 +2287,11 @@
|
|||
34B3F8A21E8EA6040035BE1A /* ViewControllerUtils.m in Sources */,
|
||||
34CA1C271F7156F300E51C51 /* MessageMetadataViewController.swift in Sources */,
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
|
||||
453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */,
|
||||
45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */,
|
||||
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */,
|
||||
34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */,
|
||||
45F170CC1E310E22003FC1F2 /* WeakTimer.swift in Sources */,
|
||||
34B3F8871E8DF1700035BE1A /* NotificationSettingsViewController.m in Sources */,
|
||||
A5E9D4BB1A65FAD800E4481C /* TSVideoAttachmentAdapter.m in Sources */,
|
||||
458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */,
|
||||
FCFA64B41A24F3880007FB87 /* UIColor+OWS.m in Sources */,
|
||||
4517642B1DE939FD00EDB8B9 /* ContactCell.swift in Sources */,
|
||||
|
@ -2338,11 +2302,13 @@
|
|||
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
|
||||
B6C93C4E199567AD00EDF894 /* DebugLogger.m in Sources */,
|
||||
34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */,
|
||||
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */,
|
||||
34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */,
|
||||
3453D8EA1EC0D4ED003F9E6F /* OWSAlerts.swift in Sources */,
|
||||
45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */,
|
||||
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
||||
34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */,
|
||||
34D1F0AF1F867BFC0066283D /* OWSOutgoingMessageCell.m in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */,
|
||||
34535D821E256BE9008A4747 /* UIView+OWS.m in Sources */,
|
||||
|
@ -2358,14 +2324,13 @@
|
|||
34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */,
|
||||
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
|
||||
34B3F8811E8DF1700035BE1A /* LockInteractionController.m in Sources */,
|
||||
3448BFCC1EDF0EA7005B2D69 /* OWSMessagesToolbarContentView.m in Sources */,
|
||||
3448BFD01EDF0EA7005B2D69 /* ConversationViewController.m in Sources */,
|
||||
34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */,
|
||||
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
|
||||
45BB93381E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */,
|
||||
458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */,
|
||||
451DE9FD1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */,
|
||||
34D99C8C1F27B13B00D284D6 /* OWSViewController.m in Sources */,
|
||||
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
|
||||
45666F761D9BFE00008FE134 /* OWS100RemoveTSRecipientsMigration.m in Sources */,
|
||||
340CB2271EAC25820001CAA1 /* UpdateGroupViewController.m in Sources */,
|
||||
34B3F89F1E8DF5490035BE1A /* OWSTableViewController.m in Sources */,
|
||||
|
@ -2373,11 +2338,10 @@
|
|||
FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */,
|
||||
3400C7991EAFB772008A8584 /* ThreadViewHelper.m in Sources */,
|
||||
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
|
||||
341207271EE19F6A00463194 /* OWSSystemMessageCell.m in Sources */,
|
||||
341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */,
|
||||
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */,
|
||||
34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */,
|
||||
34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */,
|
||||
34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */,
|
||||
45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */,
|
||||
34C42D5B1F45F7A80072EC04 /* OWSNavigationController.m in Sources */,
|
||||
B68112EA1A4D9EC400BA82FF /* UIImage+OWS.m in Sources */,
|
||||
B609597C1C2C0FC6004E8797 /* iRate.m in Sources */,
|
||||
|
@ -2386,6 +2350,7 @@
|
|||
45F170D61E315310003FC1F2 /* Weak.swift in Sources */,
|
||||
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,
|
||||
34B3F8891E8DF1700035BE1A /* OWSConversationSettingsViewController.m in Sources */,
|
||||
34D1F0AA1F867BFC0066283D /* JSQMessagesCollectionViewCell+OWS.m in Sources */,
|
||||
34C42D671F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m in Sources */,
|
||||
34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */,
|
||||
34B3F8731E8DF1700035BE1A /* AttachmentApprovalViewController.swift in Sources */,
|
||||
|
@ -2401,10 +2366,11 @@
|
|||
34E8BF381EE9E2FD00F5F4CA /* FingerprintViewScanController.m in Sources */,
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
||||
76EB058818170B33006006FC /* OWSPreferences.m in Sources */,
|
||||
34330A611E788EA900DF2FB9 /* AttachmentUploadView.m in Sources */,
|
||||
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
|
||||
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */,
|
||||
34B3F87D1E8DF1700035BE1A /* FullImageViewController.m in Sources */,
|
||||
45666F7B1D9C0533008FE134 /* OWSDatabaseMigration.m in Sources */,
|
||||
34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */,
|
||||
B90418E6183E9DD40038554A /* DateUtil.m in Sources */,
|
||||
459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */,
|
||||
);
|
||||
|
@ -2451,8 +2417,6 @@
|
|||
4504493A1F45EE7D002D1ADA /* NSString+OWS.m in Sources */,
|
||||
45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
|
||||
45AE48521E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
|
||||
450873C41D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */,
|
||||
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */,
|
||||
B660F7561C29988E00687D6E /* PushManager.m in Sources */,
|
||||
45FBC5D21DF8592E00E9B410 /* SignalCall.swift in Sources */,
|
||||
451A13B21E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */,
|
||||
|
@ -2499,7 +2463,6 @@
|
|||
45C0DC1F1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
|
||||
4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */,
|
||||
455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */,
|
||||
450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -2593,7 +2556,11 @@
|
|||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
"GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1";
|
||||
"GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
"SSK_BUILDING_FOR_TESTS=1",
|
||||
);
|
||||
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
|
||||
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
|
|
@ -2,60 +2,58 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageData.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSCall;
|
||||
@class TSInteraction;
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
kCallOutgoing = 1,
|
||||
kCallIncoming = 2,
|
||||
kCallMissed = 3,
|
||||
kCallOutgoingIncomplete = 4,
|
||||
kCallIncomingIncomplete = 5,
|
||||
// kGroupUpdateJoin = 6, has been deprecated.
|
||||
// kGroupUpdateLeft = 7, has been deprecated.
|
||||
// kGroupUpdate = 8, has been deprecated.
|
||||
kCallMissedBecauseOfChangedIdentity = 9,
|
||||
} CallStatus;
|
||||
|
||||
@interface OWSCall : NSObject <OWSMessageData>
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (instancetype)initWithCallRecord:(TSCall *)callRecord;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
/*
|
||||
* Returns the string Id of the user who initiated the call
|
||||
*/
|
||||
@property (copy, nonatomic, readonly) NSString *senderId;
|
||||
|
||||
/*
|
||||
* Returns the display name for user who initiated the call
|
||||
*/
|
||||
@property (copy, nonatomic, readonly) NSString *senderDisplayName;
|
||||
|
||||
/*
|
||||
* Returns date of the call
|
||||
*/
|
||||
@property (copy, nonatomic, readonly) NSDate *date;
|
||||
|
||||
/*
|
||||
* Returns the call status
|
||||
* @see CallStatus
|
||||
*/
|
||||
@property (nonatomic) CallStatus status;
|
||||
|
||||
/**
|
||||
* String to be displayed
|
||||
*/
|
||||
@property (nonatomic, copy) NSString *detailString;
|
||||
|
||||
- (NSString *)dateText;
|
||||
|
||||
@end
|
||||
//@class TSCall;
|
||||
//@class TSInteraction;
|
||||
//
|
||||
// typedef enum : NSUInteger {
|
||||
// kCallOutgoing = 1,
|
||||
// kCallIncoming = 2,
|
||||
// kCallMissed = 3,
|
||||
// kCallOutgoingIncomplete = 4,
|
||||
// kCallIncomingIncomplete = 5,
|
||||
// // kGroupUpdateJoin = 6, has been deprecated.
|
||||
// // kGroupUpdateLeft = 7, has been deprecated.
|
||||
// // kGroupUpdate = 8, has been deprecated.
|
||||
// kCallMissedBecauseOfChangedIdentity = 9,
|
||||
//} CallStatus;
|
||||
//
|
||||
//@interface OWSCall : NSObject <OWSMessageData>
|
||||
//
|
||||
//#pragma mark - Initialization
|
||||
//
|
||||
//- (instancetype)initWithCallRecord:(TSCall *)callRecord;
|
||||
//- (instancetype)init NS_UNAVAILABLE;
|
||||
//
|
||||
///*
|
||||
// * Returns the string Id of the user who initiated the call
|
||||
// */
|
||||
//@property (copy, nonatomic, readonly) NSString *senderId;
|
||||
//
|
||||
///*
|
||||
// * Returns the display name for user who initiated the call
|
||||
// */
|
||||
//@property (copy, nonatomic, readonly) NSString *senderDisplayName;
|
||||
//
|
||||
///*
|
||||
// * Returns date of the call
|
||||
// */
|
||||
//@property (copy, nonatomic, readonly) NSDate *date;
|
||||
//
|
||||
///*
|
||||
// * Returns the call status
|
||||
// * @see CallStatus
|
||||
// */
|
||||
//@property (nonatomic) CallStatus status;
|
||||
//
|
||||
///**
|
||||
// * String to be displayed
|
||||
// */
|
||||
//@property (nonatomic, copy) NSString *detailString;
|
||||
//
|
||||
//- (NSString *)dateText;
|
||||
//
|
||||
//@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -11,214 +11,214 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSCall ()
|
||||
|
||||
// -- Redeclaring properties from OWSMessageData protocol to synthesize variables
|
||||
@property (nonatomic) TSMessageAdapterType messageType;
|
||||
@property (nonatomic) BOOL isExpiringMessage;
|
||||
@property (nonatomic) BOOL shouldStartExpireTimer;
|
||||
@property (nonatomic) double expiresAtSeconds;
|
||||
@property (nonatomic) uint32_t expiresInSeconds;
|
||||
@property (nonatomic) TSInteraction *interaction;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSCall
|
||||
|
||||
#pragma mark - Initialzation
|
||||
|
||||
- (instancetype)initWithCallRecord:(TSCall *)callRecord
|
||||
{
|
||||
TSThread *thread = callRecord.thread;
|
||||
TSContactThread *contactThread;
|
||||
if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
contactThread = (TSContactThread *)thread;
|
||||
} else {
|
||||
DDLogError(@"%@ Unexpected thread type: %@", self.tag, thread);
|
||||
}
|
||||
|
||||
CallStatus status = 0;
|
||||
switch (callRecord.callType) {
|
||||
case RPRecentCallTypeOutgoing:
|
||||
status = kCallOutgoing;
|
||||
break;
|
||||
case RPRecentCallTypeOutgoingIncomplete:
|
||||
status = kCallOutgoingIncomplete;
|
||||
break;
|
||||
case RPRecentCallTypeMissed:
|
||||
status = kCallMissed;
|
||||
break;
|
||||
case RPRecentCallTypeIncoming:
|
||||
status = kCallIncoming;
|
||||
break;
|
||||
case RPRecentCallTypeIncomingIncomplete:
|
||||
status = kCallIncomingIncomplete;
|
||||
break;
|
||||
case RPRecentCallTypeMissedBecauseOfChangedIdentity:
|
||||
status = kCallMissedBecauseOfChangedIdentity;
|
||||
break;
|
||||
default:
|
||||
status = kCallIncoming;
|
||||
break;
|
||||
}
|
||||
|
||||
NSString *name = contactThread.name;
|
||||
NSString *detailString;
|
||||
switch (status) {
|
||||
case kCallMissed:
|
||||
detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_MISSED_CALL_WITH_NAME", nil), name];
|
||||
break;
|
||||
case kCallIncoming:
|
||||
detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_RECEIVED_CALL", nil), name];
|
||||
break;
|
||||
case kCallIncomingIncomplete:
|
||||
detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_THEY_TRIED_TO_CALL_YOU", nil), name];
|
||||
break;
|
||||
case kCallOutgoing:
|
||||
detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_YOU_CALLED", nil), name];
|
||||
break;
|
||||
case kCallOutgoingIncomplete:
|
||||
detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_YOU_TRIED_TO_CALL", nil), name];
|
||||
break;
|
||||
case kCallMissedBecauseOfChangedIdentity:
|
||||
detailString = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"MSGVIEW_MISSED_CALL_BECAUSE_OF_CHANGED_IDENTITY", nil), name];
|
||||
default:
|
||||
detailString = @"";
|
||||
break;
|
||||
}
|
||||
|
||||
return [self initWithInteraction:callRecord
|
||||
callerId:contactThread.contactIdentifier
|
||||
callerDisplayName:name
|
||||
date:callRecord.dateForSorting
|
||||
status:status
|
||||
displayString:detailString];
|
||||
}
|
||||
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
callerId:(NSString *)senderId
|
||||
callerDisplayName:(NSString *)senderDisplayName
|
||||
date:(nullable NSDate *)date
|
||||
status:(CallStatus)status
|
||||
displayString:(NSString *)detailString
|
||||
{
|
||||
NSParameterAssert(senderId != nil);
|
||||
NSParameterAssert(senderDisplayName != nil);
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_interaction = interaction;
|
||||
_senderId = [senderId copy];
|
||||
_senderDisplayName = [senderDisplayName copy];
|
||||
_date = [date copy];
|
||||
_status = status;
|
||||
_isExpiringMessage = NO; // TODO - call notifications should expire too.
|
||||
_shouldStartExpireTimer = NO; // TODO - call notifications should expire too.
|
||||
_messageType = TSCallAdapter;
|
||||
|
||||
// TODO interpret detailString from status. make sure it works for calls and
|
||||
// our re-use of calls as group update display
|
||||
_detailString = [detailString stringByAppendingFormat:@" "];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)dateText
|
||||
{
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
dateFormatter.timeStyle = NSDateFormatterShortStyle;
|
||||
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
|
||||
dateFormatter.doesRelativeDateFormatting = YES;
|
||||
return [dateFormatter stringFromDate:_date];
|
||||
}
|
||||
|
||||
#pragma mark - NSObject
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (self == object) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (![object isKindOfClass:[self class]]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
OWSCall *aCall = (OWSCall *)object;
|
||||
|
||||
return [self.senderId isEqualToString:aCall.senderId] &&
|
||||
[self.senderDisplayName isEqualToString:aCall.senderDisplayName]
|
||||
&& ([self.date compare:aCall.date] == NSOrderedSame) && self.status == aCall.status;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return self.senderId.hash ^ self.date.hash;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
return [NSString stringWithFormat:@"<%@: senderId=%@, senderDisplayName=%@, date=%@>",
|
||||
[self class],
|
||||
self.senderId,
|
||||
self.senderDisplayName,
|
||||
self.date];
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageEditing
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action
|
||||
{
|
||||
return action == @selector(delete:);
|
||||
}
|
||||
|
||||
- (void)performEditingAction:(SEL)action
|
||||
{
|
||||
// Deletes are always handled by TSMessageAdapter
|
||||
if (action == @selector(delete:)) {
|
||||
DDLogDebug(@"%@ Deleting interaction with uniqueId: %@", self.tag, self.interaction.uniqueId);
|
||||
[self.interaction remove];
|
||||
return;
|
||||
}
|
||||
|
||||
// Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
DDLogError(@"%@ '%@' action unsupported", self.tag, actionString);
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageData
|
||||
|
||||
- (BOOL)isMediaMessage
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSUInteger)messageHash
|
||||
{
|
||||
return self.interaction.hash;
|
||||
}
|
||||
|
||||
- (NSString *)text
|
||||
{
|
||||
return _detailString;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
//@interface OWSCall ()
|
||||
//
|
||||
//// -- Redeclaring properties from OWSMessageData protocol to synthesize variables
|
||||
//@property (nonatomic) TSMessageAdapterType messageType;
|
||||
//@property (nonatomic) BOOL isExpiringMessage;
|
||||
//@property (nonatomic) BOOL shouldStartExpireTimer;
|
||||
//@property (nonatomic) double expiresAtSeconds;
|
||||
//@property (nonatomic) uint32_t expiresInSeconds;
|
||||
//@property (nonatomic) TSInteraction *interaction;
|
||||
//
|
||||
//@end
|
||||
//
|
||||
//@implementation OWSCall
|
||||
//
|
||||
//#pragma mark - Initialzation
|
||||
//
|
||||
//- (instancetype)initWithCallRecord:(TSCall *)callRecord
|
||||
//{
|
||||
// TSThread *thread = callRecord.thread;
|
||||
// TSContactThread *contactThread;
|
||||
// if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
// contactThread = (TSContactThread *)thread;
|
||||
// } else {
|
||||
// DDLogError(@"%@ Unexpected thread type: %@", self.tag, thread);
|
||||
// }
|
||||
//
|
||||
// CallStatus status = 0;
|
||||
// switch (callRecord.callType) {
|
||||
// case RPRecentCallTypeOutgoing:
|
||||
// status = kCallOutgoing;
|
||||
// break;
|
||||
// case RPRecentCallTypeOutgoingIncomplete:
|
||||
// status = kCallOutgoingIncomplete;
|
||||
// break;
|
||||
// case RPRecentCallTypeMissed:
|
||||
// status = kCallMissed;
|
||||
// break;
|
||||
// case RPRecentCallTypeIncoming:
|
||||
// status = kCallIncoming;
|
||||
// break;
|
||||
// case RPRecentCallTypeIncomingIncomplete:
|
||||
// status = kCallIncomingIncomplete;
|
||||
// break;
|
||||
// case RPRecentCallTypeMissedBecauseOfChangedIdentity:
|
||||
// status = kCallMissedBecauseOfChangedIdentity;
|
||||
// break;
|
||||
// default:
|
||||
// status = kCallIncoming;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// NSString *name = contactThread.name;
|
||||
// NSString *detailString;
|
||||
// switch (status) {
|
||||
// case kCallMissed:
|
||||
// detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_MISSED_CALL_WITH_NAME", nil), name];
|
||||
// break;
|
||||
// case kCallIncoming:
|
||||
// detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_RECEIVED_CALL", nil), name];
|
||||
// break;
|
||||
// case kCallIncomingIncomplete:
|
||||
// detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_THEY_TRIED_TO_CALL_YOU", nil),
|
||||
// name]; break;
|
||||
// case kCallOutgoing:
|
||||
// detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_YOU_CALLED", nil), name];
|
||||
// break;
|
||||
// case kCallOutgoingIncomplete:
|
||||
// detailString = [NSString stringWithFormat:NSLocalizedString(@"MSGVIEW_YOU_TRIED_TO_CALL", nil), name];
|
||||
// break;
|
||||
// case kCallMissedBecauseOfChangedIdentity:
|
||||
// detailString = [NSString
|
||||
// stringWithFormat:NSLocalizedString(@"MSGVIEW_MISSED_CALL_BECAUSE_OF_CHANGED_IDENTITY", nil), name];
|
||||
// default:
|
||||
// detailString = @"";
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// return [self initWithInteraction:callRecord
|
||||
// callerId:contactThread.contactIdentifier
|
||||
// callerDisplayName:name
|
||||
// date:callRecord.dateForSorting
|
||||
// status:status
|
||||
// displayString:detailString];
|
||||
//}
|
||||
//
|
||||
//- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
// callerId:(NSString *)senderId
|
||||
// callerDisplayName:(NSString *)senderDisplayName
|
||||
// date:(nullable NSDate *)date
|
||||
// status:(CallStatus)status
|
||||
// displayString:(NSString *)detailString
|
||||
//{
|
||||
// NSParameterAssert(senderId != nil);
|
||||
// NSParameterAssert(senderDisplayName != nil);
|
||||
//
|
||||
// self = [super init];
|
||||
// if (!self) {
|
||||
// return self;
|
||||
// }
|
||||
//
|
||||
// _interaction = interaction;
|
||||
// _senderId = [senderId copy];
|
||||
// _senderDisplayName = [senderDisplayName copy];
|
||||
// _date = [date copy];
|
||||
// _status = status;
|
||||
// _isExpiringMessage = NO; // TODO - call notifications should expire too.
|
||||
// _shouldStartExpireTimer = NO; // TODO - call notifications should expire too.
|
||||
// _messageType = TSCallAdapter;
|
||||
//
|
||||
// // TODO interpret detailString from status. make sure it works for calls and
|
||||
// // our re-use of calls as group update display
|
||||
// _detailString = [detailString stringByAppendingFormat:@" "];
|
||||
//
|
||||
// return self;
|
||||
//}
|
||||
//
|
||||
//- (NSString *)dateText
|
||||
//{
|
||||
// NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
// dateFormatter.timeStyle = NSDateFormatterShortStyle;
|
||||
// dateFormatter.dateStyle = NSDateFormatterMediumStyle;
|
||||
// dateFormatter.doesRelativeDateFormatting = YES;
|
||||
// return [dateFormatter stringFromDate:_date];
|
||||
//}
|
||||
//
|
||||
//#pragma mark - NSObject
|
||||
//
|
||||
//- (BOOL)isEqual:(id)object
|
||||
//{
|
||||
// if (self == object) {
|
||||
// return YES;
|
||||
// }
|
||||
//
|
||||
// if (![object isKindOfClass:[self class]]) {
|
||||
// return NO;
|
||||
// }
|
||||
//
|
||||
// OWSCall *aCall = (OWSCall *)object;
|
||||
//
|
||||
// return [self.senderId isEqualToString:aCall.senderId] &&
|
||||
// [self.senderDisplayName isEqualToString:aCall.senderDisplayName]
|
||||
// && ([self.date compare:aCall.date] == NSOrderedSame) && self.status == aCall.status;
|
||||
//}
|
||||
//
|
||||
//- (NSUInteger)hash
|
||||
//{
|
||||
// return self.senderId.hash ^ self.date.hash;
|
||||
//}
|
||||
//
|
||||
//- (NSString *)description
|
||||
//{
|
||||
// return [NSString stringWithFormat:@"<%@: senderId=%@, senderDisplayName=%@, date=%@>",
|
||||
// [self class],
|
||||
// self.senderId,
|
||||
// self.senderDisplayName,
|
||||
// self.date];
|
||||
//}
|
||||
//
|
||||
//#pragma mark - OWSMessageEditing
|
||||
//
|
||||
//- (BOOL)canPerformEditingAction:(SEL)action
|
||||
//{
|
||||
// return action == @selector(delete:);
|
||||
//}
|
||||
//
|
||||
//- (void)performEditingAction:(SEL)action
|
||||
//{
|
||||
// // Deletes are always handled by TSMessageAdapter
|
||||
// if (action == @selector(delete:)) {
|
||||
// DDLogDebug(@"%@ Deleting interaction with uniqueId: %@", self.tag, self.interaction.uniqueId);
|
||||
// [self.interaction remove];
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
// NSString *actionString = NSStringFromSelector(action);
|
||||
// DDLogError(@"%@ '%@' action unsupported", self.tag, actionString);
|
||||
//}
|
||||
//
|
||||
//#pragma mark - JSQMessageData
|
||||
//
|
||||
//- (BOOL)isMediaMessage
|
||||
//{
|
||||
// return NO;
|
||||
//}
|
||||
//
|
||||
//- (NSUInteger)messageHash
|
||||
//{
|
||||
// return self.interaction.hash;
|
||||
//}
|
||||
//
|
||||
//- (NSString *)text
|
||||
//{
|
||||
// return _detailString;
|
||||
//}
|
||||
//
|
||||
//#pragma mark - Logging
|
||||
//
|
||||
//+ (NSString *)tag
|
||||
//{
|
||||
// return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
//}
|
||||
//
|
||||
//- (NSString *)tag
|
||||
//{
|
||||
// return self.class.tag;
|
||||
//}
|
||||
//
|
||||
//@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -47,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (OWSInteractionType)interactionType
|
||||
{
|
||||
return OWSInteractionType_Offer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (c) 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesBubblesSizeCalculator.h>
|
||||
|
||||
NS_SWIFT_NAME(MessagesBubblesSizeCalculator)
|
||||
|
||||
@interface OWSMessagesBubblesSizeCalculator : JSQMessagesBubblesSizeCalculator
|
||||
|
||||
- (CGSize)messageBubbleSizeForMessageData:(id<JSQMessageData>)messageData
|
||||
atIndexPath:(NSIndexPath *)indexPath
|
||||
withLayout:(JSQMessagesCollectionViewFlowLayout *)layout;
|
||||
|
||||
@end
|
|
@ -1,253 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessagesBubblesSizeCalculator.h"
|
||||
#import "OWSCall.h"
|
||||
#import "OWSContactOffersCell.h"
|
||||
#import "OWSContactOffersInteraction.h"
|
||||
#import "OWSSystemMessageCell.h"
|
||||
#import "OWSUnreadIndicatorCell.h"
|
||||
#import "TSGenericAttachmentAdapter.h"
|
||||
#import "TSMessageAdapter.h"
|
||||
#import "TSUnreadIndicatorInteraction.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "tgmath.h" // generic math allows fmax to handle CGFLoat correctly on 32 & 64bit.
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionView.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewFlowLayout.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* We use some private method to size our info messages.
|
||||
*/
|
||||
@interface OWSMessagesBubblesSizeCalculator (JSQPrivateMethods)
|
||||
|
||||
@property (strong, nonatomic, readonly) NSCache *cache;
|
||||
@property (assign, nonatomic, readonly) NSUInteger minimumBubbleWidth;
|
||||
@property (assign, nonatomic, readonly) BOOL usesFixedWidthBubbles;
|
||||
@property (assign, nonatomic, readonly) NSInteger additionalInset;
|
||||
@property (assign, nonatomic) CGFloat layoutWidthForFixedWidthBubbles;
|
||||
|
||||
- (CGSize)jsq_avatarSizeForMessageData:(id<JSQMessageData>)messageData
|
||||
withLayout:(JSQMessagesCollectionViewFlowLayout *)layout;
|
||||
- (CGFloat)textBubbleWidthForLayout:(JSQMessagesCollectionViewFlowLayout *)layout;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessagesBubblesSizeCalculator ()
|
||||
|
||||
@property (nonatomic, readonly) OWSSystemMessageCell *referenceSystemMessageCell;
|
||||
@property (nonatomic, readonly) OWSUnreadIndicatorCell *referenceUnreadIndicatorCell;
|
||||
@property (nonatomic, readonly) OWSContactOffersCell *referenceContactOffersCell;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessagesBubblesSizeCalculator
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_referenceSystemMessageCell = [OWSSystemMessageCell new];
|
||||
_referenceUnreadIndicatorCell = [OWSUnreadIndicatorCell new];
|
||||
_referenceContactOffersCell = [OWSContactOffersCell new];
|
||||
|
||||
// Calculating message size is relatively expensive, so unbound the size of the cache.
|
||||
self.cache.countLimit = 0;
|
||||
self.cache.totalCostLimit = 0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the size of the `messageBubbleImageView` property
|
||||
* of a `JSQMessagesCollectionViewCell` for the specified messageData at indexPath.
|
||||
*
|
||||
* @param messageData A message data object.
|
||||
* @param indexPath The index path at which messageData is located.
|
||||
* @param layout The layout object asking for this information.
|
||||
*
|
||||
* @return A sizes that specifies the required dimensions to display the entire message contents.
|
||||
* Note, this is *not* the entire cell, but only its message bubble.
|
||||
*/
|
||||
- (CGSize)messageBubbleSizeForMessageData:(id<JSQMessageData>)messageData
|
||||
atIndexPath:(NSIndexPath *)indexPath
|
||||
withLayout:(JSQMessagesCollectionViewFlowLayout *)layout
|
||||
{
|
||||
id cacheKey = [self cacheKeyForMessageData:messageData];
|
||||
NSValue *cachedSize = [self.cache objectForKey:cacheKey];
|
||||
if (cachedSize != nil) {
|
||||
return [cachedSize CGSizeValue];
|
||||
}
|
||||
|
||||
CGSize result = [self calculateMessageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout];
|
||||
[self.cache setObject:[NSValue valueWithCGSize:result] forKey:cacheKey];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (CGSize)calculateMessageBubbleSizeForMessageData:(id<JSQMessageData>)messageData
|
||||
atIndexPath:(NSIndexPath *)indexPath
|
||||
withLayout:(JSQMessagesCollectionViewFlowLayout *)layout
|
||||
{
|
||||
if ([messageData isKindOfClass:[TSMessageAdapter class]]) {
|
||||
TSMessageAdapter *message = (TSMessageAdapter *)messageData;
|
||||
|
||||
switch (message.messageType) {
|
||||
case TSCallAdapter:
|
||||
case TSInfoMessageAdapter:
|
||||
case TSErrorMessageAdapter: {
|
||||
TSInteraction *interaction = ((TSMessageAdapter *)messageData).interaction;
|
||||
return [self sizeForSystemMessage:interaction layout:layout];
|
||||
}
|
||||
case TSUnreadIndicatorAdapter: {
|
||||
TSUnreadIndicatorInteraction *interaction
|
||||
= (TSUnreadIndicatorInteraction *)((TSMessageAdapter *)messageData).interaction;
|
||||
return [self sizeForUnreadIndicator:interaction layout:layout];
|
||||
}
|
||||
case OWSContactOffersAdapter: {
|
||||
OWSContactOffersInteraction *interaction
|
||||
= (OWSContactOffersInteraction *)((TSMessageAdapter *)messageData).interaction;
|
||||
return [self sizeForContactOffers:interaction layout:layout];
|
||||
}
|
||||
case TSIncomingMessageAdapter:
|
||||
case TSOutgoingMessageAdapter:
|
||||
break;
|
||||
default:
|
||||
OWSFail(@"Unknown sizing interaction: %@", [((TSMessageAdapter *)messageData).interaction class]);
|
||||
break;
|
||||
}
|
||||
} else if ([messageData isKindOfClass:[OWSCall class]]) {
|
||||
TSInteraction *interaction = ((OWSCall *)messageData).interaction;
|
||||
return [self sizeForSystemMessage:interaction layout:layout];
|
||||
} else {
|
||||
OWSFail(@"Can't size unknown message data type: %@", [messageData class]);
|
||||
}
|
||||
|
||||
// BEGIN HACK iOS10EmojiBug see: https://github.com/WhisperSystems/Signal-iOS/issues/1368
|
||||
if ([self shouldApplyiOS10EmojiFixToString:messageData.text font:layout.messageBubbleFont]) {
|
||||
return [self withiOS10EmojiFixSuperMessageBubbleSizeForMessageData:messageData
|
||||
atIndexPath:indexPath
|
||||
withLayout:layout];
|
||||
} else {
|
||||
// END HACK iOS10EmojiBug see: https://github.com/WhisperSystems/Signal-iOS/issues/1368
|
||||
|
||||
return [super messageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)sizeForSystemMessage:(TSInteraction *)interaction
|
||||
layout:(JSQMessagesCollectionViewFlowLayout *)layout
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
OWSAssert(interaction);
|
||||
|
||||
return [self.referenceSystemMessageCell bubbleSizeForInteraction:interaction
|
||||
collectionViewWidth:layout.collectionView.width];
|
||||
}
|
||||
|
||||
- (CGSize)sizeForUnreadIndicator:(TSUnreadIndicatorInteraction *)interaction
|
||||
layout:(JSQMessagesCollectionViewFlowLayout *)layout
|
||||
{
|
||||
OWSAssert(interaction);
|
||||
|
||||
return [self.referenceUnreadIndicatorCell bubbleSizeForInteraction:interaction
|
||||
collectionViewWidth:layout.collectionView.width];
|
||||
}
|
||||
|
||||
- (CGSize)sizeForContactOffers:(OWSContactOffersInteraction *)interaction
|
||||
layout:(JSQMessagesCollectionViewFlowLayout *)layout
|
||||
{
|
||||
OWSAssert(interaction);
|
||||
|
||||
return [self.referenceContactOffersCell bubbleSizeForInteraction:interaction
|
||||
collectionViewWidth:layout.collectionView.width];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji sizing bug only affects iOS10. Unfortunately the "fix" for emoji font breaks some other fonts, so it's
|
||||
* important
|
||||
* to only apply it when emoji is actually present.
|
||||
*/
|
||||
- (BOOL)shouldApplyiOS10EmojiFixToString:(NSString *)string font:(UIFont *)font
|
||||
{
|
||||
if (!string) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL isIOS10OrGreater =
|
||||
[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 10 }];
|
||||
if (!isIOS10OrGreater) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
__block BOOL foundEmoji = NO;
|
||||
NSDictionary *attributes = @{ NSFontAttributeName : font };
|
||||
|
||||
NSMutableAttributedString *attributedString =
|
||||
[[NSMutableAttributedString alloc] initWithString:string attributes:attributes];
|
||||
[attributedString fixAttributesInRange:NSMakeRange(0, string.length)];
|
||||
[attributedString enumerateAttribute:NSFontAttributeName
|
||||
inRange:NSMakeRange(0, string.length)
|
||||
options:0
|
||||
usingBlock:^(id _Nullable value, NSRange range, BOOL *_Nonnull stop) {
|
||||
UIFont *rangeFont = (UIFont *)value;
|
||||
if ([rangeFont.fontName isEqualToString:@".AppleColorEmojiUI"]) {
|
||||
DDLogVerbose(@"Detected Emoji at location: %lu, for length: %lu",
|
||||
(unsigned long)range.location,
|
||||
(unsigned long)range.length);
|
||||
foundEmoji = YES;
|
||||
*stop = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
return foundEmoji;
|
||||
}
|
||||
|
||||
/**
|
||||
* HACK iOS10EmojiBug see: https://github.com/WhisperSystems/Signal-iOS/issues/1368
|
||||
* As of iOS10.0 the UIEmoji font doesn't present proper line heights. In some cases this causes the last line in a
|
||||
* message to get cropped off.
|
||||
*/
|
||||
- (CGSize)withiOS10EmojiFixSuperMessageBubbleSizeForMessageData:(id<JSQMessageData>)messageData
|
||||
atIndexPath:(NSIndexPath *)indexPath
|
||||
withLayout:(JSQMessagesCollectionViewFlowLayout *)layout
|
||||
{
|
||||
UIFont *emojiFont = [UIFont fontWithName:@".AppleColorEmojiUI" size:layout.messageBubbleFont.pointSize];
|
||||
CGSize superSize = [super messageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout];
|
||||
int lines = (int)floor(superSize.height / emojiFont.lineHeight);
|
||||
|
||||
// Add an extra pixel per line to fit the emoji.
|
||||
// This is a crappy solution. Long messages with only one line of emoji will have an extra pixel per line.
|
||||
return CGSizeMake(superSize.width, superSize.height + (CGFloat)1.5 * lines);
|
||||
}
|
||||
|
||||
- (id)cacheKeyForMessageData:(id<JSQMessageData>)messageData
|
||||
{
|
||||
OWSAssert(messageData);
|
||||
OWSAssert([messageData conformsToProtocol:@protocol(OWSMessageData)]);
|
||||
OWSAssert(((id<OWSMessageData>)messageData).interaction);
|
||||
OWSAssert(((id<OWSMessageData>)messageData).interaction.uniqueId);
|
||||
|
||||
return @([messageData messageHash]);
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,113 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
* Represents a download-in-progress
|
||||
*/
|
||||
class AttachmentPointerAdapter: JSQMediaItem, OWSMessageEditing {
|
||||
|
||||
let TAG = "[AttachmentPointerAdapter]"
|
||||
let isIncoming: Bool
|
||||
let attachmentPointer: TSAttachmentPointer
|
||||
var cachedView: UIView?
|
||||
var attachmentPointerView: AttachmentPointerView?
|
||||
|
||||
required init(attachmentPointer: TSAttachmentPointer, isIncoming: Bool) {
|
||||
self.isIncoming = isIncoming
|
||||
self.attachmentPointer = attachmentPointer
|
||||
super.init(maskAsOutgoing: !isIncoming)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
owsFail("init(coder:) has not been implemented")
|
||||
self.isIncoming = true
|
||||
self.attachmentPointer = TSAttachmentPointer()
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
func canPerformAction(_ action: Selector) -> Bool {
|
||||
// No actions can be performed on a downloading attachment.
|
||||
return false
|
||||
}
|
||||
|
||||
func performAction(_ action: Selector) {
|
||||
// Should not get here, as you can't perform any actions on a downloading attachment.
|
||||
owsFail("\(TAG) unexpectedly trying to perform action: \(action) on downloading attachment.")
|
||||
}
|
||||
|
||||
// MARK: JSQ Overrides
|
||||
|
||||
override func mediaHash() -> UInt {
|
||||
// In objc, `hash` returns NSUInteger, but in Swift it return an Int.
|
||||
assert(self.attachmentPointer.uniqueId != nil)
|
||||
return UInt(bitPattern: self.attachmentPointer.uniqueId.hash)
|
||||
}
|
||||
|
||||
override func mediaViewDisplaySize() -> CGSize {
|
||||
return CGSize(width: 200, height: 90)
|
||||
}
|
||||
|
||||
override func mediaView() -> UIView? {
|
||||
guard self.cachedView == nil else {
|
||||
return self.cachedView
|
||||
}
|
||||
|
||||
let frame = CGRect(origin: CGPoint.zero, size: self.mediaViewDisplaySize())
|
||||
let view = UIView(frame: frame)
|
||||
self.cachedView = view
|
||||
|
||||
JSQMessagesMediaViewBubbleImageMasker.applyBubbleImageMask(toMediaView: view, isOutgoing:!isIncoming)
|
||||
|
||||
view.isUserInteractionEnabled = false
|
||||
view.clipsToBounds = true
|
||||
|
||||
let attachmentPointerView = AttachmentPointerView(attachmentPointer: attachmentPointer, isIncoming: self.isIncoming)
|
||||
self.attachmentPointerView = attachmentPointerView
|
||||
view.addSubview(attachmentPointerView)
|
||||
|
||||
attachmentPointerView.autoPinWidthToSuperview(withMargin: 20)
|
||||
attachmentPointerView.autoVCenterInSuperview()
|
||||
|
||||
switch attachmentPointer.state {
|
||||
case .downloading:
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(attachmentDownloadProgress), name: NSNotification.Name.attachmentDownloadProgress, object: nil)
|
||||
view.backgroundColor = isIncoming ? UIColor.jsq_messageBubbleLightGray() : UIColor.ows_fadedBlue()
|
||||
case .enqueued:
|
||||
view.backgroundColor = isIncoming ? UIColor.jsq_messageBubbleLightGray() : UIColor.ows_fadedBlue()
|
||||
case .failed:
|
||||
view.backgroundColor = UIColor.gray
|
||||
}
|
||||
|
||||
return cachedView
|
||||
}
|
||||
|
||||
func attachmentDownloadProgress(_ notification: NSNotification) {
|
||||
guard let attachmentPointerView = self.attachmentPointerView else {
|
||||
owsFail("\(TAG) downloading view was unexpectedly nil for notification: \(notification)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let userInfo = notification.userInfo else {
|
||||
owsFail("\(TAG) user info was unexpectedly nil for notification: \(notification)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let progress = userInfo[kAttachmentDownloadProgressKey] as? CGFloat else {
|
||||
owsFail("\(TAG) missing progress measure for notification user info: \(userInfo)")
|
||||
return
|
||||
}
|
||||
|
||||
guard let attachmentId = userInfo[kAttachmentDownloadAttachmentIDKey] as? String else {
|
||||
owsFail("\(TAG) missing attachmentId for notification user info: \(userInfo)")
|
||||
return
|
||||
}
|
||||
|
||||
if self.attachmentPointer.uniqueId == attachmentId {
|
||||
attachmentPointerView.progress = progress
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMediaItem.h>
|
||||
|
||||
@interface JSQMediaItem (OWS)
|
||||
|
||||
- (CGFloat)ows_maxMediaBubbleWidth:(CGSize)defaultBubbleSize;
|
||||
|
||||
- (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImage:(UIImage *)image;
|
||||
|
||||
- (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImageSize:(CGSize)imageSize;
|
||||
|
||||
- (CGSize)sizeOfImageAtURL:(NSURL *)imageURL;
|
||||
|
||||
@end
|
|
@ -1,88 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "JSQMediaItem+OWS.h"
|
||||
#import "NumberUtil.h"
|
||||
#import "UIDevice+TSHardwareVersion.h"
|
||||
#import <ImageIO/ImageIO.h>
|
||||
#import <SignalServiceKit/NSData+Image.h>
|
||||
|
||||
@implementation JSQMediaItem (OWS)
|
||||
|
||||
- (CGFloat)ows_maxMediaBubbleWidth:(CGSize)defaultBubbleSize
|
||||
{
|
||||
return (
|
||||
[[UIDevice currentDevice] isiPhoneVersionSixOrMore] ? defaultBubbleSize.width * 1.2 : defaultBubbleSize.width);
|
||||
}
|
||||
|
||||
- (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImage:(UIImage *)image {
|
||||
return [self ows_adjustBubbleSize:bubbleSize forImageSize:image.size];
|
||||
}
|
||||
|
||||
- (CGSize)ows_adjustBubbleSize:(CGSize)bubbleSize forImageSize:(CGSize)imageSize
|
||||
{
|
||||
double aspectRatio = imageSize.height / imageSize.width;
|
||||
double clampedAspectRatio = [NumberUtil clamp:aspectRatio toMin:0.5 andMax:1.5];
|
||||
|
||||
if ([[UIDevice currentDevice] isiPhoneVersionSixOrMore]) {
|
||||
bubbleSize.width = [self ows_maxMediaBubbleWidth:bubbleSize];
|
||||
bubbleSize.height = (CGFloat)(bubbleSize.width * clampedAspectRatio);
|
||||
} else {
|
||||
if (aspectRatio > 1) {
|
||||
bubbleSize.height = bubbleSize.width;
|
||||
bubbleSize.width = (CGFloat)(bubbleSize.height / clampedAspectRatio);
|
||||
} else {
|
||||
bubbleSize.height = (CGFloat)(bubbleSize.width * clampedAspectRatio);
|
||||
}
|
||||
}
|
||||
return bubbleSize;
|
||||
}
|
||||
|
||||
- (CGSize)sizeOfImageAtURL:(NSURL *)imageURL
|
||||
{
|
||||
OWSAssert(imageURL);
|
||||
|
||||
if (![NSData ows_isValidImageAtPath:imageURL.path]) {
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
// With CGImageSource we avoid loading the whole image into memory.
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)imageURL, NULL);
|
||||
if (!source) {
|
||||
OWSFail(@"%@ Could not load image: %@", self.tag, imageURL);
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
NSDictionary *options = @{
|
||||
(NSString *)kCGImageSourceShouldCache : @(NO),
|
||||
};
|
||||
NSDictionary *properties
|
||||
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, (CFDictionaryRef)options);
|
||||
CGSize imageSize = CGSizeZero;
|
||||
if (properties) {
|
||||
NSNumber *width = properties[(NSString *)kCGImagePropertyPixelWidth];
|
||||
NSNumber *height = properties[(NSString *)kCGImagePropertyPixelHeight];
|
||||
if (width && height) {
|
||||
imageSize = CGSizeMake(width.floatValue, height.floatValue);
|
||||
} else {
|
||||
OWSFail(@"%@ Could not determine size of image: %@", self.tag, imageURL);
|
||||
}
|
||||
}
|
||||
CFRelease(source);
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageEditing.h"
|
||||
#import <JSQMessagesViewController/JSQMessageData.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, TSMessageAdapterType) {
|
||||
TSIncomingMessageAdapter,
|
||||
TSOutgoingMessageAdapter,
|
||||
TSCallAdapter,
|
||||
TSInfoMessageAdapter,
|
||||
TSErrorMessageAdapter,
|
||||
TSMediaAttachmentAdapter,
|
||||
TSGenericTextMessageAdapter, // Used when message direction is unknown (outgoing or incoming)
|
||||
TSUnreadIndicatorAdapter,
|
||||
OWSContactOffersAdapter,
|
||||
};
|
||||
|
||||
@protocol OWSMessageData <JSQMessageData, OWSMessageEditing>
|
||||
|
||||
@property (nonatomic, readonly) TSMessageAdapterType messageType;
|
||||
@property (nonatomic, readonly) TSInteraction *interaction;
|
||||
@property (nonatomic, readonly) BOOL isExpiringMessage;
|
||||
@property (nonatomic, readonly) BOOL shouldStartExpireTimer;
|
||||
@property (nonatomic, readonly) double expiresAtSeconds;
|
||||
@property (nonatomic, readonly) uint32_t expiresInSeconds;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
// Created by Michael Kirk on 3/11/16.
|
||||
|
||||
@class TSInteraction;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol OWSMessageEditing <NSObject>
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action;
|
||||
- (void)performEditingAction:(SEL)action;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol OWSMessageMediaAdapter
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible;
|
||||
|
||||
// Cells will request that this adapter clear its cached media views,
|
||||
// but the adapter should only honor requests from the last cell to
|
||||
// use its views.
|
||||
- (void)setLastPresentingCell:(nullable id)cell;
|
||||
- (void)clearCachedMediaViewsIfLastPresentingCell:(id)cell;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageEditing.h"
|
||||
#import "OWSMessageMediaAdapter.h"
|
||||
#import <JSQMessagesViewController/JSQPhotoMediaItem.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface TSAnimatedAdapter : JSQMediaItem <OWSMessageEditing, OWSMessageMediaAdapter>
|
||||
|
||||
@property (nonatomic, readonly) NSString *attachmentId;
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,196 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSAnimatedAdapter.h"
|
||||
#import "AttachmentUploadView.h"
|
||||
#import "JSQMediaItem+OWS.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesMediaViewBubbleImageMasker.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <SignalServiceKit/MIMETypeUtil.h>
|
||||
#import <SignalServiceKit/NSData+Image.h>
|
||||
#import <YYImage/YYImage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TSAnimatedAdapter ()
|
||||
|
||||
@property (nonatomic, nullable) YYAnimatedImageView *cachedImageView;
|
||||
@property (nonatomic) TSAttachmentStream *attachment;
|
||||
@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView;
|
||||
@property (nonatomic) BOOL incoming;
|
||||
@property (nonatomic) CGSize imageSize;
|
||||
@property (nonatomic) NSString *attachmentId;
|
||||
|
||||
// See comments on OWSMessageMediaAdapter.
|
||||
@property (nonatomic, nullable, weak) id lastPresentingCell;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation TSAnimatedAdapter
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_cachedImageView = nil;
|
||||
_attachment = attachment;
|
||||
_attachmentId = attachment.uniqueId;
|
||||
_incoming = incoming;
|
||||
_imageSize = [attachment imageSizeWithoutTransaction];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)clearAllViews
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[_cachedImageView removeFromSuperview];
|
||||
_cachedImageView = nil;
|
||||
_attachmentUploadView = nil;
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViews {
|
||||
[super clearCachedMediaViews];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing {
|
||||
[super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
#pragma mark - NSObject
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return super.hash ^ self.attachment.uniqueId.hash;
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageMediaAdapter
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible
|
||||
{
|
||||
if (isVisible) {
|
||||
[self.cachedImageView startAnimating];
|
||||
} else {
|
||||
[self.cachedImageView stopAnimating];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViewsIfLastPresentingCell:(id)cell
|
||||
{
|
||||
OWSAssert(cell);
|
||||
|
||||
if (cell == self.lastPresentingCell) {
|
||||
[self clearCachedMediaViews];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageMediaData protocol
|
||||
|
||||
- (UIView *)mediaView {
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
if (self.cachedImageView == nil) {
|
||||
NSString *_Nullable filePath = [self.attachment filePath];
|
||||
if (![NSData ows_isValidImageAtPath:filePath]) {
|
||||
return nil;
|
||||
}
|
||||
YYImage *_Nullable animatedGif = filePath ? [YYImage imageWithContentsOfFile:filePath] : nil;
|
||||
if (!animatedGif) {
|
||||
DDLogError(@"%@ Could not load image: %@", [self tag], filePath);
|
||||
UIView *view = [UIView new];
|
||||
view.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
|
||||
return view;
|
||||
}
|
||||
YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] init];
|
||||
imageView.image = animatedGif;
|
||||
CGSize size = [self mediaViewDisplaySize];
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
imageView.frame = CGRectMake(0.0, 0.0, size.width, size.height);
|
||||
imageView.clipsToBounds = YES;
|
||||
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:imageView
|
||||
isOutgoing:self.appliesMediaViewMaskAsOutgoing];
|
||||
self.cachedImageView = imageView;
|
||||
|
||||
if (!self.incoming) {
|
||||
self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachment
|
||||
superview:imageView
|
||||
attachmentStateCallback:nil];
|
||||
}
|
||||
}
|
||||
|
||||
return self.cachedImageView;
|
||||
}
|
||||
|
||||
- (CGSize)mediaViewDisplaySize {
|
||||
return [self ows_adjustBubbleSize:[super mediaViewDisplaySize] forImageSize:self.imageSize];
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageEditing Protocol
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action
|
||||
{
|
||||
return (action == @selector(copy:) || action == NSSelectorFromString(@"save:"));
|
||||
}
|
||||
|
||||
- (void)performEditingAction:(SEL)action
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.attachment.contentType];
|
||||
if (!utiType) {
|
||||
OWSFail(@"%@ Unknown MIME type: %@", self.tag, self.attachment.contentType);
|
||||
utiType = (NSString *)kUTTypeGIF;
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachment mediaURL]];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
} else if (action == NSSelectorFromString(@"save:")) {
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachment mediaURL]];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
|
||||
[library writeImageDataToSavedPhotosAlbum:data
|
||||
metadata:nil
|
||||
completionBlock:^(NSURL *assetURL, NSError *error) {
|
||||
if (error) {
|
||||
DDLogWarn(@"Error Saving image to photo album: %@", error);
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
// Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
DDLogError(@"'%@' action unsupported for %@: attachmentId=%@", actionString, [self class], self.attachmentId);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,17 +0,0 @@
|
|||
//
|
||||
// TSContentAdapters.h
|
||||
// Signal
|
||||
//
|
||||
// Created by Frederic Jacobs on 05/12/15.
|
||||
// Copyright © 2015 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef TSContentAdapters_h
|
||||
#define TSContentAdapters_h
|
||||
|
||||
#import "TSAnimatedAdapter.h"
|
||||
#import "TSMessageAdapter.h"
|
||||
#import "TSPhotoAdapter.h"
|
||||
#import "TSVideoAttachmentAdapter.h"
|
||||
|
||||
#endif /* TSContentAdapters_h */
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageEditing.h"
|
||||
#import "OWSMessageMediaAdapter.h"
|
||||
#import <JSQMessagesViewController/JSQMediaItem.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface TSGenericAttachmentAdapter : JSQMediaItem <OWSMessageEditing, OWSMessageMediaAdapter>
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,311 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSGenericAttachmentAdapter.h"
|
||||
#import "AttachmentUploadView.h"
|
||||
#import "JSQMediaItem+OWS.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "UIColor+JSQMessages.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <JSQMessagesViewController/JSQMessagesMediaViewBubbleImageMasker.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <SignalServiceKit/MimeTypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TSGenericAttachmentAdapter ()
|
||||
|
||||
@property (nonatomic, nullable) UIView *cachedMediaView;
|
||||
@property (nonatomic) TSAttachmentStream *attachment;
|
||||
@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView;
|
||||
@property (nonatomic) BOOL incoming;
|
||||
|
||||
// See comments on OWSMessageMediaAdapter.
|
||||
@property (nonatomic, nullable, weak) id lastPresentingCell;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation TSGenericAttachmentAdapter
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_attachment = attachment;
|
||||
_incoming = incoming;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)attachmentId
|
||||
{
|
||||
return self.attachment.uniqueId;
|
||||
}
|
||||
|
||||
- (void)clearAllViews
|
||||
{
|
||||
[_cachedMediaView removeFromSuperview];
|
||||
_cachedMediaView = nil;
|
||||
_attachmentUploadView = nil;
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViews
|
||||
{
|
||||
[super clearCachedMediaViews];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing
|
||||
{
|
||||
[super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
// TODO: Should we override hash or mediaHash?
|
||||
- (NSUInteger)mediaHash
|
||||
{
|
||||
return [self.attachment.uniqueId hash];
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageMediaData protocol
|
||||
|
||||
- (CGFloat)iconHMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconHSpacing
|
||||
{
|
||||
return 10.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconVMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.iconSize + self.iconVMargin * 2;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return 40.f;
|
||||
}
|
||||
|
||||
- (CGFloat)vMargin
|
||||
{
|
||||
return 10.f;
|
||||
}
|
||||
|
||||
- (UIColor *)bubbleBackgroundColor
|
||||
{
|
||||
return self.incoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_materialBlueColor];
|
||||
}
|
||||
|
||||
- (UIColor *)textColor
|
||||
{
|
||||
return (self.incoming ? [UIColor colorWithWhite:0.2f alpha:1.f] : [UIColor whiteColor]);
|
||||
}
|
||||
|
||||
- (UIColor *)foregroundColorWithOpacity:(CGFloat)alpha
|
||||
{
|
||||
return [self.textColor blendWithColor:self.bubbleBackgroundColor alpha:alpha];
|
||||
}
|
||||
|
||||
- (UIView *)mediaView
|
||||
{
|
||||
if (_cachedMediaView == nil) {
|
||||
CGSize viewSize = [self mediaViewDisplaySize];
|
||||
UIColor *textColor = (self.incoming ? [UIColor colorWithWhite:0.2 alpha:1.f] : [UIColor whiteColor]);
|
||||
|
||||
_cachedMediaView = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, viewSize.width, viewSize.height)];
|
||||
|
||||
_cachedMediaView.backgroundColor = self.bubbleBackgroundColor;
|
||||
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:_cachedMediaView
|
||||
isOutgoing:!self.incoming];
|
||||
|
||||
const CGFloat kBubbleTailWidth = 6.f;
|
||||
CGRect contentFrame = CGRectMake(self.incoming ? kBubbleTailWidth : 0.f,
|
||||
self.vMargin,
|
||||
viewSize.width - kBubbleTailWidth - self.iconHMargin,
|
||||
viewSize.height - self.vMargin * 2);
|
||||
|
||||
UIImage *image = [UIImage imageNamed:@"generic-attachment-small"];
|
||||
OWSAssert(image);
|
||||
UIImageView *imageView = [UIImageView new];
|
||||
CGRect iconFrame = CGRectMake(round(contentFrame.origin.x + self.iconHMargin),
|
||||
round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
|
||||
self.iconSize,
|
||||
self.iconSize);
|
||||
imageView.frame = iconFrame;
|
||||
imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
imageView.tintColor = self.bubbleBackgroundColor;
|
||||
imageView.backgroundColor
|
||||
= (self.incoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self foregroundColorWithOpacity:0.15f]);
|
||||
imageView.layer.cornerRadius = MIN(imageView.bounds.size.width, imageView.bounds.size.height) * 0.5f;
|
||||
[_cachedMediaView addSubview:imageView];
|
||||
|
||||
NSString *filename = self.attachment.sourceFilename;
|
||||
if (!filename) {
|
||||
filename = [[self.attachment filePath] lastPathComponent];
|
||||
}
|
||||
NSString *fileExtension = filename.pathExtension;
|
||||
if (fileExtension.length < 1) {
|
||||
[MIMETypeUtil fileExtensionForMIMEType:self.attachment.contentType];
|
||||
}
|
||||
if (fileExtension.length < 1) {
|
||||
fileExtension = NSLocalizedString(@"GENERIC_ATTACHMENT_DEFAULT_TYPE",
|
||||
@"A default label for attachment whose file extension cannot be determined.");
|
||||
}
|
||||
|
||||
UILabel *fileTypeLabel = [UILabel new];
|
||||
fileTypeLabel.text = fileExtension.uppercaseString;
|
||||
fileTypeLabel.textColor = imageView.backgroundColor;
|
||||
fileTypeLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
fileTypeLabel.font = [UIFont ows_mediumFontWithSize:20.f];
|
||||
fileTypeLabel.adjustsFontSizeToFitWidth = YES;
|
||||
fileTypeLabel.textAlignment = NSTextAlignmentCenter;
|
||||
CGRect fileTypeLabelFrame = CGRectZero;
|
||||
fileTypeLabelFrame.size = [fileTypeLabel sizeThatFits:CGSizeZero];
|
||||
// This dimension depends on the space within the icon boundaries.
|
||||
fileTypeLabelFrame.size.width = 15.f;
|
||||
// Center on icon.
|
||||
fileTypeLabelFrame.origin.x
|
||||
= round(iconFrame.origin.x + (iconFrame.size.width - fileTypeLabelFrame.size.width) * 0.5f);
|
||||
fileTypeLabelFrame.origin.y
|
||||
= round(iconFrame.origin.y + (iconFrame.size.height - fileTypeLabelFrame.size.height) * 0.5f);
|
||||
fileTypeLabel.frame = fileTypeLabelFrame;
|
||||
[_cachedMediaView addSubview:fileTypeLabel];
|
||||
|
||||
const CGFloat kLabelHSpacing = self.iconHSpacing;
|
||||
const CGFloat kLabelVSpacing = 2;
|
||||
NSString *topText =
|
||||
[self.attachment.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (topText.length < 1) {
|
||||
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachment.contentType].uppercaseString;
|
||||
}
|
||||
if (topText.length < 1) {
|
||||
topText = NSLocalizedString(@"GENERIC_ATTACHMENT_LABEL", @"A label for generic attachments.");
|
||||
}
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = topText;
|
||||
topLabel.textColor = textColor;
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 15.f)];
|
||||
[topLabel sizeToFit];
|
||||
[_cachedMediaView addSubview:topLabel];
|
||||
|
||||
NSError *error;
|
||||
unsigned long long fileSize =
|
||||
[[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachment filePath] error:&error].fileSize;
|
||||
OWSAssert(!error);
|
||||
NSString *bottomText = [ViewControllerUtils formatFileSize:fileSize];
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
bottomLabel.text = bottomText;
|
||||
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
|
||||
[bottomLabel sizeToFit];
|
||||
[_cachedMediaView addSubview:bottomLabel];
|
||||
|
||||
CGRect topLabelFrame = CGRectZero;
|
||||
topLabelFrame.size = topLabel.bounds.size;
|
||||
topLabelFrame.origin.x = round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
|
||||
topLabelFrame.origin.y = round(contentFrame.origin.y
|
||||
+ (contentFrame.size.height - (topLabel.frame.size.height + bottomLabel.frame.size.height + kLabelVSpacing))
|
||||
* 0.5f);
|
||||
topLabelFrame.size.width = round((contentFrame.origin.x + contentFrame.size.width) - topLabelFrame.origin.x);
|
||||
topLabel.frame = topLabelFrame;
|
||||
|
||||
CGRect bottomLabelFrame = topLabelFrame;
|
||||
bottomLabelFrame.origin.y += topLabelFrame.size.height + kLabelVSpacing;
|
||||
bottomLabel.frame = bottomLabelFrame;
|
||||
|
||||
if (!self.incoming) {
|
||||
self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachment
|
||||
superview:_cachedMediaView
|
||||
attachmentStateCallback:nil];
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedMediaView;
|
||||
}
|
||||
|
||||
- (CGSize)mediaViewDisplaySize
|
||||
{
|
||||
CGSize size = [super mediaViewDisplaySize];
|
||||
size.width = [self ows_maxMediaBubbleWidth:size];
|
||||
size.height = (CGFloat)ceil(self.bubbleHeight);
|
||||
return size;
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageEditing Protocol
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.attachment.contentType];
|
||||
return utiType.length > 0;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)performEditingAction:(SEL)action
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.attachment.contentType];
|
||||
OWSAssert(utiType.length > 0);
|
||||
NSData *data = [NSData dataWithContentsOfURL:self.attachment.mediaURL];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
} else {
|
||||
// Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
DDLogError(@"'%@' action unsupported for %@: attachmentId=%@", actionString, [self class], self.attachmentId);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageMediaAdapter
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible
|
||||
{
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViewsIfLastPresentingCell:(id)cell
|
||||
{
|
||||
OWSAssert(cell);
|
||||
|
||||
if (cell == self.lastPresentingCell) {
|
||||
[self clearCachedMediaViews];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,30 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ContactsManagerProtocol.h"
|
||||
#import "OWSMessageData.h"
|
||||
#import "OWSMessageEditing.h"
|
||||
#import "TSInfoMessage.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSInteraction;
|
||||
@class TSThread;
|
||||
|
||||
#define ME_MESSAGE_IDENTIFIER @"Me";
|
||||
|
||||
@interface TSMessageAdapter : NSObject <OWSMessageData>
|
||||
|
||||
+ (id<OWSMessageData>)messageViewDataWithInteraction:(TSInteraction *)interaction inThread:(TSThread *)thread contactsManager:(id<ContactsManagerProtocol>)contactsManager;
|
||||
|
||||
@property (nonatomic) TSInteraction *interaction;
|
||||
@property (readonly) TSInfoMessageType infoMessageType;
|
||||
@property (nonatomic, readonly) CGFloat mediaViewAlpha;
|
||||
@property (nonatomic, readonly) BOOL isMediaBeingSent;
|
||||
|
||||
+ (SEL)messageMetadataSelector;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,421 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSMessageAdapter.h"
|
||||
#import "AttachmentSharing.h"
|
||||
#import "OWSCall.h"
|
||||
#import "OWSContactOffersInteraction.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "TSAttachmentPointer.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSCall.h"
|
||||
#import "TSContactThread.h"
|
||||
#import "TSContentAdapters.h"
|
||||
#import "TSErrorMessage.h"
|
||||
#import "TSGenericAttachmentAdapter.h"
|
||||
#import "TSGroupThread.h"
|
||||
#import "TSIncomingMessage.h"
|
||||
#import "TSInfoMessage.h"
|
||||
#import "TSOutgoingMessage.h"
|
||||
#import "TSUnreadIndicatorInteraction.h"
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TSMessageAdapter ()
|
||||
|
||||
// ---
|
||||
|
||||
@property (nonatomic, retain) TSContactThread *thread;
|
||||
|
||||
// OR for groups
|
||||
|
||||
@property (nonatomic, copy) NSString *senderId;
|
||||
@property (nonatomic, copy) NSString *senderDisplayName;
|
||||
|
||||
// for InfoMessages
|
||||
|
||||
@property TSInfoMessageType infoMessageType;
|
||||
|
||||
// for ErrorMessages
|
||||
|
||||
@property TSErrorMessageType errorMessageType;
|
||||
|
||||
// for outgoing Messages only
|
||||
|
||||
@property NSInteger outgoingMessageStatus;
|
||||
|
||||
// for MediaMessages
|
||||
|
||||
@property JSQMediaItem<OWSMessageEditing> *mediaItem;
|
||||
|
||||
|
||||
// -- Redeclaring properties from OWSMessageData protocol to synthesize variables
|
||||
@property (nonatomic) TSMessageAdapterType messageType;
|
||||
@property (nonatomic) BOOL isExpiringMessage;
|
||||
@property (nonatomic) BOOL shouldStartExpireTimer;
|
||||
@property (nonatomic) double expiresAtSeconds;
|
||||
@property (nonatomic) uint32_t expiresInSeconds;
|
||||
|
||||
@property (nonatomic) NSString *messageBody;
|
||||
|
||||
@property (nonatomic) NSString *interactionUniqueId;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation TSMessageAdapter
|
||||
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_interaction = interaction;
|
||||
|
||||
self.interactionUniqueId = interaction.uniqueId;
|
||||
|
||||
if ([interaction isKindOfClass:[TSMessage class]]) {
|
||||
TSMessage *message = (TSMessage *)interaction;
|
||||
_isExpiringMessage = message.isExpiringMessage;
|
||||
_expiresAtSeconds = message.expiresAt / 1000.0;
|
||||
_expiresInSeconds = message.expiresInSeconds;
|
||||
_shouldStartExpireTimer = message.shouldStartExpireTimer;
|
||||
} else {
|
||||
_isExpiringMessage = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSCache *)displayableTextCache
|
||||
{
|
||||
static NSCache *cache = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
cache = [NSCache new];
|
||||
// Cache the results for up to 1,000 messages.
|
||||
cache.countLimit = 1000;
|
||||
});
|
||||
return cache;
|
||||
}
|
||||
|
||||
+ (id<OWSMessageData>)messageViewDataWithInteraction:(TSInteraction *)interaction inThread:(TSThread *)thread contactsManager:(id<ContactsManagerProtocol>)contactsManager
|
||||
{
|
||||
TSMessageAdapter *adapter = [[TSMessageAdapter alloc] initWithInteraction:interaction];
|
||||
|
||||
if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
adapter.thread = (TSContactThread *)thread;
|
||||
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
|
||||
NSString *contactId = ((TSContactThread *)thread).contactIdentifier;
|
||||
adapter.senderId = contactId;
|
||||
adapter.senderDisplayName = [contactsManager displayNameForPhoneIdentifier:contactId];
|
||||
adapter.messageType = TSIncomingMessageAdapter;
|
||||
} else {
|
||||
adapter.senderId = ME_MESSAGE_IDENTIFIER;
|
||||
adapter.senderDisplayName = NSLocalizedString(@"ME_STRING", @"");
|
||||
adapter.messageType = TSOutgoingMessageAdapter;
|
||||
}
|
||||
} else if ([thread isKindOfClass:[TSGroupThread class]]) {
|
||||
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
|
||||
TSIncomingMessage *message = (TSIncomingMessage *)interaction;
|
||||
adapter.senderId = message.authorId;
|
||||
adapter.senderDisplayName = [contactsManager displayNameForPhoneIdentifier:message.authorId];
|
||||
adapter.messageType = TSIncomingMessageAdapter;
|
||||
} else {
|
||||
adapter.senderId = ME_MESSAGE_IDENTIFIER;
|
||||
adapter.senderDisplayName = NSLocalizedString(@"ME_STRING", @"");
|
||||
adapter.messageType = TSOutgoingMessageAdapter;
|
||||
}
|
||||
} else {
|
||||
OWSFail(@"%@ Unknown thread type: %@", self.tag, [thread class]);
|
||||
}
|
||||
|
||||
if ([interaction isKindOfClass:[TSIncomingMessage class]] ||
|
||||
[interaction isKindOfClass:[TSOutgoingMessage class]]) {
|
||||
TSMessage *message = (TSMessage *)interaction;
|
||||
adapter.messageBody = [[DisplayableTextFilter new] displayableText:message.body];
|
||||
|
||||
if ([message hasAttachments]) {
|
||||
for (NSString *attachmentID in message.attachmentIds) {
|
||||
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentID];
|
||||
|
||||
BOOL isIncomingAttachment = [interaction isKindOfClass:[TSIncomingMessage class]];
|
||||
|
||||
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
TSAttachmentStream *stream = (TSAttachmentStream *)attachment;
|
||||
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
|
||||
NSString *displayableText = [[self displayableTextCache] objectForKey:interaction.uniqueId];
|
||||
if (!displayableText) {
|
||||
NSData *textData = [NSData dataWithContentsOfURL:stream.mediaURL];
|
||||
NSString *fullText = [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
|
||||
// Only show up to 2kb of text.
|
||||
const NSUInteger kMaxTextDisplayLength = 2 * 1024;
|
||||
displayableText = [[DisplayableTextFilter new] displayableText:fullText];
|
||||
if (displayableText.length > kMaxTextDisplayLength) {
|
||||
// Trim whitespace before _AND_ after slicing the snipper from the string.
|
||||
NSString *snippet = [[[displayableText
|
||||
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]
|
||||
substringWithRange:NSMakeRange(0, kMaxTextDisplayLength)]
|
||||
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
displayableText =
|
||||
[NSString stringWithFormat:NSLocalizedString(@"OVERSIZE_TEXT_DISPLAY_FORMAT",
|
||||
@"A display format for oversize text messages."),
|
||||
snippet];
|
||||
}
|
||||
if (!displayableText) {
|
||||
displayableText = @"";
|
||||
}
|
||||
[[self displayableTextCache] setObject:displayableText forKey:interaction.uniqueId];
|
||||
}
|
||||
adapter.messageBody = displayableText;
|
||||
} else if ([stream isAnimated]) {
|
||||
adapter.mediaItem =
|
||||
[[TSAnimatedAdapter alloc] initWithAttachment:stream incoming:isIncomingAttachment];
|
||||
adapter.mediaItem.appliesMediaViewMaskAsOutgoing = !isIncomingAttachment;
|
||||
break;
|
||||
} else if ([stream isImage]) {
|
||||
adapter.mediaItem =
|
||||
[[TSPhotoAdapter alloc] initWithAttachment:stream incoming:isIncomingAttachment];
|
||||
adapter.mediaItem.appliesMediaViewMaskAsOutgoing = !isIncomingAttachment;
|
||||
break;
|
||||
} else if ([stream isVideo] || [stream isAudio]) {
|
||||
adapter.mediaItem = [[TSVideoAttachmentAdapter alloc]
|
||||
initWithAttachment:stream
|
||||
incoming:[interaction isKindOfClass:[TSIncomingMessage class]]];
|
||||
adapter.mediaItem.appliesMediaViewMaskAsOutgoing = !isIncomingAttachment;
|
||||
break;
|
||||
} else {
|
||||
adapter.mediaItem = [[TSGenericAttachmentAdapter alloc]
|
||||
initWithAttachment:stream
|
||||
incoming:[interaction isKindOfClass:[TSIncomingMessage class]]];
|
||||
adapter.mediaItem.appliesMediaViewMaskAsOutgoing = !isIncomingAttachment;
|
||||
break;
|
||||
}
|
||||
} else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
|
||||
TSAttachmentPointer *pointer = (TSAttachmentPointer *)attachment;
|
||||
adapter.mediaItem =
|
||||
[[AttachmentPointerAdapter alloc] initWithAttachmentPointer:pointer
|
||||
isIncoming:isIncomingAttachment];
|
||||
} else {
|
||||
DDLogError(@"We retrieved an attachment that doesn't have a known type : %@",
|
||||
NSStringFromClass([attachment class]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSString *displayableText = [[self displayableTextCache] objectForKey:interaction.uniqueId];
|
||||
if (!displayableText) {
|
||||
displayableText = [[DisplayableTextFilter new] displayableText:message.body];
|
||||
if (!displayableText) {
|
||||
displayableText = @"";
|
||||
}
|
||||
[[self displayableTextCache] setObject:displayableText forKey:interaction.uniqueId];
|
||||
}
|
||||
adapter.messageBody = displayableText;
|
||||
}
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
TSCall *callRecord = (TSCall *)interaction;
|
||||
return [[OWSCall alloc] initWithCallRecord:callRecord];
|
||||
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
|
||||
TSInfoMessage *infoMessage = (TSInfoMessage *)interaction;
|
||||
adapter.infoMessageType = infoMessage.messageType;
|
||||
adapter.messageBody = infoMessage.description;
|
||||
adapter.messageType = TSInfoMessageAdapter;
|
||||
} else if ([interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]) {
|
||||
adapter.messageType = TSUnreadIndicatorAdapter;
|
||||
} else if ([interaction isKindOfClass:[OWSContactOffersInteraction class]]) {
|
||||
adapter.messageType = OWSContactOffersAdapter;
|
||||
} else if ([interaction isKindOfClass:[TSErrorMessage class]]) {
|
||||
TSErrorMessage *errorMessage = (TSErrorMessage *)interaction;
|
||||
adapter.errorMessageType = errorMessage.errorType;
|
||||
adapter.messageBody = errorMessage.description;
|
||||
adapter.messageType = TSErrorMessageAdapter;
|
||||
} else {
|
||||
OWSFail(@"%@ Unknown interaction type: %@", self.tag, [interaction class]);
|
||||
}
|
||||
|
||||
if ([interaction isKindOfClass:[TSOutgoingMessage class]]) {
|
||||
adapter.outgoingMessageStatus = ((TSOutgoingMessage *)interaction).messageState;
|
||||
}
|
||||
|
||||
OWSAssert(adapter.date);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
- (NSString *)senderId {
|
||||
if (_senderId) {
|
||||
return _senderId;
|
||||
} else {
|
||||
return ME_MESSAGE_IDENTIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSDate *)date {
|
||||
return self.interaction.dateForSorting;
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageEditing Protocol
|
||||
|
||||
+ (SEL)messageMetadataSelector
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
return @selector(showMessageMetadata:);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action
|
||||
{
|
||||
if ([self attachmentStream] && ![self attachmentStream].isUploaded) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Deletes are always handled by TSMessageAdapter
|
||||
if (action == @selector(delete:)) {
|
||||
return YES;
|
||||
} else if (action == [TSMessageAdapter messageMetadataSelector]) {
|
||||
return ([self.interaction isKindOfClass:[TSIncomingMessage class]] ||
|
||||
[self.interaction isKindOfClass:[TSOutgoingMessage class]]);
|
||||
}
|
||||
|
||||
// Delegate other actions for media items
|
||||
if ([self attachmentStream] && action == NSSelectorFromString(@"share:")) {
|
||||
return YES;
|
||||
} else if (self.isMediaMessage) {
|
||||
return [self.mediaItem canPerformEditingAction:action];
|
||||
} else if (self.messageType == TSInfoMessageAdapter || self.messageType == TSErrorMessageAdapter) {
|
||||
return NO;
|
||||
} else {
|
||||
// Text message - no media attachment
|
||||
if (action == @selector(copy:)) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)performEditingAction:(SEL)action
|
||||
{
|
||||
// Deletes are always handled by TSMessageAdapter
|
||||
if (action == @selector(delete:)) {
|
||||
DDLogDebug(@"Deleting interaction with uniqueId: %@", self.interaction.uniqueId);
|
||||
[self.interaction remove];
|
||||
return;
|
||||
} else if (action == NSSelectorFromString(@"share:")) {
|
||||
TSAttachmentStream *stream = [self attachmentStream];
|
||||
OWSAssert(stream);
|
||||
if (stream) {
|
||||
[AttachmentSharing showShareUIForAttachment:stream];
|
||||
}
|
||||
return;
|
||||
} else if (action == [TSMessageAdapter messageMetadataSelector]) {
|
||||
OWSFail(@"Conversation view should handle message metadata events.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate other actions for media items
|
||||
if (self.isMediaMessage) {
|
||||
[self.mediaItem performEditingAction:action];
|
||||
return;
|
||||
} else {
|
||||
// Text message - no media attachment
|
||||
if (action == @selector(copy:)) {
|
||||
UIPasteboard.generalPasteboard.string = self.messageBody;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
OWSFail(@"'%@' action unsupported for TSInteraction: uniqueId=%@, mediaType=%@",
|
||||
actionString,
|
||||
self.interaction.uniqueId,
|
||||
[self.mediaItem class]);
|
||||
}
|
||||
|
||||
- (TSAttachmentStream *)attachmentStream
|
||||
{
|
||||
if (![self.interaction isKindOfClass:[TSMessage class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TSMessage *message = (TSMessage *)self.interaction;
|
||||
|
||||
if (![message hasAttachments]) {
|
||||
return nil;
|
||||
}
|
||||
OWSAssert(message.attachmentIds.count <= 1);
|
||||
NSString *attachmentID = message.attachmentIds[0];
|
||||
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentID];
|
||||
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
TSAttachmentStream *stream = (TSAttachmentStream *)attachment;
|
||||
return stream;
|
||||
}
|
||||
|
||||
- (BOOL)isMediaMessage {
|
||||
return _mediaItem ? YES : NO;
|
||||
}
|
||||
|
||||
- (id<JSQMessageMediaData>)media {
|
||||
return _mediaItem;
|
||||
}
|
||||
|
||||
- (NSString *)text {
|
||||
return self.messageBody;
|
||||
}
|
||||
|
||||
- (NSUInteger)messageHash
|
||||
{
|
||||
OWSAssert(self.interactionUniqueId);
|
||||
|
||||
// messageHash is used as a key in the "message bubble size" cache,
|
||||
// so messageHash's value must change whenever the message's bubble size
|
||||
// changes. Incoming messages change size after their attachment's been
|
||||
// downloaded, so we use the mediaItem's class (which will be nil before
|
||||
// the attachment is downloaded) to reflect attachment status.
|
||||
return self.interactionUniqueId.hash ^ [self.mediaItem class].description.hash;
|
||||
}
|
||||
|
||||
- (NSInteger)messageState {
|
||||
return self.outgoingMessageStatus;
|
||||
}
|
||||
|
||||
- (CGFloat)mediaViewAlpha
|
||||
{
|
||||
return (CGFloat)(self.isMediaBeingSent ? 0.75 : 1);
|
||||
}
|
||||
|
||||
- (BOOL)isMediaBeingSent
|
||||
{
|
||||
if ([self.interaction isKindOfClass:[TSOutgoingMessage class]]) {
|
||||
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.interaction;
|
||||
if (outgoingMessage.hasAttachments && outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageEditing.h"
|
||||
#import "OWSMessageMediaAdapter.h"
|
||||
#import <JSQMessagesViewController/JSQPhotoMediaItem.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface TSPhotoAdapter : JSQPhotoMediaItem <OWSMessageEditing, OWSMessageMediaAdapter>
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming;
|
||||
|
||||
@property TSAttachmentStream *attachment;
|
||||
@property NSString *attachmentId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,189 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSPhotoAdapter.h"
|
||||
#import "AttachmentUploadView.h"
|
||||
#import "JSQMediaItem+OWS.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesMediaViewBubbleImageMasker.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <SignalServiceKit/MimeTypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TSPhotoAdapter ()
|
||||
|
||||
@property (nonatomic, nullable) UIImageView *cachedImageView;
|
||||
@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView;
|
||||
@property (nonatomic) BOOL incoming;
|
||||
@property (nonatomic) CGSize imageSize;
|
||||
|
||||
// See comments on OWSMessageMediaAdapter.
|
||||
@property (nonatomic, nullable, weak) id lastPresentingCell;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation TSPhotoAdapter
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_cachedImageView = nil;
|
||||
_attachment = attachment;
|
||||
_attachmentId = attachment.uniqueId;
|
||||
_incoming = incoming;
|
||||
_imageSize = [attachment imageSizeWithoutTransaction];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)clearAllViews
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[_cachedImageView removeFromSuperview];
|
||||
_cachedImageView = nil;
|
||||
_attachmentUploadView = nil;
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViews
|
||||
{
|
||||
[super clearCachedMediaViews];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing {
|
||||
[super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageMediaData protocol
|
||||
|
||||
- (UIView *)mediaView {
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
if (self.cachedImageView == nil) {
|
||||
UIImage *image = self.attachment.image;
|
||||
if (!image) {
|
||||
DDLogError(@"%@ Could not load image: %@", [self tag], [self.attachment mediaURL]);
|
||||
UIView *view = [UIView new];
|
||||
view.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
|
||||
return view;
|
||||
}
|
||||
CGSize size = [self mediaViewDisplaySize];
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
imageView.frame = CGRectMake(0.0f, 0.0f, size.width, size.height);
|
||||
imageView.clipsToBounds = YES;
|
||||
// Use trilinear filters for better scaling quality at
|
||||
// some performance cost.
|
||||
imageView.layer.minificationFilter = kCAFilterTrilinear;
|
||||
imageView.layer.magnificationFilter = kCAFilterTrilinear;
|
||||
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:imageView
|
||||
isOutgoing:self.appliesMediaViewMaskAsOutgoing];
|
||||
self.cachedImageView = imageView;
|
||||
|
||||
if (!self.incoming) {
|
||||
self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachment
|
||||
superview:imageView
|
||||
attachmentStateCallback:nil];
|
||||
}
|
||||
}
|
||||
|
||||
return self.cachedImageView;
|
||||
}
|
||||
|
||||
- (CGSize)mediaViewDisplaySize {
|
||||
return [self ows_adjustBubbleSize:[super mediaViewDisplaySize] forImageSize:self.imageSize];
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageEditing Protocol
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action
|
||||
{
|
||||
return (action == @selector(copy:) || action == NSSelectorFromString(@"save:"));
|
||||
}
|
||||
|
||||
- (void)performEditingAction:(SEL)action
|
||||
{
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
|
||||
if (action == @selector(copy:)) {
|
||||
// We should always copy to the pasteboard as data, not an UIImage.
|
||||
// The pasteboard should have as specific as UTI type as possible and
|
||||
// data support should be far more general than UIImage support.
|
||||
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.attachment.contentType];
|
||||
if (!utiType) {
|
||||
OWSFail(@"%@ Unknown MIME type: %@", self.tag, self.attachment.contentType);
|
||||
utiType = (NSString *)kUTTypeImage;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfURL:self.attachment.mediaURL];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
return;
|
||||
} else if (action == NSSelectorFromString(@"save:")) {
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachment mediaURL]];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load image data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
|
||||
[library writeImageDataToSavedPhotosAlbum:data
|
||||
metadata:nil
|
||||
completionBlock:^(NSURL *assetURL, NSError *error) {
|
||||
if (error) {
|
||||
DDLogWarn(@"Error Saving image to photo album: %@", error);
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
// Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
OWSFail(@"'%@' action unsupported for %@: attachmentId=%@", actionString, self.class, self.attachmentId);
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageMediaAdapter
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible
|
||||
{
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViewsIfLastPresentingCell:(id)cell
|
||||
{
|
||||
OWSAssert(cell);
|
||||
|
||||
if (cell == self.lastPresentingCell) {
|
||||
[self clearCachedMediaViews];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSAudioAttachmentPlayer.h"
|
||||
#import "OWSMessageEditing.h"
|
||||
#import "OWSMessageMediaAdapter.h"
|
||||
#import <JSQMessagesViewController/JSQVideoMediaItem.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface TSVideoAttachmentAdapter
|
||||
: JSQVideoMediaItem <OWSMessageEditing, OWSMessageMediaAdapter, OWSAudioAttachmentPlayerDelegate>
|
||||
|
||||
@property NSString *attachmentId;
|
||||
@property (nonatomic, strong) NSString *contentType;
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming;
|
||||
|
||||
- (BOOL)isAudio;
|
||||
- (BOOL)isVideo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,470 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSVideoAttachmentAdapter.h"
|
||||
#import "AttachmentUploadView.h"
|
||||
#import "JSQMediaItem+OWS.h"
|
||||
#import "MIMETypeUtil.h"
|
||||
#import "OWSMessageManager.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "UIColor+JSQMessages.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIDevice+TSHardwareVersion.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <JSQMessagesViewController/JSQMessagesMediaViewBubbleImageMasker.h>
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <SignalServiceKit/MIMETypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface TSVideoAttachmentAdapter ()
|
||||
|
||||
@property (nonatomic, nullable) UIView *cachedMediaView;
|
||||
@property (nonatomic) TSAttachmentStream *attachment;
|
||||
@property (nonatomic, nullable) UIButton *audioPlayPauseButton;
|
||||
@property (nonatomic, nullable) UILabel *audioBottomLabel;
|
||||
@property (nonatomic, nullable) AudioProgressView *audioProgressView;
|
||||
@property (nonatomic) BOOL incoming;
|
||||
@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView;
|
||||
@property (nonatomic) BOOL isAudioPlaying;
|
||||
@property (nonatomic) CGFloat audioProgressSeconds;
|
||||
@property (nonatomic) CGFloat audioDurationSeconds;
|
||||
@property (nonatomic) BOOL isPaused;
|
||||
@property (nonatomic) CGSize imageSize;
|
||||
|
||||
// See comments on OWSMessageMediaAdapter.
|
||||
@property (nonatomic, nullable, weak) id lastPresentingCell;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation TSVideoAttachmentAdapter
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment incoming:(BOOL)incoming {
|
||||
self = [super initWithFileURL:[attachment mediaURL] isReadyToPlay:YES];
|
||||
|
||||
if (self) {
|
||||
_cachedMediaView = nil;
|
||||
_attachmentId = attachment.uniqueId;
|
||||
_contentType = attachment.contentType;
|
||||
_attachment = attachment;
|
||||
_incoming = incoming;
|
||||
_imageSize = [attachment imageSizeWithoutTransaction];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)clearAllViews
|
||||
{
|
||||
[self.cachedMediaView removeFromSuperview];
|
||||
self.cachedMediaView = nil;
|
||||
self.attachmentUploadView = nil;
|
||||
self.audioProgressView = nil;
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViews
|
||||
{
|
||||
[super clearCachedMediaViews];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
- (void)setAppliesMediaViewMaskAsOutgoing:(BOOL)appliesMediaViewMaskAsOutgoing
|
||||
{
|
||||
[super setAppliesMediaViewMaskAsOutgoing:appliesMediaViewMaskAsOutgoing];
|
||||
[self clearAllViews];
|
||||
}
|
||||
|
||||
- (BOOL)isAudio {
|
||||
return [MIMETypeUtil isSupportedAudioMIMEType:_contentType];
|
||||
}
|
||||
|
||||
- (BOOL)isVideo {
|
||||
return [MIMETypeUtil isSupportedVideoMIMEType:_contentType];
|
||||
}
|
||||
|
||||
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
self.audioProgressSeconds = progress;
|
||||
if (duration > 0) {
|
||||
self.audioDurationSeconds = duration;
|
||||
}
|
||||
|
||||
[self updateAudioProgressView];
|
||||
|
||||
[self updateAudioBottomLabel];
|
||||
}
|
||||
|
||||
- (void)updateAudioBottomLabel
|
||||
{
|
||||
if (self.isAudioPlaying && self.audioProgressSeconds > 0 && self.audioDurationSeconds > 0) {
|
||||
self.audioBottomLabel.text =
|
||||
[NSString stringWithFormat:@"%@ / %@",
|
||||
[ViewControllerUtils formatDurationSeconds:(long)round(self.audioProgressSeconds)],
|
||||
[ViewControllerUtils formatDurationSeconds:(long)round(self.audioDurationSeconds)]];
|
||||
} else {
|
||||
self.audioBottomLabel.text = [NSString
|
||||
stringWithFormat:@"%@", [ViewControllerUtils formatDurationSeconds:(long)round(self.audioDurationSeconds)]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAudioIcon:(UIImage *)icon iconColor:(UIColor *)iconColor
|
||||
{
|
||||
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
[_audioPlayPauseButton setImage:icon forState:UIControlStateNormal];
|
||||
[_audioPlayPauseButton setImage:icon forState:UIControlStateDisabled];
|
||||
_audioPlayPauseButton.imageView.tintColor = self.bubbleBackgroundColor;
|
||||
_audioPlayPauseButton.backgroundColor = iconColor;
|
||||
_audioPlayPauseButton.layer.cornerRadius
|
||||
= MIN(_audioPlayPauseButton.bounds.size.width, _audioPlayPauseButton.bounds.size.height) * 0.5f;
|
||||
}
|
||||
|
||||
- (void)setAudioIconToPlay {
|
||||
[self setAudioIcon:[UIImage imageNamed:@"audio_play_black_40"]
|
||||
iconColor:(self.incoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self audioColorWithOpacity:0.15f])];
|
||||
}
|
||||
|
||||
- (void)setAudioIconToPause {
|
||||
[self setAudioIcon:[UIImage imageNamed:@"audio_pause_black_40"]
|
||||
iconColor:(self.incoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self audioColorWithOpacity:0.15f])];
|
||||
}
|
||||
|
||||
- (void)setIsAudioPlaying:(BOOL)isAudioPlaying
|
||||
{
|
||||
_isAudioPlaying = isAudioPlaying;
|
||||
|
||||
[self updateAudioProgressView];
|
||||
}
|
||||
|
||||
- (void)updateAudioProgressView
|
||||
{
|
||||
[self.audioProgressView
|
||||
setProgress:(self.audioDurationSeconds > 0 ? self.audioProgressSeconds / self.audioDurationSeconds : 0.f)];
|
||||
|
||||
self.audioProgressView.horizontalBarColor = [self audioColorWithOpacity:0.75f];
|
||||
self.audioProgressView.progressColor
|
||||
= (self.isAudioPlaying ? [self audioColorWithOpacity:self.incoming ? 0.2f : 0.1f]
|
||||
: [self audioColorWithOpacity:0.4f]);
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageMediaData protocol
|
||||
|
||||
- (CGFloat)audioIconHMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)audioIconHSpacing
|
||||
{
|
||||
return 10.f;
|
||||
}
|
||||
|
||||
- (CGFloat)audioIconVMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)audioBubbleHeight
|
||||
{
|
||||
return self.iconSize + self.audioIconVMargin * 2;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return 40.f;
|
||||
}
|
||||
|
||||
- (UIColor *)audioTextColor
|
||||
{
|
||||
return (self.incoming ? [UIColor colorWithWhite:0.2f alpha:1.f] : [UIColor whiteColor]);
|
||||
}
|
||||
|
||||
- (UIColor *)audioColorWithOpacity:(CGFloat)alpha
|
||||
{
|
||||
return [self.audioTextColor blendWithColor:self.bubbleBackgroundColor alpha:alpha];
|
||||
}
|
||||
|
||||
- (UIColor *)bubbleBackgroundColor
|
||||
{
|
||||
return self.incoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_materialBlueColor];
|
||||
}
|
||||
|
||||
- (UIView *)mediaView {
|
||||
if ([self isVideo]) {
|
||||
if (self.cachedMediaView == nil) {
|
||||
self.cachedMediaView = [self createVideoMediaView];
|
||||
}
|
||||
} else if ([self isAudio]) {
|
||||
if (self.cachedMediaView == nil) {
|
||||
self.cachedMediaView = [self createAudioMediaView];
|
||||
}
|
||||
|
||||
if (self.isAudioPlaying) {
|
||||
[self setAudioIconToPause];
|
||||
} else {
|
||||
[self setAudioIconToPlay];
|
||||
}
|
||||
} else {
|
||||
OWSFail(@"%@ Unknown media type: %@", self.tag, self.attachment.contentType);
|
||||
}
|
||||
return self.cachedMediaView;
|
||||
}
|
||||
|
||||
- (UIView *)createVideoMediaView
|
||||
{
|
||||
OWSAssert([self isVideo]);
|
||||
|
||||
CGSize size = [self mediaViewDisplaySize];
|
||||
|
||||
UIImage *image = self.attachment.image;
|
||||
if (!image) {
|
||||
DDLogError(@"%@ Could not load image: %@", [self tag], [self.attachment mediaURL]);
|
||||
UIView *view = [UIView new];
|
||||
view.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
|
||||
return view;
|
||||
}
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
imageView.frame = CGRectMake(0.0f, 0.0f, size.width, size.height);
|
||||
imageView.clipsToBounds = YES;
|
||||
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:imageView
|
||||
isOutgoing:self.appliesMediaViewMaskAsOutgoing];
|
||||
UIImage *img = [UIImage imageNamed:@"play_button"];
|
||||
UIImageView *videoPlayButton = [[UIImageView alloc] initWithImage:img];
|
||||
videoPlayButton.frame = CGRectMake((size.width / 2) - 18, (size.height / 2) - 18, 37, 37);
|
||||
[imageView addSubview:videoPlayButton];
|
||||
|
||||
if (!_incoming) {
|
||||
self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachment
|
||||
superview:imageView
|
||||
attachmentStateCallback:^(BOOL isAttachmentReady) {
|
||||
videoPlayButton.hidden = !isAttachmentReady;
|
||||
}];
|
||||
}
|
||||
|
||||
return imageView;
|
||||
}
|
||||
|
||||
- (BOOL)isVoiceMessage
|
||||
{
|
||||
OWSAssert([self isAudio]);
|
||||
|
||||
// We want to treat "pre-voice messages flag" messages as voice messages if
|
||||
// they have no file name.
|
||||
//
|
||||
// TODO: Remove this after the flag has been in production for a few months.
|
||||
return (self.attachment.isVoiceMessage || self.attachment.sourceFilename.length < 1);
|
||||
}
|
||||
|
||||
- (UIView *)createAudioMediaView
|
||||
{
|
||||
OWSAssert([self isAudio]);
|
||||
|
||||
self.audioDurationSeconds = [self.attachment audioDurationSecondsWithoutTransaction];
|
||||
|
||||
CGSize viewSize = [self mediaViewDisplaySize];
|
||||
UIColor *textColor = [self audioTextColor];
|
||||
|
||||
UIView *mediaView = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, viewSize.width, viewSize.height)];
|
||||
|
||||
mediaView.backgroundColor = self.bubbleBackgroundColor;
|
||||
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:mediaView isOutgoing:!self.incoming];
|
||||
|
||||
const CGFloat kBubbleTailWidth = 6.f;
|
||||
CGRect contentFrame = CGRectMake(self.incoming ? kBubbleTailWidth : 0.f,
|
||||
self.audioIconVMargin,
|
||||
viewSize.width - kBubbleTailWidth - self.audioIconHMargin,
|
||||
viewSize.height - self.audioIconVMargin * 2);
|
||||
|
||||
CGRect iconFrame = CGRectMake((CGFloat)round(contentFrame.origin.x + self.audioIconHMargin),
|
||||
(CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
|
||||
self.iconSize,
|
||||
self.iconSize);
|
||||
_audioPlayPauseButton = [[UIButton alloc] initWithFrame:iconFrame];
|
||||
_audioPlayPauseButton.enabled = NO;
|
||||
[mediaView addSubview:_audioPlayPauseButton];
|
||||
|
||||
const CGFloat kLabelHSpacing = self.audioIconHSpacing;
|
||||
const CGFloat kLabelVSpacing = 2;
|
||||
NSString *filename = self.attachment.sourceFilename;
|
||||
if (!filename) {
|
||||
filename = [[self.attachment filePath] lastPathComponent];
|
||||
}
|
||||
NSString *topText = [[filename stringByDeletingPathExtension]
|
||||
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (topText.length < 1) {
|
||||
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachment.contentType].uppercaseString;
|
||||
}
|
||||
if (topText.length < 1) {
|
||||
topText = NSLocalizedString(@"GENERIC_ATTACHMENT_LABEL", @"A label for generic attachments.");
|
||||
}
|
||||
if (self.isVoiceMessage) {
|
||||
topText = nil;
|
||||
}
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = topText;
|
||||
topLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
|
||||
[topLabel sizeToFit];
|
||||
[mediaView addSubview:topLabel];
|
||||
|
||||
AudioProgressView *audioProgressView = [AudioProgressView new];
|
||||
self.audioProgressView = audioProgressView;
|
||||
[self updateAudioProgressView];
|
||||
[mediaView addSubview:audioProgressView];
|
||||
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
self.audioBottomLabel = bottomLabel;
|
||||
[self updateAudioBottomLabel];
|
||||
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
|
||||
[bottomLabel sizeToFit];
|
||||
[mediaView addSubview:bottomLabel];
|
||||
|
||||
const CGFloat topLabelHeight = (CGFloat)ceil(topLabel.font.lineHeight);
|
||||
const CGFloat kAudioProgressViewHeight = 12.f;
|
||||
const CGFloat bottomLabelHeight = (CGFloat)ceil(bottomLabel.font.lineHeight);
|
||||
CGRect labelsBounds = CGRectZero;
|
||||
labelsBounds.origin.x = (CGFloat)round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
|
||||
labelsBounds.size.width = contentFrame.origin.x + contentFrame.size.width - labelsBounds.origin.x;
|
||||
labelsBounds.size.height = topLabelHeight + kAudioProgressViewHeight + bottomLabelHeight + kLabelVSpacing * 2;
|
||||
labelsBounds.origin.y
|
||||
= (CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - labelsBounds.size.height) * 0.5f);
|
||||
|
||||
CGFloat y = labelsBounds.origin.y;
|
||||
topLabel.frame
|
||||
= CGRectMake(labelsBounds.origin.x, labelsBounds.origin.y, labelsBounds.size.width, topLabelHeight);
|
||||
y += topLabelHeight + kLabelVSpacing;
|
||||
audioProgressView.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, kAudioProgressViewHeight);
|
||||
y += kAudioProgressViewHeight + kLabelVSpacing;
|
||||
bottomLabel.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, bottomLabelHeight);
|
||||
|
||||
if (!self.incoming) {
|
||||
self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachment
|
||||
superview:mediaView
|
||||
attachmentStateCallback:nil];
|
||||
}
|
||||
|
||||
return mediaView;
|
||||
}
|
||||
|
||||
- (CGSize)mediaViewDisplaySize {
|
||||
CGSize size = [super mediaViewDisplaySize];
|
||||
if ([self isAudio]) {
|
||||
size.width = [self ows_maxMediaBubbleWidth:size];
|
||||
size.height = (CGFloat)ceil(self.audioBubbleHeight);
|
||||
} else if ([self isVideo]) {
|
||||
return [self ows_adjustBubbleSize:size forImageSize:self.imageSize];
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
- (UIView *)mediaPlaceholderView {
|
||||
return [self mediaView];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return [super hash];
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageEditing Protocol
|
||||
|
||||
- (BOOL)canPerformEditingAction:(SEL)action
|
||||
{
|
||||
if ([self isVideo]) {
|
||||
return (action == @selector(copy:) || action == NSSelectorFromString(@"save:"));
|
||||
} else if ([self isAudio]) {
|
||||
return (action == @selector(copy:));
|
||||
}
|
||||
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
DDLogError(
|
||||
@"Unexpected action: %@ for VideoAttachmentAdapter with contentType: %@", actionString, self.contentType);
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)performEditingAction:(SEL)action
|
||||
{
|
||||
if ([self isVideo]) {
|
||||
if (action == @selector(copy:)) {
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.contentType];
|
||||
if (!utiType) {
|
||||
OWSFail(@"%@ Unknown MIME type: %@", self.tag, self.contentType);
|
||||
utiType = (NSString *)kUTTypeVideo;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfURL:self.fileURL];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
return;
|
||||
} else if (action == NSSelectorFromString(@"save:")) {
|
||||
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.fileURL.path)) {
|
||||
UISaveVideoAtPathToSavedPhotosAlbum(self.fileURL.path, self, nil, nil);
|
||||
} else {
|
||||
DDLogWarn(@"cowardly refusing to save incompatible video attachment");
|
||||
}
|
||||
}
|
||||
} else if ([self isAudio]) {
|
||||
if (action == @selector(copy:)) {
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.contentType];
|
||||
if (!utiType) {
|
||||
OWSFail(@"%@ Unknown MIME type: %@", self.tag, self.contentType);
|
||||
utiType = (NSString *)kUTTypeAudio;
|
||||
}
|
||||
|
||||
NSData *data = [NSData dataWithContentsOfURL:self.fileURL];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load data: %@", [self tag], [self.attachment mediaURL]);
|
||||
return;
|
||||
}
|
||||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
}
|
||||
} else {
|
||||
// Shouldn't get here, as only supported actions should be exposed via canPerformEditingAction
|
||||
NSString *actionString = NSStringFromSelector(action);
|
||||
OWSFail(
|
||||
@"Unexpected action: %@ for VideoAttachmentAdapter with contentType: %@", actionString, self.contentType);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - OWSMessageMediaAdapter
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible
|
||||
{
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
- (void)clearCachedMediaViewsIfLastPresentingCell:(id)cell
|
||||
{
|
||||
OWSAssert(cell);
|
||||
|
||||
if (cell == self.lastPresentingCell) {
|
||||
[self clearCachedMediaViews];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -50,6 +50,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (OWSInteractionType)interactionType
|
||||
{
|
||||
return OWSInteractionType_UnreadIndicator;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -9,6 +9,8 @@ class OWSMessagesBubbleImageFactory: NSObject {
|
|||
|
||||
let jsqFactory = JSQMessagesBubbleImageFactory()!
|
||||
|
||||
// TODO: UIView is a little bit expensive to instantiate.
|
||||
// Can we cache this value?
|
||||
var isRTL: Bool {
|
||||
return UIView().isRTL()
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
#import "OWSContactsManager.h"
|
||||
#import "OWSDatabaseMigration.h"
|
||||
#import "OWSLogger.h"
|
||||
#import "OWSMessageEditing.h"
|
||||
#import "OWSNavigationController.h"
|
||||
#import "OWSPreferences.h"
|
||||
#import "OWSProfileManager.h"
|
||||
|
@ -32,7 +31,6 @@
|
|||
#import "PushManager.h"
|
||||
#import "Release.h"
|
||||
#import "RemoteVideoView.h"
|
||||
#import "TSMessageAdapter.h"
|
||||
#import "ThreadUtil.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
|
|
|
@ -21,13 +21,13 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value);
|
|||
@interface UIView (OWS)
|
||||
|
||||
// Pins the width of this view to the width of its superview, with uniform margins.
|
||||
- (void)autoPinWidthToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (void)autoPinWidthToSuperview;
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinWidthToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinWidthToSuperview;
|
||||
// Pins the height of this view to the height of its superview, with uniform margins.
|
||||
- (void)autoPinHeightToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (void)autoPinHeightToSuperview;
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinHeightToSuperviewWithMargin:(CGFloat)margin;
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinHeightToSuperview;
|
||||
|
||||
- (void)autoPinToSuperviewEdges;
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinToSuperviewEdges;
|
||||
|
||||
- (void)autoHCenterInSuperview;
|
||||
- (void)autoVCenterInSuperview;
|
||||
|
|
|
@ -33,16 +33,22 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
|
|||
|
||||
@implementation UIView (OWS)
|
||||
|
||||
- (void)autoPinWidthToSuperviewWithMargin:(CGFloat)margin
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinWidthToSuperviewWithMargin:(CGFloat)margin
|
||||
{
|
||||
[self autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:self.superview withOffset:+margin];
|
||||
[self autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:self.superview withOffset:-margin];
|
||||
NSArray<NSLayoutConstraint *> *result = @[
|
||||
[self autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:self.superview withOffset:+margin],
|
||||
[self autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:self.superview withOffset:-margin],
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)autoPinWidthToSuperview
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinWidthToSuperview
|
||||
{
|
||||
[self autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:self.superview];
|
||||
[self autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:self.superview];
|
||||
NSArray<NSLayoutConstraint *> *result = @[
|
||||
[self autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:self.superview],
|
||||
[self autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:self.superview],
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinLeadingAndTrailingToSuperview
|
||||
|
@ -54,22 +60,33 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
|
|||
return result;
|
||||
}
|
||||
|
||||
- (void)autoPinHeightToSuperviewWithMargin:(CGFloat)margin
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinHeightToSuperviewWithMargin:(CGFloat)margin
|
||||
{
|
||||
[self autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.superview withOffset:+margin];
|
||||
[self autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.superview withOffset:-margin];
|
||||
NSArray<NSLayoutConstraint *> *result = @[
|
||||
[self autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.superview withOffset:+margin],
|
||||
[self autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.superview withOffset:-margin],
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)autoPinHeightToSuperview
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinHeightToSuperview
|
||||
{
|
||||
[self autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.superview];
|
||||
[self autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.superview];
|
||||
NSArray<NSLayoutConstraint *> *result = @[
|
||||
[self autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.superview],
|
||||
[self autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.superview],
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)autoPinToSuperviewEdges
|
||||
- (NSArray<NSLayoutConstraint *> *)autoPinToSuperviewEdges
|
||||
{
|
||||
[self autoPinWidthToSuperview];
|
||||
[self autoPinHeightToSuperview];
|
||||
NSArray<NSLayoutConstraint *> *result = @[
|
||||
[self autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:self.superview],
|
||||
[self autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:self.superview],
|
||||
[self autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.superview],
|
||||
[self autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.superview],
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)autoHCenterInSuperview
|
||||
|
@ -277,7 +294,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
|
|||
constraint.active = YES;
|
||||
return constraint;
|
||||
} else {
|
||||
CGFloat oldMargin = margin;
|
||||
// CGFloat oldMargin = margin;
|
||||
margin += (self.isRTL ? self.superview.layoutMargins.left : self.superview.layoutMargins.right);
|
||||
return [self autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:margin];
|
||||
}
|
||||
|
@ -356,7 +373,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value)
|
|||
UIView *view = [UIView new];
|
||||
// Leading and trailing anchors honor layout margins.
|
||||
// When using a UIView as a "div" to structure layout, we don't want it to have margins.
|
||||
view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
|
||||
view.layoutMargins = UIEdgeInsetsZero;
|
||||
return view;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,4 +10,6 @@
|
|||
|
||||
+ (void)showShareUIForURL:(NSURL *)url;
|
||||
|
||||
+ (void)showShareUIForText:(NSString *)text;
|
||||
|
||||
@end
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#import "AttachmentSharing.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "Threading.h"
|
||||
#import "UIUtil.h"
|
||||
|
||||
@implementation AttachmentSharing
|
||||
|
@ -11,50 +12,64 @@
|
|||
+ (void)showShareUIForAttachment:(TSAttachmentStream *)stream {
|
||||
OWSAssert(stream);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[AttachmentSharing showShareUIForURL:stream.mediaURL];
|
||||
});
|
||||
[self showShareUIForURL:stream.mediaURL];
|
||||
}
|
||||
|
||||
+ (void)showShareUIForURL:(NSURL *)url {
|
||||
AssertIsOnMainThread();
|
||||
OWSAssert(url);
|
||||
|
||||
UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[
|
||||
url,
|
||||
]
|
||||
applicationActivities:@[
|
||||
]];
|
||||
[AttachmentSharing showShareUIForActivityItems:@[
|
||||
url,
|
||||
]];
|
||||
}
|
||||
|
||||
[activityViewController setCompletionWithItemsHandler:^(UIActivityType __nullable activityType,
|
||||
BOOL completed,
|
||||
NSArray *__nullable returnedItems,
|
||||
NSError *__nullable activityError) {
|
||||
+ (void)showShareUIForActivityItems:(NSArray *)activityItems
|
||||
{
|
||||
OWSAssert(activityItems);
|
||||
|
||||
DDLogDebug(@"%@ applying signal appearence", self.tag);
|
||||
[UIUtil applySignalAppearence];
|
||||
DispatchMainThreadSafe(^{
|
||||
UIActivityViewController *activityViewController =
|
||||
[[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:@[]];
|
||||
|
||||
if (activityError) {
|
||||
DDLogInfo(@"%@ Failed to share with activityError: %@", self.tag, activityError);
|
||||
} else if (completed) {
|
||||
DDLogInfo(@"%@ Did share with activityType: %@", self.tag, activityType);
|
||||
[activityViewController setCompletionWithItemsHandler:^(UIActivityType __nullable activityType,
|
||||
BOOL completed,
|
||||
NSArray *__nullable returnedItems,
|
||||
NSError *__nullable activityError) {
|
||||
|
||||
DDLogDebug(@"%@ applying signal appearence", self.tag);
|
||||
[UIUtil applySignalAppearence];
|
||||
|
||||
if (activityError) {
|
||||
DDLogInfo(@"%@ Failed to share with activityError: %@", self.tag, activityError);
|
||||
} else if (completed) {
|
||||
DDLogInfo(@"%@ Did share with activityType: %@", self.tag, activityType);
|
||||
}
|
||||
}];
|
||||
|
||||
// Find the frontmost presented UIViewController from which to present the
|
||||
// share view.
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
UIViewController *fromViewController = window.rootViewController;
|
||||
while (fromViewController.presentedViewController) {
|
||||
fromViewController = fromViewController.presentedViewController;
|
||||
}
|
||||
}];
|
||||
OWSAssert(fromViewController);
|
||||
[fromViewController presentViewController:activityViewController
|
||||
animated:YES
|
||||
completion:^{
|
||||
DDLogDebug(@"%@ applying default system appearence", self.tag);
|
||||
[UIUtil applyDefaultSystemAppearence];
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
// Find the frontmost presented UIViewController from which to present the
|
||||
// share view.
|
||||
UIWindow *window = [UIApplication sharedApplication].keyWindow;
|
||||
UIViewController *fromViewController = window.rootViewController;
|
||||
while (fromViewController.presentedViewController) {
|
||||
fromViewController = fromViewController.presentedViewController;
|
||||
}
|
||||
OWSAssert(fromViewController);
|
||||
[fromViewController presentViewController:activityViewController
|
||||
animated:YES
|
||||
completion:^{
|
||||
DDLogDebug(@"%@ applying default system appearence", self.tag);
|
||||
[UIUtil applyDefaultSystemAppearence];
|
||||
}];
|
||||
+ (void)showShareUIForText:(NSString *)text
|
||||
{
|
||||
OWSAssert(text);
|
||||
|
||||
[AttachmentSharing showShareUIForActivityItems:@[
|
||||
text,
|
||||
]];
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
|
|
@ -8,8 +8,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
typedef void (^AttachmentStateBlock)(BOOL isAttachmentReady);
|
||||
|
||||
// This entity is used by various attachment adapters to
|
||||
// coordinate view state with attachment uploads.
|
||||
// This entity is used by display download progress for incoming
|
||||
// attachments in conversation view cells.
|
||||
//
|
||||
// During attachment uploads we want to:
|
||||
//
|
||||
// * Dim the media view using a mask layer.
|
|
@ -49,8 +49,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[superview.layer addSublayer:_maskLayer];
|
||||
|
||||
const CGFloat progressWidth = round(superview.frame.size.width * 0.45f);
|
||||
const CGFloat progressHeight = round(MIN(superview.frame.size.height * 0.5f,
|
||||
progressWidth * 0.09f));
|
||||
const CGFloat progressHeight = round(MIN(superview.frame.size.height * 0.5f, progressWidth * 0.09f));
|
||||
CGRect progressFrame = CGRectMake(round((superview.frame.size.width - progressWidth) * 0.5f),
|
||||
round((superview.frame.size.height - progressHeight) * 0.5f),
|
||||
progressWidth,
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ConversationViewCell;
|
||||
@class ConversationViewItem;
|
||||
@class OWSContactOffersInteraction;
|
||||
@class TSAttachmentPointer;
|
||||
@class TSAttachmentStream;
|
||||
@class TSInteraction;
|
||||
@class TSMessage;
|
||||
@class TSOutgoingMessage;
|
||||
|
||||
@protocol ConversationViewCellDelegate <NSObject>
|
||||
|
||||
- (void)didTapImageViewItem:(ConversationViewItem *)viewItem
|
||||
attachmentStream:(TSAttachmentStream *)attachmentStream
|
||||
imageView:(UIView *)imageView;
|
||||
- (void)didTapVideoViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
|
||||
- (void)didTapAudioViewItem:(ConversationViewItem *)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
|
||||
- (void)didTapOversizeTextMessage:(NSString *)displayableText attachmentStream:(TSAttachmentStream *)attachmentStream;
|
||||
- (void)didTapFailedIncomingAttachment:(ConversationViewItem *)viewItem
|
||||
attachmentPointer:(TSAttachmentPointer *)attachmentPointer;
|
||||
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message;
|
||||
|
||||
- (void)showMetadataViewForMessage:(TSMessage *)message;
|
||||
|
||||
#pragma mark - System Cell
|
||||
|
||||
// TODO: We might want to decompose this method.
|
||||
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction;
|
||||
- (void)didLongPressSystemMessageCell:(ConversationViewCell *)systemMessageCell fromView:(UIView *)fromView;
|
||||
|
||||
#pragma mark - Offers
|
||||
|
||||
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface ConversationViewCell : UICollectionViewCell
|
||||
|
||||
@property (nonatomic, nullable, weak) id<ConversationViewCellDelegate> delegate;
|
||||
|
||||
@property (nonatomic, nullable) ConversationViewItem *viewItem;
|
||||
|
||||
@property (nonatomic) BOOL isCellVisible;
|
||||
|
||||
// If this is non-null, we should show the message date header.
|
||||
@property (nonatomic, nullable) NSAttributedString *messageDateHeaderText;
|
||||
|
||||
- (void)loadForDisplay;
|
||||
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation ConversationViewCell
|
||||
|
||||
- (void)prepareForReuse
|
||||
{
|
||||
[super prepareForReuse];
|
||||
|
||||
self.viewItem = nil;
|
||||
self.messageDateHeaderText = nil;
|
||||
self.delegate = nil;
|
||||
self.isCellVisible = NO;
|
||||
}
|
||||
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSFail(@"%@ This method should be overridden.", self.logTag);
|
||||
}
|
||||
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth
|
||||
{
|
||||
OWSFail(@"%@ This method should be overridden.", self.logTag);
|
||||
return CGSizeZero;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)logTag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)logTag
|
||||
{
|
||||
return self.class.logTag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,5 +1,6 @@
|
|||
// Created by Michael Kirk on 11/13/16.
|
||||
// Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewCell.h>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSAudioAttachmentPlayer.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ConversationViewItem;
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface OWSAudioMessageView : UIView
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream
|
||||
isIncoming:(BOOL)isIncoming
|
||||
viewItem:(ConversationViewItem *)viewItem;
|
||||
|
||||
- (void)createContentsForSize:(CGSize)viewSize;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
- (void)updateContents;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,277 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSAudioMessageView.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "UIColor+JSQMessages.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SignalServiceKit/MIMETypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSAudioMessageView ()
|
||||
|
||||
@property (nonatomic) TSAttachmentStream *attachmentStream;
|
||||
@property (nonatomic) BOOL isIncoming;
|
||||
@property (nonatomic, weak) ConversationViewItem *viewItem;
|
||||
|
||||
@property (nonatomic, nullable) UIButton *audioPlayPauseButton;
|
||||
@property (nonatomic, nullable) UILabel *audioBottomLabel;
|
||||
@property (nonatomic, nullable) AudioProgressView *audioProgressView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSAudioMessageView
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream
|
||||
isIncoming:(BOOL)isIncoming
|
||||
viewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_attachmentStream = attachmentStream;
|
||||
_isIncoming = isIncoming;
|
||||
_viewItem = viewItem;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateContents
|
||||
{
|
||||
[self updateAudioProgressView];
|
||||
[self updateAudioBottomLabel];
|
||||
|
||||
if (self.audioPlaybackState == AudioPlaybackState_Playing) {
|
||||
[self setAudioIconToPause];
|
||||
} else {
|
||||
[self setAudioIconToPlay];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)audioProgressSeconds
|
||||
{
|
||||
return [self.viewItem audioProgressSeconds];
|
||||
}
|
||||
|
||||
- (CGFloat)audioDurationSeconds
|
||||
{
|
||||
NSNumber *_Nullable audioDurationSeconds = self.viewItem.audioDurationSeconds;
|
||||
if (!audioDurationSeconds) {
|
||||
audioDurationSeconds = @([self.attachmentStream audioDurationSecondsWithoutTransaction]);
|
||||
self.viewItem.audioDurationSeconds = audioDurationSeconds;
|
||||
}
|
||||
return [audioDurationSeconds floatValue];
|
||||
}
|
||||
|
||||
- (AudioPlaybackState)audioPlaybackState
|
||||
{
|
||||
return [self.viewItem audioPlaybackState];
|
||||
}
|
||||
|
||||
- (BOOL)isAudioPlaying
|
||||
{
|
||||
return self.audioPlaybackState == AudioPlaybackState_Playing;
|
||||
}
|
||||
|
||||
- (void)updateAudioBottomLabel
|
||||
{
|
||||
if (self.isAudioPlaying && self.audioProgressSeconds > 0 && self.audioDurationSeconds > 0) {
|
||||
self.audioBottomLabel.text =
|
||||
[NSString stringWithFormat:@"%@ / %@",
|
||||
[ViewControllerUtils formatDurationSeconds:(long)round(self.audioProgressSeconds)],
|
||||
[ViewControllerUtils formatDurationSeconds:(long)round(self.audioDurationSeconds)]];
|
||||
} else {
|
||||
self.audioBottomLabel.text = [NSString
|
||||
stringWithFormat:@"%@", [ViewControllerUtils formatDurationSeconds:(long)round(self.audioDurationSeconds)]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAudioIcon:(UIImage *)icon iconColor:(UIColor *)iconColor
|
||||
{
|
||||
icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
[_audioPlayPauseButton setImage:icon forState:UIControlStateNormal];
|
||||
[_audioPlayPauseButton setImage:icon forState:UIControlStateDisabled];
|
||||
_audioPlayPauseButton.imageView.tintColor = self.bubbleBackgroundColor;
|
||||
_audioPlayPauseButton.backgroundColor = iconColor;
|
||||
_audioPlayPauseButton.layer.cornerRadius
|
||||
= MIN(_audioPlayPauseButton.bounds.size.width, _audioPlayPauseButton.bounds.size.height) * 0.5f;
|
||||
}
|
||||
|
||||
- (void)setAudioIconToPlay
|
||||
{
|
||||
[self setAudioIcon:[UIImage imageNamed:@"audio_play_black_40"]
|
||||
iconColor:(self.isIncoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self audioColorWithOpacity:0.15f])];
|
||||
}
|
||||
|
||||
- (void)setAudioIconToPause
|
||||
{
|
||||
[self setAudioIcon:[UIImage imageNamed:@"audio_pause_black_40"]
|
||||
iconColor:(self.isIncoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self audioColorWithOpacity:0.15f])];
|
||||
}
|
||||
|
||||
- (void)updateAudioProgressView
|
||||
{
|
||||
[self.audioProgressView
|
||||
setProgress:(self.audioDurationSeconds > 0 ? self.audioProgressSeconds / self.audioDurationSeconds : 0.f)];
|
||||
|
||||
self.audioProgressView.horizontalBarColor = [self audioColorWithOpacity:0.75f];
|
||||
self.audioProgressView.progressColor
|
||||
= (self.isAudioPlaying ? [self audioColorWithOpacity:self.isIncoming ? 0.2f : 0.1f]
|
||||
: [self audioColorWithOpacity:0.4f]);
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageMediaData protocol
|
||||
|
||||
- (CGFloat)audioIconHMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)audioIconHSpacing
|
||||
{
|
||||
return 10.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)audioIconVMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)audioIconVMargin
|
||||
{
|
||||
return [OWSAudioMessageView audioIconVMargin];
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.iconSize + self.audioIconVMargin * 2;
|
||||
}
|
||||
|
||||
- (CGFloat)bubbleHeight
|
||||
{
|
||||
return [OWSAudioMessageView bubbleHeight];
|
||||
}
|
||||
|
||||
+ (CGFloat)iconSize
|
||||
{
|
||||
return 40.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return [OWSAudioMessageView iconSize];
|
||||
}
|
||||
|
||||
- (UIColor *)audioTextColor
|
||||
{
|
||||
return (self.isIncoming ? [UIColor colorWithWhite:0.2f alpha:1.f] : [UIColor whiteColor]);
|
||||
}
|
||||
|
||||
- (UIColor *)audioColorWithOpacity:(CGFloat)alpha
|
||||
{
|
||||
return [self.audioTextColor blendWithColor:self.bubbleBackgroundColor alpha:alpha];
|
||||
}
|
||||
|
||||
- (UIColor *)bubbleBackgroundColor
|
||||
{
|
||||
return self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_materialBlueColor];
|
||||
}
|
||||
|
||||
- (BOOL)isVoiceMessage
|
||||
{
|
||||
// We want to treat "pre-voice messages flag" messages as voice messages if
|
||||
// they have no file name.
|
||||
//
|
||||
// TODO: Remove this after the flag has been in production for a few months.
|
||||
return (self.attachmentStream.isVoiceMessage || self.attachmentStream.sourceFilename.length < 1);
|
||||
}
|
||||
|
||||
- (void)createContentsForSize:(CGSize)viewSize
|
||||
{
|
||||
UIColor *textColor = [self audioTextColor];
|
||||
|
||||
self.backgroundColor = self.bubbleBackgroundColor;
|
||||
|
||||
const CGFloat kBubbleTailWidth = 6.f;
|
||||
CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f,
|
||||
self.audioIconVMargin,
|
||||
viewSize.width - kBubbleTailWidth - self.audioIconHMargin,
|
||||
viewSize.height - self.audioIconVMargin * 2);
|
||||
|
||||
CGRect iconFrame = CGRectMake((CGFloat)round(contentFrame.origin.x + self.audioIconHMargin),
|
||||
(CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
|
||||
self.iconSize,
|
||||
self.iconSize);
|
||||
_audioPlayPauseButton = [[UIButton alloc] initWithFrame:iconFrame];
|
||||
_audioPlayPauseButton.enabled = NO;
|
||||
[self addSubview:_audioPlayPauseButton];
|
||||
|
||||
const CGFloat kLabelHSpacing = self.audioIconHSpacing;
|
||||
const CGFloat kLabelVSpacing = 2;
|
||||
NSString *filename = self.attachmentStream.sourceFilename;
|
||||
if (!filename) {
|
||||
filename = [[self.attachmentStream filePath] lastPathComponent];
|
||||
}
|
||||
NSString *topText = [[filename stringByDeletingPathExtension]
|
||||
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (topText.length < 1) {
|
||||
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString;
|
||||
}
|
||||
if (topText.length < 1) {
|
||||
topText = NSLocalizedString(@"GENERIC_ATTACHMENT_LABEL", @"A label for generic attachments.");
|
||||
}
|
||||
if (self.isVoiceMessage) {
|
||||
topText = nil;
|
||||
}
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = topText;
|
||||
topLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
|
||||
[topLabel sizeToFit];
|
||||
[self addSubview:topLabel];
|
||||
|
||||
AudioProgressView *audioProgressView = [AudioProgressView new];
|
||||
self.audioProgressView = audioProgressView;
|
||||
[self updateAudioProgressView];
|
||||
[self addSubview:audioProgressView];
|
||||
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
self.audioBottomLabel = bottomLabel;
|
||||
[self updateAudioBottomLabel];
|
||||
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
|
||||
[bottomLabel sizeToFit];
|
||||
[self addSubview:bottomLabel];
|
||||
|
||||
const CGFloat topLabelHeight = (CGFloat)ceil(topLabel.font.lineHeight);
|
||||
const CGFloat kAudioProgressViewHeight = 12.f;
|
||||
const CGFloat bottomLabelHeight = (CGFloat)ceil(bottomLabel.font.lineHeight);
|
||||
CGRect labelsBounds = CGRectZero;
|
||||
labelsBounds.origin.x = (CGFloat)round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
|
||||
labelsBounds.size.width = contentFrame.origin.x + contentFrame.size.width - labelsBounds.origin.x;
|
||||
labelsBounds.size.height = topLabelHeight + kAudioProgressViewHeight + bottomLabelHeight + kLabelVSpacing * 2;
|
||||
labelsBounds.origin.y
|
||||
= (CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - labelsBounds.size.height) * 0.5f);
|
||||
|
||||
CGFloat y = labelsBounds.origin.y;
|
||||
topLabel.frame = CGRectMake(labelsBounds.origin.x, labelsBounds.origin.y, labelsBounds.size.width, topLabelHeight);
|
||||
y += topLabelHeight + kLabelVSpacing;
|
||||
audioProgressView.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, kAudioProgressViewHeight);
|
||||
y += kAudioProgressViewHeight + kLabelVSpacing;
|
||||
bottomLabel.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, bottomLabelHeight);
|
||||
|
||||
[self updateContents];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSContactOffersInteraction;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactOffersCell : ConversationViewCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
#import "OWSContactOffersCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "NSBundle+JSQMessages.h"
|
||||
#import "OWSContactOffersInteraction.h"
|
||||
#import "UIColor+OWS.h"
|
||||
|
@ -41,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssert(!self.titleLabel);
|
||||
|
||||
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
// [self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
|
||||
self.titleLabel = [UILabel new];
|
||||
self.titleLabel.textColor = [UIColor blackColor];
|
||||
|
@ -86,11 +87,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (void)configureWithInteraction:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)configure
|
||||
{
|
||||
OWSAssert(interaction);
|
||||
OWSAssert(self.viewItem);
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
_interaction = interaction;
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
OWSAssert(
|
||||
interaction.hasBlockOffer || interaction.hasAddToContactsOffer || interaction.hasAddToProfileWhitelistOffer);
|
||||
|
@ -137,6 +139,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
// We're using a bit of a hack to get JSQ to layout this and the unread indicator as
|
||||
// "full width" cells. These cells will end up with an erroneous left margin that we
|
||||
// want to reverse.
|
||||
|
@ -164,15 +168,20 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
};
|
||||
|
||||
layoutButton(self.addToContactsButton, self.interaction.hasAddToContactsOffer);
|
||||
layoutButton(self.addToProfileWhitelistButton, self.interaction.hasAddToProfileWhitelistOffer);
|
||||
layoutButton(self.blockButton, self.interaction.hasBlockOffer);
|
||||
layoutButton(self.addToContactsButton, interaction.hasAddToContactsOffer);
|
||||
layoutButton(self.addToProfileWhitelistButton, interaction.hasAddToProfileWhitelistOffer);
|
||||
layoutButton(self.blockButton, interaction.hasBlockOffer);
|
||||
}
|
||||
|
||||
- (CGSize)bubbleSizeForInteraction:(OWSContactOffersInteraction *)interaction
|
||||
collectionViewWidth:(CGFloat)collectionViewWidth
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth
|
||||
{
|
||||
CGSize result = CGSizeMake(collectionViewWidth, 0);
|
||||
OWSAssert(self.viewItem);
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
// TODO: Should we use viewWidth?
|
||||
CGSize result = CGSizeMake(viewWidth, 0);
|
||||
result.height += self.topVMargin;
|
||||
result.height += self.bottomVMargin;
|
||||
|
||||
|
@ -197,26 +206,26 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)addToContacts
|
||||
{
|
||||
OWSAssert(self.contactOffersCellDelegate);
|
||||
OWSAssert(self.delegate);
|
||||
OWSAssert(self.interaction);
|
||||
|
||||
[self.contactOffersCellDelegate tappedAddToContactsOfferMessage:self.interaction];
|
||||
[self.delegate tappedAddToContactsOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)addToProfileWhitelist
|
||||
{
|
||||
OWSAssert(self.contactOffersCellDelegate);
|
||||
OWSAssert(self.delegate);
|
||||
OWSAssert(self.interaction);
|
||||
|
||||
[self.contactOffersCellDelegate tappedAddToProfileWhitelistOfferMessage:self.interaction];
|
||||
[self.delegate tappedAddToProfileWhitelistOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)block
|
||||
{
|
||||
OWSAssert(self.contactOffersCellDelegate);
|
||||
OWSAssert(self.delegate);
|
||||
OWSAssert(self.interaction);
|
||||
|
||||
[self.contactOffersCellDelegate tappedUnknownContactBlockOfferMessage:self.interaction];
|
||||
[self.delegate tappedUnknownContactBlockOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
|
@ -0,0 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
// TODO:
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
double const OWSExpirationTimerViewBlinkingSeconds = 2;
|
||||
|
||||
@interface OWSExpirationTimerView ()
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface OWSGenericAttachmentView : UIView
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream isIncoming:(BOOL)isIncoming;
|
||||
|
||||
- (void)createContentsForSize:(CGSize)viewSize;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,207 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSGenericAttachmentView.h"
|
||||
#import "UIColor+JSQMessages.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SignalServiceKit/MimeTypeUtil.h>
|
||||
#import <SignalServiceKit/TSAttachmentStream.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSGenericAttachmentView ()
|
||||
|
||||
@property (nonatomic) TSAttachmentStream *attachmentStream;
|
||||
@property (nonatomic) BOOL isIncoming;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSGenericAttachmentView
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream isIncoming:(BOOL)isIncoming
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_attachmentStream = attachmentStream;
|
||||
_isIncoming = isIncoming;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - JSQMessageMediaData protocol
|
||||
|
||||
- (CGFloat)iconHMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconHSpacing
|
||||
{
|
||||
return 10.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)iconVMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconVMargin
|
||||
{
|
||||
return [OWSGenericAttachmentView iconVMargin];
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.iconSize + self.iconVMargin * 2;
|
||||
}
|
||||
|
||||
- (CGFloat)bubbleHeight
|
||||
{
|
||||
return [OWSGenericAttachmentView bubbleHeight];
|
||||
}
|
||||
|
||||
+ (CGFloat)iconSize
|
||||
{
|
||||
return 40.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return [OWSGenericAttachmentView iconSize];
|
||||
}
|
||||
|
||||
- (CGFloat)vMargin
|
||||
{
|
||||
return 10.f;
|
||||
}
|
||||
|
||||
- (UIColor *)bubbleBackgroundColor
|
||||
{
|
||||
return self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_materialBlueColor];
|
||||
}
|
||||
|
||||
- (UIColor *)textColor
|
||||
{
|
||||
return (self.isIncoming ? [UIColor colorWithWhite:0.2f alpha:1.f] : [UIColor whiteColor]);
|
||||
}
|
||||
|
||||
- (UIColor *)foregroundColorWithOpacity:(CGFloat)alpha
|
||||
{
|
||||
return [self.textColor blendWithColor:self.bubbleBackgroundColor alpha:alpha];
|
||||
}
|
||||
|
||||
- (void)createContentsForSize:(CGSize)viewSize
|
||||
{
|
||||
UIColor *textColor = (self.isIncoming ? [UIColor colorWithWhite:0.2 alpha:1.f] : [UIColor whiteColor]);
|
||||
|
||||
self.backgroundColor = self.bubbleBackgroundColor;
|
||||
|
||||
const CGFloat kBubbleTailWidth = 6.f;
|
||||
CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f,
|
||||
self.vMargin,
|
||||
viewSize.width - kBubbleTailWidth - self.iconHMargin,
|
||||
viewSize.height - self.vMargin * 2);
|
||||
|
||||
UIImage *image = [UIImage imageNamed:@"generic-attachment-small"];
|
||||
OWSAssert(image);
|
||||
UIImageView *imageView = [UIImageView new];
|
||||
CGRect iconFrame = CGRectMake(round(contentFrame.origin.x + self.iconHMargin),
|
||||
round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
|
||||
self.iconSize,
|
||||
self.iconSize);
|
||||
imageView.frame = iconFrame;
|
||||
imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
imageView.tintColor = self.bubbleBackgroundColor;
|
||||
imageView.backgroundColor
|
||||
= (self.isIncoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self foregroundColorWithOpacity:0.15f]);
|
||||
imageView.layer.cornerRadius = MIN(imageView.bounds.size.width, imageView.bounds.size.height) * 0.5f;
|
||||
[self addSubview:imageView];
|
||||
|
||||
NSString *filename = self.attachmentStream.sourceFilename;
|
||||
if (!filename) {
|
||||
filename = [[self.attachmentStream filePath] lastPathComponent];
|
||||
}
|
||||
NSString *fileExtension = filename.pathExtension;
|
||||
if (fileExtension.length < 1) {
|
||||
[MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType];
|
||||
}
|
||||
if (fileExtension.length < 1) {
|
||||
fileExtension = NSLocalizedString(@"GENERIC_ATTACHMENT_DEFAULT_TYPE",
|
||||
@"A default label for attachment whose file extension cannot be determined.");
|
||||
}
|
||||
|
||||
UILabel *fileTypeLabel = [UILabel new];
|
||||
fileTypeLabel.text = fileExtension.uppercaseString;
|
||||
fileTypeLabel.textColor = imageView.backgroundColor;
|
||||
fileTypeLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
fileTypeLabel.font = [UIFont ows_mediumFontWithSize:20.f];
|
||||
fileTypeLabel.adjustsFontSizeToFitWidth = YES;
|
||||
fileTypeLabel.textAlignment = NSTextAlignmentCenter;
|
||||
CGRect fileTypeLabelFrame = CGRectZero;
|
||||
fileTypeLabelFrame.size = [fileTypeLabel sizeThatFits:CGSizeZero];
|
||||
// This dimension depends on the space within the icon boundaries.
|
||||
fileTypeLabelFrame.size.width = 15.f;
|
||||
// Center on icon.
|
||||
fileTypeLabelFrame.origin.x
|
||||
= round(iconFrame.origin.x + (iconFrame.size.width - fileTypeLabelFrame.size.width) * 0.5f);
|
||||
fileTypeLabelFrame.origin.y
|
||||
= round(iconFrame.origin.y + (iconFrame.size.height - fileTypeLabelFrame.size.height) * 0.5f);
|
||||
fileTypeLabel.frame = fileTypeLabelFrame;
|
||||
[self addSubview:fileTypeLabel];
|
||||
|
||||
const CGFloat kLabelHSpacing = self.iconHSpacing;
|
||||
const CGFloat kLabelVSpacing = 2;
|
||||
NSString *topText =
|
||||
[self.attachmentStream.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
if (topText.length < 1) {
|
||||
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString;
|
||||
}
|
||||
if (topText.length < 1) {
|
||||
topText = NSLocalizedString(@"GENERIC_ATTACHMENT_LABEL", @"A label for generic attachments.");
|
||||
}
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = topText;
|
||||
topLabel.textColor = textColor;
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 15.f)];
|
||||
[topLabel sizeToFit];
|
||||
[self addSubview:topLabel];
|
||||
|
||||
NSError *error;
|
||||
unsigned long long fileSize =
|
||||
[[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachmentStream filePath] error:&error].fileSize;
|
||||
OWSAssert(!error);
|
||||
NSString *bottomText = [ViewControllerUtils formatFileSize:fileSize];
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
bottomLabel.text = bottomText;
|
||||
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
|
||||
[bottomLabel sizeToFit];
|
||||
[self addSubview:bottomLabel];
|
||||
|
||||
CGRect topLabelFrame = CGRectZero;
|
||||
topLabelFrame.size = topLabel.bounds.size;
|
||||
topLabelFrame.origin.x = round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
|
||||
topLabelFrame.origin.y = round(contentFrame.origin.y
|
||||
+ (contentFrame.size.height - (topLabel.frame.size.height + bottomLabel.frame.size.height + kLabelVSpacing))
|
||||
* 0.5f);
|
||||
topLabelFrame.size.width = round((contentFrame.origin.x + contentFrame.size.width) - topLabelFrame.origin.x);
|
||||
topLabel.frame = topLabelFrame;
|
||||
|
||||
CGRect bottomLabelFrame = topLabelFrame;
|
||||
bottomLabelFrame.origin.y += topLabelFrame.size.height + kLabelVSpacing;
|
||||
bottomLabel.frame = bottomLabelFrame;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
|
||||
//#import "OWSExpirableMessageView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// TODO: Remove this class.
|
||||
@interface OWSIncomingMessageCell : OWSMessageCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSIncomingMessageCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import <SignalServiceKit/TSIncomingMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSIncomingMessageCell ()
|
||||
|
||||
//@property (strong, nonatomic) IBOutlet OWSExpirationTimerView *expirationTimerView;
|
||||
//@property (strong, nonatomic) IBOutlet NSLayoutConstraint *expirationTimerViewWidthConstraint;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSIncomingMessageCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier
|
||||
{
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (BOOL)isIncoming
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
//- (void)awakeFromNib
|
||||
//{
|
||||
// [super awakeFromNib];
|
||||
// self.expirationTimerViewWidthConstraint.constant = 0.0;
|
||||
//}
|
||||
//
|
||||
//- (void)prepareForReuse
|
||||
//{
|
||||
// [super prepareForReuse];
|
||||
// self.expirationTimerViewWidthConstraint.constant = 0.0f;
|
||||
//
|
||||
// [self.mediaAdapter setCellVisible:NO];
|
||||
//
|
||||
// // Clear this adapter's views IFF this was the last cell to use this adapter.
|
||||
// [self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
|
||||
// [_mediaAdapter setLastPresentingCell:nil];
|
||||
//
|
||||
// self.mediaAdapter = nil;
|
||||
//}
|
||||
//
|
||||
//- (void)setMediaAdapter:(nullable id<OWSMessageMediaAdapter>)mediaAdapter
|
||||
//{
|
||||
// _mediaAdapter = mediaAdapter;
|
||||
//
|
||||
// // Mark this as the last cell to use this adapter.
|
||||
// [_mediaAdapter setLastPresentingCell:self];
|
||||
//}
|
||||
//
|
||||
//// pragma mark - OWSMessageCollectionViewCell
|
||||
//
|
||||
//// TODO:
|
||||
//- (void)setCellVisible:(BOOL)isVisible
|
||||
//{
|
||||
// [self.mediaAdapter setCellVisible:isVisible];
|
||||
//}
|
||||
//
|
||||
//// pragma mark - OWSExpirableMessageView
|
||||
//
|
||||
//- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
// initialDurationSeconds:(uint32_t)initialDurationSeconds
|
||||
//{
|
||||
// self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
|
||||
// [self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
|
||||
// initialDurationSeconds:initialDurationSeconds];
|
||||
//}
|
||||
//
|
||||
//- (void)stopExpirationTimer
|
||||
//{
|
||||
// [self.expirationTimerView stopTimer];
|
||||
//}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
//#import "JSQMessagesCollectionViewCell+OWS.h"
|
||||
//#import "OWSExpirableMessageView.h"
|
||||
//#import "OWSMessageMediaAdapter.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSExpirationTimerView;
|
||||
|
||||
// TODO: Move to source.
|
||||
static const CGFloat OWSExpirableMessageViewTimerWidth = 10.0f;
|
||||
|
||||
@interface OWSMessageCell : ConversationViewCell
|
||||
// <OWSExpirableMessageView>
|
||||
|
||||
@property (nonatomic, readonly) OWSExpirationTimerView *expirationTimerView;
|
||||
@property (nonatomic, readonly) NSLayoutConstraint *expirationTimerViewWidthConstraint;
|
||||
|
||||
- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
initialDurationSeconds:(uint32_t)initialDurationSeconds;
|
||||
|
||||
- (void)stopExpirationTimer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,739 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "AttachmentSharing.h"
|
||||
#import "AttachmentUploadView.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "OWSAudioMessageView.h"
|
||||
#import "OWSGenericAttachmentView.h"
|
||||
#import "UIColor+OWS.h"
|
||||
|
||||
//#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import "Signal-Swift.h"
|
||||
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
|
||||
|
||||
//#import "OWSExpirationTimerView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageCell ()
|
||||
|
||||
// The text label is used so frequently that we always keep one around.
|
||||
@property (nonatomic) UILabel *textLabel;
|
||||
@property (nonatomic, nullable) UIImageView *bubbleImageView;
|
||||
@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView;
|
||||
@property (nonatomic, nullable) UIImageView *stillImageView;
|
||||
@property (nonatomic, nullable) YYAnimatedImageView *animatedImageView;
|
||||
@property (nonatomic, nullable) UIView *customView;
|
||||
@property (nonatomic, nullable) AttachmentPointerView *attachmentPointerView;
|
||||
@property (nonatomic, nullable) OWSGenericAttachmentView *attachmentView;
|
||||
@property (nonatomic, nullable) OWSAudioMessageView *audioMessageView;
|
||||
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *contentConstraints;
|
||||
|
||||
//@property (strong, nonatomic) OWSExpirationTimerView *expirationTimerView;
|
||||
//@property (strong, nonatomic) NSLayoutConstraint *expirationTimerViewWidthConstraint;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSMessageCell
|
||||
|
||||
// `[UIView init]` invokes `[self initWithFrame:...]`.
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self commontInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commontInit
|
||||
{
|
||||
OWSAssert(!self.textLabel);
|
||||
|
||||
// [self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
|
||||
self.contentView.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
self.bubbleImageView = [UIImageView new];
|
||||
self.bubbleImageView.layoutMargins = UIEdgeInsetsZero;
|
||||
self.bubbleImageView.userInteractionEnabled = NO;
|
||||
[self.contentView addSubview:self.bubbleImageView];
|
||||
[self.bubbleImageView autoPinToSuperviewEdges];
|
||||
|
||||
self.textLabel = [UILabel new];
|
||||
self.textLabel.font = [UIFont ows_regularFontWithSize:16.f];
|
||||
self.textLabel.numberOfLines = 0;
|
||||
self.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
self.textLabel.textAlignment = NSTextAlignmentLeft;
|
||||
[self.bubbleImageView addSubview:self.textLabel];
|
||||
OWSAssert(self.textLabel.superview);
|
||||
|
||||
// Hide these views by default.
|
||||
self.bubbleImageView.hidden = YES;
|
||||
self.textLabel.hidden = YES;
|
||||
|
||||
UITapGestureRecognizer *tap =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
|
||||
[self addGestureRecognizer:tap];
|
||||
|
||||
UILongPressGestureRecognizer *longPress =
|
||||
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
|
||||
[self addGestureRecognizer:longPress];
|
||||
}
|
||||
|
||||
- (OWSMessageCellType)cellType
|
||||
{
|
||||
return self.viewItem.messageCellType;
|
||||
}
|
||||
|
||||
- (nullable NSString *)textMessage
|
||||
{
|
||||
return self.viewItem.textMessage;
|
||||
}
|
||||
|
||||
- (nullable TSAttachmentStream *)attachmentStream
|
||||
{
|
||||
return self.viewItem.attachmentStream;
|
||||
}
|
||||
|
||||
- (nullable TSAttachmentPointer *)attachmentPointer
|
||||
{
|
||||
return self.viewItem.attachmentPointer;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize
|
||||
{
|
||||
return self.viewItem.contentSize;
|
||||
}
|
||||
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSAssert(self.viewItem);
|
||||
OWSAssert(self.viewItem.interaction);
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
|
||||
|
||||
BOOL isIncoming = self.isIncoming;
|
||||
JSQMessagesBubbleImage *bubbleImageData
|
||||
= isIncoming ? [self.bubbleFactory incoming] : [self.bubbleFactory outgoing];
|
||||
self.bubbleImageView.image = bubbleImageData.messageBubbleImage;
|
||||
|
||||
switch (self.cellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
[self loadForTextDisplay];
|
||||
break;
|
||||
case OWSMessageCellType_StillImage:
|
||||
[self loadForStillImageDisplay];
|
||||
break;
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
[self loadForAnimatedImageDisplay];
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
[self loadForAudioDisplay];
|
||||
break;
|
||||
case OWSMessageCellType_Video:
|
||||
[self loadForVideoDisplay];
|
||||
break;
|
||||
case OWSMessageCellType_GenericAttachment: {
|
||||
self.attachmentView =
|
||||
[[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming];
|
||||
[self.attachmentView createContentsForSize:self.bounds.size];
|
||||
[self replaceBubbleWithView:self.attachmentView];
|
||||
[self addAttachmentUploadViewIfNecessary:self.attachmentView];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
[self loadForDownloadingAttachment];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an outgoing attachment and we haven't created a
|
||||
// AttachmentUploadView yet, do so now.
|
||||
//
|
||||
// For some attachment types, we may create this view earlier
|
||||
// so that we can take advantage of its callback.
|
||||
// if (self.attachmentStream &&
|
||||
// !self.isIncoming &&
|
||||
// !self.attachmentUploadView) {
|
||||
// self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream
|
||||
// superview:imageView
|
||||
// attachmentStateCallback:^(BOOL isAttachmentReady) {
|
||||
// }];
|
||||
// }
|
||||
|
||||
// [self.textLabel addBorderWithColor:[UIColor blueColor]];
|
||||
// [self.bubbleImageView addBorderWithColor:[UIColor greenColor]];
|
||||
|
||||
// dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// NSLog(@"---- %@", self.viewItem.interaction.debugDescription);
|
||||
// NSLog(@"cell: %@", NSStringFromCGRect(self.frame));
|
||||
// NSLog(@"contentView: %@", NSStringFromCGRect(self.contentView.frame));
|
||||
// NSLog(@"textLabel: %@", NSStringFromCGRect(self.textLabel.frame));
|
||||
// NSLog(@"bubbleImageView: %@", NSStringFromCGRect(self.bubbleImageView.frame));
|
||||
// });
|
||||
}
|
||||
|
||||
- (void)loadForTextDisplay
|
||||
{
|
||||
self.bubbleImageView.hidden = NO;
|
||||
self.textLabel.hidden = NO;
|
||||
self.textLabel.text = self.textMessage;
|
||||
self.textLabel.textColor = [self textColor];
|
||||
|
||||
self.contentConstraints = @[
|
||||
[self.textLabel autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin],
|
||||
[self.textLabel autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin],
|
||||
[self.textLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin],
|
||||
[self.textLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin],
|
||||
];
|
||||
}
|
||||
|
||||
- (void)loadForStillImageDisplay
|
||||
{
|
||||
OWSAssert(self.attachmentStream);
|
||||
OWSAssert([self.attachmentStream isImage]);
|
||||
|
||||
UIImage *_Nullable image = self.attachmentStream.image;
|
||||
if (!image) {
|
||||
DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]);
|
||||
[self showAttachmentErrorView];
|
||||
return;
|
||||
}
|
||||
|
||||
self.stillImageView = [[UIImageView alloc] initWithImage:image];
|
||||
// Use trilinear filters for better scaling quality at
|
||||
// some performance cost.
|
||||
self.stillImageView.layer.minificationFilter = kCAFilterTrilinear;
|
||||
self.stillImageView.layer.magnificationFilter = kCAFilterTrilinear;
|
||||
[self replaceBubbleWithView:self.stillImageView];
|
||||
[self addAttachmentUploadViewIfNecessary:self.stillImageView];
|
||||
}
|
||||
|
||||
- (void)loadForAnimatedImageDisplay
|
||||
{
|
||||
OWSAssert(self.attachmentStream);
|
||||
OWSAssert([self.attachmentStream isAnimated]);
|
||||
|
||||
NSString *_Nullable filePath = [self.attachmentStream filePath];
|
||||
YYImage *_Nullable animatedImage = nil;
|
||||
if (filePath && [NSData ows_isValidImageAtPath:filePath]) {
|
||||
animatedImage = [YYImage imageWithContentsOfFile:filePath];
|
||||
}
|
||||
if (!animatedImage) {
|
||||
DDLogError(@"%@ Could not load animated image: %@", [self logTag], [self.attachmentStream mediaURL]);
|
||||
[self showAttachmentErrorView];
|
||||
return;
|
||||
}
|
||||
|
||||
self.animatedImageView = [[YYAnimatedImageView alloc] init];
|
||||
self.animatedImageView.image = animatedImage;
|
||||
[self replaceBubbleWithView:self.animatedImageView];
|
||||
[self addAttachmentUploadViewIfNecessary:self.animatedImageView];
|
||||
}
|
||||
|
||||
- (void)loadForAudioDisplay
|
||||
{
|
||||
OWSAssert(self.attachmentStream);
|
||||
OWSAssert([self.attachmentStream isAudio]);
|
||||
|
||||
self.audioMessageView = [[OWSAudioMessageView alloc] initWithAttachment:self.attachmentStream
|
||||
isIncoming:self.isIncoming
|
||||
viewItem:self.viewItem];
|
||||
self.viewItem.lastAudioMessageView = self.audioMessageView;
|
||||
[self.audioMessageView createContentsForSize:self.bounds.size];
|
||||
[self replaceBubbleWithView:self.audioMessageView];
|
||||
[self addAttachmentUploadViewIfNecessary:self.audioMessageView];
|
||||
}
|
||||
|
||||
- (void)loadForVideoDisplay
|
||||
{
|
||||
OWSAssert(self.attachmentStream);
|
||||
OWSAssert([self.attachmentStream isVideo]);
|
||||
|
||||
// CGSize size = [self mediaViewDisplaySize];
|
||||
|
||||
UIImage *_Nullable image = self.attachmentStream.image;
|
||||
if (!image) {
|
||||
DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]);
|
||||
[self showAttachmentErrorView];
|
||||
return;
|
||||
}
|
||||
|
||||
self.stillImageView = [[UIImageView alloc] initWithImage:image];
|
||||
// Use trilinear filters for better scaling quality at
|
||||
// some performance cost.
|
||||
self.stillImageView.layer.minificationFilter = kCAFilterTrilinear;
|
||||
self.stillImageView.layer.magnificationFilter = kCAFilterTrilinear;
|
||||
[self replaceBubbleWithView:self.stillImageView];
|
||||
|
||||
UIImage *videoPlayIcon = [UIImage imageNamed:@"play_button"];
|
||||
UIImageView *videoPlayButton = [[UIImageView alloc] initWithImage:videoPlayIcon];
|
||||
[self.stillImageView addSubview:videoPlayButton];
|
||||
[videoPlayButton autoCenterInSuperview];
|
||||
[self addAttachmentUploadViewIfNecessary:self.stillImageView
|
||||
attachmentStateCallback:^(BOOL isAttachmentReady) {
|
||||
videoPlayButton.hidden = !isAttachmentReady;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)loadForDownloadingAttachment
|
||||
{
|
||||
OWSAssert(self.attachmentPointer);
|
||||
|
||||
self.customView = [UIView new];
|
||||
switch (self.attachmentPointer.state) {
|
||||
case TSAttachmentPointerStateEnqueued:
|
||||
self.customView.backgroundColor
|
||||
= (self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_fadedBlueColor]);
|
||||
break;
|
||||
case TSAttachmentPointerStateDownloading:
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(attachmentDownloadProgress:)
|
||||
name:kAttachmentDownloadProgressNotification
|
||||
object:nil];
|
||||
self.customView.backgroundColor
|
||||
= (self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_fadedBlueColor]);
|
||||
break;
|
||||
case TSAttachmentPointerStateFailed:
|
||||
self.customView.backgroundColor = [UIColor grayColor];
|
||||
break;
|
||||
}
|
||||
[self replaceBubbleWithView:self.customView];
|
||||
|
||||
self.attachmentPointerView =
|
||||
[[AttachmentPointerView alloc] initWithAttachmentPointer:self.attachmentPointer isIncoming:self.isIncoming];
|
||||
[self.customView addSubview:self.attachmentPointerView];
|
||||
[self.attachmentPointerView autoPinWidthToSuperviewWithMargin:20.f];
|
||||
[self.attachmentPointerView autoVCenterInSuperview];
|
||||
}
|
||||
|
||||
- (void)replaceBubbleWithView:(UIView *)view
|
||||
{
|
||||
OWSAssert(view);
|
||||
|
||||
view.userInteractionEnabled = NO;
|
||||
[self.contentView addSubview:view];
|
||||
self.contentConstraints = [view autoPinToSuperviewEdges];
|
||||
[self cropViewToBubbbleShape:view];
|
||||
if (self.isMediaBeingSent) {
|
||||
view.layer.opacity = 0.75f;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addAttachmentUploadViewIfNecessary:(UIView *)attachmentView
|
||||
{
|
||||
[self addAttachmentUploadViewIfNecessary:attachmentView
|
||||
attachmentStateCallback:^(BOOL isAttachmentReady){
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)addAttachmentUploadViewIfNecessary:(UIView *)attachmentView
|
||||
attachmentStateCallback:(AttachmentStateBlock)attachmentStateCallback
|
||||
{
|
||||
OWSAssert(attachmentView);
|
||||
OWSAssert(attachmentStateCallback);
|
||||
|
||||
if (!self.isIncoming) {
|
||||
self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream
|
||||
superview:attachmentView
|
||||
attachmentStateCallback:attachmentStateCallback];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cropViewToBubbbleShape:(UIView *)view
|
||||
{
|
||||
// OWSAssert(CGRectEqualToRect(self.bounds, self.contentView.frame));
|
||||
// DDLogError(@"cropViewToBubbbleShape: %@ %@", self.viewItem.interaction.uniqueId,
|
||||
// self.viewItem.interaction.description); DDLogError(@"\t %@ %@ %@ %@",
|
||||
// NSStringFromCGRect(self.frame),
|
||||
// NSStringFromCGRect(self.contentView.frame),
|
||||
// NSStringFromCGRect(view.frame),
|
||||
// NSStringFromCGRect(view.superview.bounds));
|
||||
|
||||
// view.frame = view.superview.bounds;
|
||||
view.frame = self.bounds;
|
||||
[JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:view isOutgoing:!self.isIncoming];
|
||||
}
|
||||
|
||||
//// TODO:
|
||||
//- (void)setFrame:(CGRect)frame {
|
||||
// [super setFrame:frame];
|
||||
//
|
||||
// DDLogError(@"setFrame: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description,
|
||||
// NSStringFromCGRect(frame));
|
||||
//}
|
||||
//
|
||||
//// TODO:
|
||||
//- (void)setBounds:(CGRect)bounds {
|
||||
// [super setBounds:bounds];
|
||||
//
|
||||
// DDLogError(@"setBounds: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description,
|
||||
// NSStringFromCGRect(bounds));
|
||||
//}
|
||||
|
||||
- (void)showAttachmentErrorView
|
||||
{
|
||||
// TODO: We could do a better job of indicating that the image could not be loaded.
|
||||
self.customView = [UIView new];
|
||||
self.customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
|
||||
self.customView.userInteractionEnabled = NO;
|
||||
[self.contentView addSubview:self.customView];
|
||||
self.contentConstraints = [self.customView autoPinToSuperviewEdges];
|
||||
[self cropViewToBubbbleShape:self.customView];
|
||||
}
|
||||
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth
|
||||
{
|
||||
OWSAssert(self.viewItem);
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
|
||||
|
||||
switch (self.cellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage: {
|
||||
BOOL isRTL = self.isRTL;
|
||||
CGFloat leftMargin = isRTL ? self.textTrailingMargin : self.textLeadingMargin;
|
||||
CGFloat rightMargin = isRTL ? self.textLeadingMargin : self.textTrailingMargin;
|
||||
CGFloat textVMargin = self.textVMargin;
|
||||
CGFloat maxTextWidth = maxMessageWidth - (leftMargin + rightMargin);
|
||||
|
||||
self.textLabel.text = self.textMessage;
|
||||
CGSize textSize = [self.textLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)];
|
||||
CGSize result = CGSizeMake((CGFloat)ceil(textSize.width + leftMargin + rightMargin),
|
||||
(CGFloat)ceil(textSize.height + textVMargin * 2));
|
||||
// NSLog(@"???? %@", self.viewItem.interaction.debugDescription);
|
||||
// NSLog(@"\t %@", messageBody);
|
||||
// NSLog(@"textSize: %@", NSStringFromCGSize(textSize));
|
||||
// NSLog(@"result: %@", NSStringFromCGSize(result));
|
||||
return result;
|
||||
}
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
case OWSMessageCellType_Video: {
|
||||
OWSAssert(self.contentSize.width > 0);
|
||||
OWSAssert(self.contentSize.height > 0);
|
||||
|
||||
// TODO: Adjust this behavior.
|
||||
// TODO: This behavior is a bit different than the old behavior defined
|
||||
// in JSQMediaItem+OWS.h. Let's discuss.
|
||||
const CGFloat maxContentWidth = maxMessageWidth;
|
||||
const CGFloat maxContentHeight = maxMessageWidth;
|
||||
CGFloat contentWidth = (CGFloat)round(maxContentWidth);
|
||||
CGFloat contentHeight = (CGFloat)round(maxContentWidth * self.contentSize.height / self.contentSize.width);
|
||||
if (contentHeight > maxContentHeight) {
|
||||
contentWidth = (CGFloat)round(maxContentHeight * self.contentSize.width / self.contentSize.height);
|
||||
contentHeight = (CGFloat)round(maxContentHeight);
|
||||
}
|
||||
CGSize result = CGSizeMake(contentWidth, contentHeight);
|
||||
// DDLogError(@"measuring: %@ %@ %@", self.viewItem.interaction.uniqueId,
|
||||
// self.viewItem.interaction.description, self.attachmentStream.contentType); DDLogError(@"\t
|
||||
// contentSize: %@", NSStringFromCGSize(self.contentSize)); DDLogError(@"\t result: %@",
|
||||
// NSStringFromCGSize(result));
|
||||
return result;
|
||||
}
|
||||
case OWSMessageCellType_Audio:
|
||||
return CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight);
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]);
|
||||
case OWSMessageCellType_DownloadingAttachment:
|
||||
return CGSizeMake(200, 90);
|
||||
}
|
||||
|
||||
return CGSizeMake(maxMessageWidth, maxMessageWidth);
|
||||
}
|
||||
|
||||
- (BOOL)isIncoming
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (CGFloat)textLeadingMargin
|
||||
{
|
||||
return self.isIncoming ? 15 : 10;
|
||||
}
|
||||
|
||||
- (CGFloat)textTrailingMargin
|
||||
{
|
||||
return self.isIncoming ? 10 : 15;
|
||||
}
|
||||
|
||||
- (CGFloat)textVMargin
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
- (UIColor *)textColor
|
||||
{
|
||||
return self.isIncoming ? [UIColor blackColor] : [UIColor whiteColor];
|
||||
}
|
||||
|
||||
- (BOOL)isMediaBeingSent
|
||||
{
|
||||
if (self.isIncoming) {
|
||||
return NO;
|
||||
}
|
||||
if (!self.attachmentStream) {
|
||||
return NO;
|
||||
}
|
||||
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
|
||||
return outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut;
|
||||
}
|
||||
|
||||
- (OWSMessagesBubbleImageFactory *)bubbleFactory
|
||||
{
|
||||
static OWSMessagesBubbleImageFactory *instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [OWSMessagesBubbleImageFactory new];
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
- (void)prepareForReuse
|
||||
{
|
||||
[super prepareForReuse];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
[NSLayoutConstraint deactivateConstraints:self.contentConstraints];
|
||||
self.contentConstraints = nil;
|
||||
|
||||
// The text label is used so frequently that we always keep one around.
|
||||
self.textLabel.text = nil;
|
||||
self.textLabel.hidden = YES;
|
||||
self.bubbleImageView.image = nil;
|
||||
self.bubbleImageView.hidden = YES;
|
||||
|
||||
[self.stillImageView removeFromSuperview];
|
||||
self.stillImageView = nil;
|
||||
[self.animatedImageView removeFromSuperview];
|
||||
self.animatedImageView = nil;
|
||||
[self.customView removeFromSuperview];
|
||||
self.customView = nil;
|
||||
[self.attachmentPointerView removeFromSuperview];
|
||||
self.attachmentPointerView = nil;
|
||||
[self.attachmentView removeFromSuperview];
|
||||
self.attachmentView = nil;
|
||||
[self.audioMessageView removeFromSuperview];
|
||||
self.audioMessageView = nil;
|
||||
self.attachmentUploadView = nil;
|
||||
}
|
||||
|
||||
//- (void)awakeFromNib
|
||||
//{
|
||||
// [super awakeFromNib];
|
||||
// self.expirationTimerViewWidthConstraint.constant = 0.0;
|
||||
//
|
||||
// // Our text alignment needs to adapt to RTL.
|
||||
// self.cellBottomLabel.textAlignment = [self.cellBottomLabel textAlignmentUnnatural];
|
||||
//}
|
||||
//
|
||||
//- (void)prepareForReuse
|
||||
//{
|
||||
// [super prepareForReuse];
|
||||
// self.mediaView.alpha = 1.0;
|
||||
// self.expirationTimerViewWidthConstraint.constant = 0.0f;
|
||||
//
|
||||
// [self.mediaAdapter setCellVisible:NO];
|
||||
//
|
||||
// // Clear this adapter's views IFF this was the last cell to use this adapter.
|
||||
// [self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
|
||||
// [_mediaAdapter setLastPresentingCell:nil];
|
||||
//
|
||||
// self.mediaAdapter = nil;
|
||||
//}
|
||||
//
|
||||
//- (void)setMediaAdapter:(nullable id<OWSMessageMediaAdapter>)mediaAdapter
|
||||
//{
|
||||
// _mediaAdapter = mediaAdapter;
|
||||
//
|
||||
// // Mark this as the last cell to use this adapter.
|
||||
// [_mediaAdapter setLastPresentingCell:self];
|
||||
//}
|
||||
//
|
||||
//// pragma mark - OWSMessageCollectionViewCell
|
||||
//
|
||||
//- (void)setCellVisible:(BOOL)isVisible
|
||||
//{
|
||||
// [self.mediaAdapter setCellVisible:isVisible];
|
||||
//}
|
||||
//
|
||||
//- (UIColor *)ows_textColor
|
||||
//{
|
||||
// return [UIColor whiteColor];
|
||||
//}
|
||||
//
|
||||
//// pragma mark - OWSExpirableMessageView
|
||||
//
|
||||
//- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
// initialDurationSeconds:(uint32_t)initialDurationSeconds
|
||||
//{
|
||||
// self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
|
||||
// [self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
|
||||
// initialDurationSeconds:initialDurationSeconds];
|
||||
//}
|
||||
//
|
||||
//- (void)stopExpirationTimer
|
||||
//{
|
||||
// [self.expirationTimerView stopTimer];
|
||||
//}
|
||||
|
||||
#pragma mark - Notifications:
|
||||
|
||||
// TODO: Move this logic into AttachmentPointerView.
|
||||
- (void)attachmentDownloadProgress:(NSNotification *)notification
|
||||
{
|
||||
NSNumber *progress = notification.userInfo[kAttachmentDownloadProgressKey];
|
||||
NSString *attachmentId = notification.userInfo[kAttachmentDownloadAttachmentIDKey];
|
||||
if (!self.attachmentPointer || ![self.attachmentPointer.uniqueId isEqualToString:attachmentId]) {
|
||||
OWSFail(@"%@ Unexpected attachment progress notification: %@", self.logTag, attachmentId);
|
||||
return;
|
||||
}
|
||||
self.attachmentPointerView.progress = progress.floatValue;
|
||||
}
|
||||
|
||||
#pragma mark - Gesture recognizers
|
||||
|
||||
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
|
||||
{
|
||||
OWSAssert(self.delegate);
|
||||
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
|
||||
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
|
||||
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
|
||||
if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) {
|
||||
[self.delegate didTapFailedOutgoingMessage:outgoingMessage];
|
||||
return;
|
||||
} else if (outgoingMessage.messageState == TSOutgoingMessageStateAttemptingOut) {
|
||||
// Ignore taps on outgoing messages being sent.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (self.cellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
break;
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
[self.delegate didTapOversizeTextMessage:self.textMessage attachmentStream:self.attachmentStream];
|
||||
break;
|
||||
case OWSMessageCellType_StillImage:
|
||||
[self.delegate didTapImageViewItem:self.viewItem
|
||||
attachmentStream:self.attachmentStream
|
||||
imageView:self.stillImageView];
|
||||
break;
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
[self.delegate didTapImageViewItem:self.viewItem
|
||||
attachmentStream:self.attachmentStream
|
||||
imageView:self.animatedImageView];
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
[self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
||||
return;
|
||||
case OWSMessageCellType_Video:
|
||||
[self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream];
|
||||
return;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
// [self.delegate didTapGenericAttachment:self.viewItem
|
||||
// attachmentStream:self.attachmentStream];
|
||||
break;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
OWSAssert(self.attachmentPointer);
|
||||
if (self.attachmentPointer.state == TSAttachmentPointerStateFailed) {
|
||||
[self.delegate didTapFailedIncomingAttachment:self.viewItem
|
||||
attachmentPointer:self.attachmentPointer];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DDLogInfo(@"%@ Ignoring tap on message: %@", self.logTag, self.viewItem.interaction.debugDescription);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)sender
|
||||
{
|
||||
OWSAssert(self.delegate);
|
||||
|
||||
// We "eagerly" respond when the long press begins, not when it ends.
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
CGPoint location = [sender locationInView:self];
|
||||
[self showMenuController:location];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIMenuController
|
||||
|
||||
- (void)showMenuController:(CGPoint)fromLocation
|
||||
{
|
||||
[self becomeFirstResponder];
|
||||
|
||||
if ([UIMenuController sharedMenuController].isMenuVisible) {
|
||||
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
|
||||
}
|
||||
|
||||
// We use custom action selectors so that we can control
|
||||
// the ordering of the actions in the menu.
|
||||
NSArray *menuItems = self.viewItem.menuControllerItems;
|
||||
[UIMenuController sharedMenuController].menuItems = menuItems;
|
||||
CGRect targetRect = CGRectMake(fromLocation.x, fromLocation.y, 1, 1);
|
||||
[[UIMenuController sharedMenuController] setTargetRect:targetRect inView:self];
|
||||
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
||||
{
|
||||
return [self.viewItem canPerformAction:action];
|
||||
}
|
||||
|
||||
- (void)copyAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem copyAction];
|
||||
}
|
||||
|
||||
- (void)shareAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem shareAction];
|
||||
}
|
||||
|
||||
- (void)saveAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem saveAction];
|
||||
}
|
||||
|
||||
- (void)deleteAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem deleteAction];
|
||||
}
|
||||
|
||||
- (void)metadataAction:(nullable id)sender
|
||||
{
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
|
||||
|
||||
[self.delegate showMetadataViewForMessage:(TSMessage *)self.viewItem.interaction];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)logTag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)logTag
|
||||
{
|
||||
return self.class.logTag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// TODO: Remove this class.
|
||||
@interface OWSOutgoingMessageCell : OWSMessageCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSOutgoingMessageCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalServiceKit/TSOutgoingMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSOutgoingMessageCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier
|
||||
{
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (BOOL)isIncoming
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
//- (void)prepareForReuse
|
||||
//{
|
||||
// [super prepareForReuse];
|
||||
// self.mediaView.alpha = 1.0;
|
||||
// self.expirationTimerViewWidthConstraint.constant = 0.0f;
|
||||
//
|
||||
// [self.mediaAdapter setCellVisible:NO];
|
||||
//
|
||||
// // Clear this adapter's views IFF this was the last cell to use this adapter.
|
||||
// [self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
|
||||
// [_mediaAdapter setLastPresentingCell:nil];
|
||||
//
|
||||
// self.mediaAdapter = nil;
|
||||
//}
|
||||
//
|
||||
//// pragma mark - OWSExpirableMessageView
|
||||
//
|
||||
//- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
// initialDurationSeconds:(uint32_t)initialDurationSeconds
|
||||
//{
|
||||
// self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
|
||||
// [self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
|
||||
// initialDurationSeconds:initialDurationSeconds];
|
||||
//}
|
||||
//
|
||||
//- (void)stopExpirationTimer
|
||||
//{
|
||||
// [self.expirationTimerView stopTimer];
|
||||
//}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSInteraction;
|
||||
|
||||
@interface OWSSystemMessageCell : ConversationViewCell
|
||||
|
||||
//- (CGSize)bubbleSizeForInteraction:(TSInteraction *)interaction collectionViewWidth:(CGFloat)collectionViewWidth;
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
#import "OWSSystemMessageCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "Environment.h"
|
||||
#import "NSBundle+JSQMessages.h"
|
||||
#import "OWSContactsManager.h"
|
||||
|
@ -84,17 +85,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (void)configureWithInteraction:(TSInteraction *)interaction;
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSAssert(interaction);
|
||||
OWSAssert(self.viewItem);
|
||||
|
||||
_interaction = interaction;
|
||||
TSInteraction *interaction = self.viewItem.interaction;
|
||||
|
||||
UIImage *icon = [self iconForInteraction:self.interaction];
|
||||
UIImage *icon = [self iconForInteraction:interaction];
|
||||
self.imageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
self.imageView.tintColor = [self iconColorForInteraction:self.interaction];
|
||||
self.imageView.tintColor = [self iconColorForInteraction:interaction];
|
||||
self.titleLabel.textColor = [self textColor];
|
||||
[self applyTitleForInteraction:self.interaction label:self.titleLabel];
|
||||
[self applyTitleForInteraction:interaction label:self.titleLabel];
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
@ -120,8 +121,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
UIImage *result = nil;
|
||||
|
||||
// TODO: Don't cast.
|
||||
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
|
||||
switch (((TSErrorMessage *)self.interaction).errorType) {
|
||||
switch (((TSErrorMessage *)interaction).errorType) {
|
||||
case TSErrorMessageNonBlockingIdentityChange:
|
||||
case TSErrorMessageWrongTrustedIdentityKey:
|
||||
result = [UIImage imageNamed:@"system_message_security"];
|
||||
|
@ -138,7 +140,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
break;
|
||||
}
|
||||
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
|
||||
switch (((TSInfoMessage *)self.interaction).messageType) {
|
||||
switch (((TSInfoMessage *)interaction).messageType) {
|
||||
case TSInfoMessageUserNotRegistered:
|
||||
case TSInfoMessageTypeSessionDidEnd:
|
||||
case TSInfoMessageTypeUnsupportedMessage:
|
||||
|
@ -272,33 +274,49 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self iconSize],
|
||||
[self iconSize]);
|
||||
|
||||
// DDLogError(@"system: %@", self.viewItem.interaction.description);
|
||||
// DDLogError(@"\t cell: %@", NSStringFromCGRect(self.frame));
|
||||
// DDLogError(@"\t self.contentView: %@", NSStringFromCGRect(self.contentView.frame));
|
||||
// DDLogError(@"\t imageView: %@", NSStringFromCGRect(self.imageView.frame));
|
||||
// DDLogError(@"\t titleLabel: %@", NSStringFromCGRect(self.titleLabel.frame));
|
||||
// [DDLog flushLog];
|
||||
|
||||
self.titleLabel.frame = CGRectMake(titleLeft,
|
||||
round((self.contentView.height - titleSize.height + topLabelSpacing) * 0.5f),
|
||||
ceil(titleSize.width + 1.f),
|
||||
ceil(titleSize.height + 1.f));
|
||||
|
||||
// [self addRedBorder];
|
||||
}
|
||||
|
||||
- (CGSize)bubbleSizeForInteraction:(TSInteraction *)interaction collectionViewWidth:(CGFloat)collectionViewWidth
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth
|
||||
{
|
||||
CGSize result = CGSizeMake(collectionViewWidth, 0);
|
||||
OWSAssert(self.viewItem);
|
||||
|
||||
TSInteraction *interaction = self.viewItem.interaction;
|
||||
|
||||
// TODO: Should we use maxMessageWidth?
|
||||
CGSize result = CGSizeMake(viewWidth, 0);
|
||||
result.height += self.topVMargin;
|
||||
result.height += self.bottomVMargin;
|
||||
|
||||
[self applyTitleForInteraction:interaction label:self.titleLabel];
|
||||
CGFloat maxTitleWidth = (collectionViewWidth - ([self hMargin] * 2.f + [self hSpacing] + [self iconSize]));
|
||||
CGFloat maxTitleWidth = (viewWidth - ([self hMargin] * 2.f + [self hSpacing] + [self iconSize]));
|
||||
CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)];
|
||||
|
||||
CGFloat contentHeight = ceil(MAX([self iconSize], titleSize.height));
|
||||
result.height += contentHeight;
|
||||
|
||||
// DDLogError(@"system?: %@", self.viewItem.interaction.description);
|
||||
// DDLogError(@"\t result: %@", NSStringFromCGSize(result));
|
||||
// [DDLog flushLog];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)prepareForReuse
|
||||
{
|
||||
[super prepareForReuse];
|
||||
|
||||
self.interaction = nil;
|
||||
}
|
||||
|
||||
#pragma mark - editing
|
||||
|
@ -308,30 +326,38 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return YES;
|
||||
}
|
||||
|
||||
- (void)delete:(nullable id)sender
|
||||
- (void) delete:(nullable id)sender
|
||||
{
|
||||
DDLogInfo(@"%@ chose delete", self.logTag);
|
||||
OWSAssert(self.interaction);
|
||||
|
||||
[self.interaction remove];
|
||||
TSInteraction *interaction = self.viewItem.interaction;
|
||||
OWSAssert(interaction);
|
||||
|
||||
[interaction remove];
|
||||
}
|
||||
|
||||
#pragma mark - Gesture recognizers
|
||||
|
||||
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
|
||||
{
|
||||
OWSAssert(self.interaction);
|
||||
OWSAssert(self.delegate);
|
||||
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.systemMessageCellDelegate didTapSystemMessageWithInteraction:self.interaction];
|
||||
TSInteraction *interaction = self.viewItem.interaction;
|
||||
OWSAssert(interaction);
|
||||
[self.delegate didTapSystemMessageWithInteraction:interaction];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
|
||||
{
|
||||
OWSAssert(self.interaction);
|
||||
OWSAssert(self.delegate);
|
||||
|
||||
TSInteraction *interaction = self.viewItem.interaction;
|
||||
OWSAssert(interaction);
|
||||
|
||||
if (longPress.state == UIGestureRecognizerStateBegan) {
|
||||
[self.systemMessageCellDelegate didLongPressSystemMessageCell:self];
|
||||
[self.delegate didLongPressSystemMessageCell:self fromView:self.titleLabel];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSUnreadIndicatorInteraction;
|
||||
|
||||
@interface OWSUnreadIndicatorCell : ConversationViewCell
|
||||
|
||||
//- (CGSize)bubbleSizeForInteraction:(TSUnreadIndicatorInteraction *)interaction
|
||||
// collectionViewWidth:(CGFloat)collectionViewWidth;
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
#import "OWSUnreadIndicatorCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "NSBundle+JSQMessages.h"
|
||||
#import "TSUnreadIndicatorInteraction.h"
|
||||
#import "UIColor+OWS.h"
|
||||
|
@ -83,14 +84,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (void)configureWithInteraction:(TSUnreadIndicatorInteraction *)interaction;
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSAssert(interaction);
|
||||
OWSAssert(self.viewItem);
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]);
|
||||
|
||||
_interaction = interaction;
|
||||
TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction;
|
||||
|
||||
self.titleLabel.text = [self titleForInteraction:self.interaction];
|
||||
self.subtitleLabel.text = [self subtitleForInteraction:self.interaction];
|
||||
self.titleLabel.text = [self titleForInteraction:interaction];
|
||||
self.subtitleLabel.text = [self subtitleForInteraction:interaction];
|
||||
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
|
@ -200,10 +202,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
- (CGSize)bubbleSizeForInteraction:(TSUnreadIndicatorInteraction *)interaction
|
||||
collectionViewWidth:(CGFloat)collectionViewWidth
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth
|
||||
{
|
||||
CGSize result = CGSizeMake(collectionViewWidth, 0);
|
||||
OWSAssert(self.viewItem);
|
||||
OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]);
|
||||
|
||||
TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction;
|
||||
|
||||
// TODO: Should we use viewWidth?
|
||||
CGSize result = CGSizeMake(viewWidth, 0);
|
||||
result.height += self.titleVMargin * 2.f;
|
||||
result.height += self.topVMargin;
|
||||
result.height += self.bottomVMargin;
|
||||
|
@ -219,8 +226,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
self.subtitleLabel.text = subtitle;
|
||||
result.height += ceil(
|
||||
[self.subtitleLabel sizeThatFits:CGSizeMake(collectionViewWidth - self.subtitleHMargin * 2.f, CGFLOAT_MAX)]
|
||||
.height);
|
||||
[self.subtitleLabel sizeThatFits:CGSizeMake(viewWidth - self.subtitleHMargin * 2.f, CGFLOAT_MAX)].height);
|
||||
}
|
||||
|
||||
return result;
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ConversationCollectionViewDelegate <NSObject>
|
||||
|
||||
- (void)collectionViewWillChangeLayout;
|
||||
- (void)collectionViewDidChangeLayout;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface ConversationCollectionView : UICollectionView
|
||||
|
||||
@property (weak, nonatomic) id<ConversationCollectionViewDelegate> layoutDelegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationCollectionView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ConversationCollectionView
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
BOOL isChanging = !CGSizeEqualToSize(frame.size, self.frame.size);
|
||||
if (isChanging) {
|
||||
[self.layoutDelegate collectionViewWillChangeLayout];
|
||||
}
|
||||
[super setFrame:frame];
|
||||
if (isChanging) {
|
||||
[self.layoutDelegate collectionViewDidChangeLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds
|
||||
{
|
||||
BOOL isChanging = !CGSizeEqualToSize(bounds.size, self.bounds.size);
|
||||
if (isChanging) {
|
||||
[self.layoutDelegate collectionViewWillChangeLayout];
|
||||
}
|
||||
[super setBounds:bounds];
|
||||
if (isChanging) {
|
||||
[self.layoutDelegate collectionViewDidChangeLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setContentOffset:(CGPoint)contentOffset
|
||||
{
|
||||
if (self.contentSize.height < 1 && CGPointEqualToPoint(CGPointZero, contentOffset)) {
|
||||
// [UIScrollView _adjustContentOffsetIfNecessary] resets the content
|
||||
// offset to zero under a number of undocumented conditions. We don't
|
||||
// want this behavior; we want fine-grained control over the default
|
||||
// scroll state of the message view.
|
||||
//
|
||||
// [UIScrollView _adjustContentOffsetIfNecessary] is called in
|
||||
// response to many different events; trying to prevent them all is
|
||||
// whack-a-mole.
|
||||
//
|
||||
// It's not safe to override [UIScrollView _adjustContentOffsetIfNecessary],
|
||||
// since its a private API.
|
||||
//
|
||||
// We can avoid the issue by simply ignoring attempt to reset the content
|
||||
// offset to zero before the collection view has determined its content size.
|
||||
return;
|
||||
}
|
||||
|
||||
[super setContentOffset:contentOffset];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ConversationHeaderView : UIView
|
||||
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) UILabel *subtitleLabel;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationHeaderView.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation ConversationHeaderView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self) {
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds
|
||||
{
|
||||
[super setBounds:bounds];
|
||||
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
[super setFrame:frame];
|
||||
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)setCenter:(CGPoint)center
|
||||
{
|
||||
[super setCenter:center];
|
||||
|
||||
[self layoutSubviews];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// We need to manually resize and position the title views;
|
||||
// iOS AutoLayout doesn't work inside navigation bar items.
|
||||
const int kTitleVSpacing = 0.f;
|
||||
const int kTitleHMargin = 0.f;
|
||||
CGFloat titleHeight = ceil([self.titleLabel sizeThatFits:CGSizeZero].height);
|
||||
CGFloat subtitleHeight = ceil([self.subtitleLabel sizeThatFits:CGSizeZero].height);
|
||||
CGFloat contentHeight = titleHeight + kTitleVSpacing + subtitleHeight;
|
||||
CGFloat contentWidth = round(self.width - 2 * kTitleHMargin);
|
||||
|
||||
CGFloat y = MAX(0, round((self.height - contentHeight) * 0.5f));
|
||||
self.titleLabel.frame = CGRectMake(kTitleHMargin, y, contentWidth, titleHeight);
|
||||
self.subtitleLabel.frame
|
||||
= CGRectMake(kTitleHMargin, ceil(y + titleHeight + kTitleVSpacing), contentWidth, subtitleHeight);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
//#import <JSQMessagesViewController/JSQMessagesViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SignalAttachment;
|
||||
|
||||
@protocol ConversationInputTextViewDelegate <NSObject>
|
||||
|
||||
- (void)didPasteAttachment:(SignalAttachment *_Nullable)attachment;
|
||||
|
||||
- (void)textViewDidChangeLayout;
|
||||
|
||||
- (void)inputTextViewDidBecomeFirstResponder;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface ConversationInputTextView : UITextView
|
||||
|
||||
@property (weak, nonatomic) id<ConversationInputTextViewDelegate> inputTextViewDelegate;
|
||||
|
||||
- (NSString *)trimmedText;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,250 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationInputTextView.h"
|
||||
#import "Signal-Swift.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation ConversationInputTextView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
|
||||
|
||||
CGFloat cornerRadius = 6.0f;
|
||||
|
||||
self.font = [UIFont ows_dynamicTypeBodyFont];
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.layer.borderColor = [UIColor lightGrayColor].CGColor;
|
||||
self.layer.borderWidth = 0.5f;
|
||||
self.layer.cornerRadius = cornerRadius;
|
||||
|
||||
self.scrollIndicatorInsets = UIEdgeInsetsMake(cornerRadius, 0.0f, cornerRadius, 0.0f);
|
||||
|
||||
self.textContainerInset = UIEdgeInsetsMake(4.0f, 2.0f, 4.0f, 2.0f);
|
||||
self.contentInset = UIEdgeInsetsMake(1.0f, 0.0f, 1.0f, 0.0f);
|
||||
|
||||
self.scrollEnabled = YES;
|
||||
self.scrollsToTop = NO;
|
||||
self.userInteractionEnabled = YES;
|
||||
|
||||
self.font = [UIFont systemFontOfSize:16.0f];
|
||||
self.textColor = [UIColor blackColor];
|
||||
self.textAlignment = NSTextAlignmentNatural;
|
||||
|
||||
self.contentMode = UIViewContentModeRedraw;
|
||||
self.dataDetectorTypes = UIDataDetectorTypeNone;
|
||||
self.keyboardAppearance = UIKeyboardAppearanceDefault;
|
||||
self.keyboardType = UIKeyboardTypeDefault;
|
||||
self.returnKeyType = UIReturnKeyDefault;
|
||||
|
||||
self.text = nil;
|
||||
|
||||
// _placeHolder = nil;
|
||||
// _placeHolderTextColor = [UIColor lightGrayColor];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
BOOL becameFirstResponder = [super becomeFirstResponder];
|
||||
if (becameFirstResponder) {
|
||||
// Intercept to scroll to bottom when text view is tapped.
|
||||
[self.inputTextViewDelegate inputTextViewDidBecomeFirstResponder];
|
||||
}
|
||||
return becameFirstResponder;
|
||||
}
|
||||
|
||||
- (BOOL)pasteboardHasPossibleAttachment
|
||||
{
|
||||
// We don't want to load/convert images more than once so we
|
||||
// only do a cursory validation pass at this time.
|
||||
return ([SignalAttachment pasteboardHasPossibleAttachment] && ![SignalAttachment pasteboardHasText]);
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
||||
{
|
||||
if (action == @selector(paste:)) {
|
||||
if ([self pasteboardHasPossibleAttachment]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return [super canPerformAction:action withSender:sender];
|
||||
}
|
||||
|
||||
- (void)paste:(nullable id)sender
|
||||
{
|
||||
if ([self pasteboardHasPossibleAttachment]) {
|
||||
SignalAttachment *attachment = [SignalAttachment attachmentFromPasteboard];
|
||||
// Note: attachment might be nil or have an error at this point; that's fine.
|
||||
[self.inputTextViewDelegate didPasteAttachment:attachment];
|
||||
return;
|
||||
}
|
||||
|
||||
[super paste:sender];
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
BOOL isNonEmpty = (self.width > 0.f && self.height > 0.f);
|
||||
BOOL didChangeSize = !CGSizeEqualToSize(frame.size, self.frame.size);
|
||||
|
||||
[super setFrame:frame];
|
||||
|
||||
if (didChangeSize && isNonEmpty) {
|
||||
[self.inputTextViewDelegate textViewDidChangeLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds
|
||||
{
|
||||
BOOL isNonEmpty = (self.width > 0.f && self.height > 0.f);
|
||||
BOOL didChangeSize = !CGSizeEqualToSize(bounds.size, self.bounds.size);
|
||||
|
||||
[super setBounds:bounds];
|
||||
|
||||
if (didChangeSize && isNonEmpty) {
|
||||
[self.inputTextViewDelegate textViewDidChangeLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)trimmedText
|
||||
{
|
||||
return [self.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
}
|
||||
|
||||
// TODO:
|
||||
//#import <QuartzCore/QuartzCore.h>
|
||||
//
|
||||
//#import "NSString+JSQMessages.h"
|
||||
//
|
||||
//
|
||||
//@implementation JSQMessagesComposerTextView
|
||||
//
|
||||
//#pragma mark - Initialization
|
||||
//
|
||||
//- (void)jsq_configureTextView
|
||||
//{
|
||||
//
|
||||
// [self jsq_addTextViewNotificationObservers];
|
||||
//}
|
||||
//
|
||||
//
|
||||
//- (void)dealloc
|
||||
//{
|
||||
// [self jsq_removeTextViewNotificationObservers];
|
||||
//}
|
||||
//
|
||||
//#pragma mark - Composer text view
|
||||
//
|
||||
//- (BOOL)hasText
|
||||
//{
|
||||
// return ([[self.text jsq_stringByTrimingWhitespace] length] > 0);
|
||||
//}
|
||||
//
|
||||
//- (void)paste:(id)sender
|
||||
//{
|
||||
// if (!self.jsqPasteDelegate || [self.jsqPasteDelegate composerTextView:self shouldPasteWithSender:sender]) {
|
||||
// [super paste:sender];
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#pragma mark - Drawing
|
||||
//
|
||||
//- (void)drawRect:(CGRect)rect
|
||||
//{
|
||||
// [super drawRect:rect];
|
||||
//
|
||||
// if ([self.text length] == 0 && self.placeHolder) {
|
||||
// [self.placeHolderTextColor set];
|
||||
//
|
||||
// [self.placeHolder drawInRect:CGRectInset(rect, 7.0f, 5.0f)
|
||||
// withAttributes:[self jsq_placeholderTextAttributes]];
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#pragma mark - Notifications
|
||||
//
|
||||
//- (void)jsq_addTextViewNotificationObservers
|
||||
//{
|
||||
// [[NSNotificationCenter defaultCenter] addObserver:self
|
||||
// selector:@selector(jsq_didReceiveTextViewNotification:)
|
||||
// name:UITextViewTextDidChangeNotification
|
||||
// object:self];
|
||||
//
|
||||
// [[NSNotificationCenter defaultCenter] addObserver:self
|
||||
// selector:@selector(jsq_didReceiveTextViewNotification:)
|
||||
// name:UITextViewTextDidBeginEditingNotification
|
||||
// object:self];
|
||||
//
|
||||
// [[NSNotificationCenter defaultCenter] addObserver:self
|
||||
// selector:@selector(jsq_didReceiveTextViewNotification:)
|
||||
// name:UITextViewTextDidEndEditingNotification
|
||||
// object:self];
|
||||
//}
|
||||
//
|
||||
//- (void)jsq_removeTextViewNotificationObservers
|
||||
//{
|
||||
// [[NSNotificationCenter defaultCenter] removeObserver:self
|
||||
// name:UITextViewTextDidChangeNotification
|
||||
// object:self];
|
||||
//
|
||||
// [[NSNotificationCenter defaultCenter] removeObserver:self
|
||||
// name:UITextViewTextDidBeginEditingNotification
|
||||
// object:self];
|
||||
//
|
||||
// [[NSNotificationCenter defaultCenter] removeObserver:self
|
||||
// name:UITextViewTextDidEndEditingNotification
|
||||
// object:self];
|
||||
//}
|
||||
//
|
||||
//- (void)jsq_didReceiveTextViewNotification:(NSNotification *)notification
|
||||
//{
|
||||
// [self setNeedsDisplay];
|
||||
//}
|
||||
//
|
||||
//#pragma mark - Utilities
|
||||
//
|
||||
//- (NSDictionary *)jsq_placeholderTextAttributes
|
||||
//{
|
||||
// NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
|
||||
// paragraphStyle.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
// paragraphStyle.alignment = self.textAlignment;
|
||||
//
|
||||
// return @{ NSFontAttributeName : self.font,
|
||||
// NSForegroundColorAttributeName : self.placeHolderTextColor,
|
||||
// NSParagraphStyleAttributeName : paragraphStyle };
|
||||
//}
|
||||
//
|
||||
//#pragma mark - UIMenuController
|
||||
//
|
||||
//- (BOOL)canBecomeFirstResponder
|
||||
//{
|
||||
// return [super canBecomeFirstResponder];
|
||||
//}
|
||||
//
|
||||
//- (BOOL)becomeFirstResponder
|
||||
//{
|
||||
// return [super becomeFirstResponder];
|
||||
//}
|
||||
//
|
||||
//- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
|
||||
// [UIMenuController sharedMenuController].menuItems = nil;
|
||||
// return [super canPerformAction:action withSender:sender];
|
||||
//}
|
||||
//@end
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol ConversationInputToolbarDelegate <NSObject>
|
||||
|
||||
- (void)sendButtonPressed;
|
||||
|
||||
- (void)attachmentButtonPressed;
|
||||
|
||||
- (void)voiceMemoGestureDidStart;
|
||||
|
||||
- (void)voiceMemoGestureDidEnd;
|
||||
|
||||
- (void)voiceMemoGestureDidCancel;
|
||||
|
||||
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha;
|
||||
|
||||
- (void)textViewDidChange;
|
||||
|
||||
// TODO: Is this necessary.
|
||||
//- (void)textViewDidBeginEditing;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@class ConversationInputTextView;
|
||||
|
||||
@protocol ConversationInputTextViewDelegate;
|
||||
|
||||
@interface ConversationInputToolbar : UIToolbar
|
||||
|
||||
@property (nonatomic, weak) id<ConversationInputToolbarDelegate> inputToolbarDelegate;
|
||||
|
||||
- (void)beginEditingTextMessage;
|
||||
- (void)endEditingTextMessage;
|
||||
|
||||
- (void)setInputTextViewDelegate:(id<ConversationInputTextViewDelegate>)value;
|
||||
|
||||
- (NSString *)messageText;
|
||||
- (void)setMessageText:(NSString *_Nullable)value;
|
||||
- (void)clearTextMessage;
|
||||
|
||||
#pragma mark - Voice Memo
|
||||
|
||||
- (void)showVoiceMemoUI;
|
||||
|
||||
- (void)hideVoiceMemoUI:(BOOL)animated;
|
||||
|
||||
- (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha;
|
||||
|
||||
- (void)cancelVoiceMemoIfNecessary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
File diff suppressed because it is too large
Load Diff
|
@ -2,13 +2,16 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesViewController.h>
|
||||
#import "OWSViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSThread;
|
||||
|
||||
// TODO: Audit this.
|
||||
extern NSString *const ConversationViewControllerDidAppearNotification;
|
||||
|
||||
@interface ConversationViewController : JSQMessagesViewController
|
||||
@interface ConversationViewController : OWSViewController
|
||||
|
||||
@property (nonatomic, readonly) TSThread *thread;
|
||||
|
||||
|
@ -24,3 +27,5 @@ extern NSString *const ConversationViewControllerDidAppearNotification;
|
|||
- (void)popped;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,58 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MessagesViewController">
|
||||
<connections>
|
||||
<outlet property="collectionView" destination="l9u-2b-4LK" id="bLP-6g-CkO"/>
|
||||
<outlet property="inputToolbar" destination="BoD-Az-3DM" id="w74-g9-1qA"/>
|
||||
<outlet property="toolbarBottomLayoutGuide" destination="rHs-6q-NX4" id="d6h-iu-VMX"/>
|
||||
<outlet property="toolbarHeightConstraint" destination="HIk-02-qcW" id="jE8-xC-1eD"/>
|
||||
<outlet property="view" destination="mUa-cS-ru4" id="nki-T1-RTI"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="mUa-cS-ru4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<collectionView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.0" maximumZoomScale="0.0" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="l9u-2b-4LK" customClass="JSQMessagesCollectionView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<collectionViewLayout key="collectionViewLayout" id="dZl-7C-LHR" customClass="JSQMessagesCollectionViewFlowLayout"/>
|
||||
<cells/>
|
||||
</collectionView>
|
||||
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="BoD-Az-3DM" customClass="OWSMessagesInputToolbar">
|
||||
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="HIk-02-qcW"/>
|
||||
</constraints>
|
||||
<items/>
|
||||
</toolbar>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="BoD-Az-3DM" secondAttribute="trailing" id="7xc-Ha-asg"/>
|
||||
<constraint firstItem="l9u-2b-4LK" firstAttribute="leading" secondItem="mUa-cS-ru4" secondAttribute="leading" id="MmF-oh-Y75"/>
|
||||
<constraint firstAttribute="trailing" secondItem="l9u-2b-4LK" secondAttribute="trailing" id="O9u-TA-A0e"/>
|
||||
<constraint firstAttribute="bottom" secondItem="l9u-2b-4LK" secondAttribute="bottom" id="Re7-WW-UmS"/>
|
||||
<constraint firstItem="l9u-2b-4LK" firstAttribute="top" secondItem="mUa-cS-ru4" secondAttribute="top" id="dCQ-DM-Wdj"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BoD-Az-3DM" secondAttribute="bottom" id="rHs-6q-NX4"/>
|
||||
<constraint firstItem="BoD-Az-3DM" firstAttribute="leading" secondItem="mUa-cS-ru4" secondAttribute="leading" id="ts7-8f-0lH"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
</view>
|
||||
</objects>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewLayout.h"
|
||||
#import "OWSAudioAttachmentPlayer.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, OWSMessageCellType) {
|
||||
OWSMessageCellType_TextMessage,
|
||||
OWSMessageCellType_OversizeTextMessage,
|
||||
OWSMessageCellType_StillImage,
|
||||
OWSMessageCellType_AnimatedImage,
|
||||
OWSMessageCellType_Audio,
|
||||
OWSMessageCellType_Video,
|
||||
OWSMessageCellType_GenericAttachment,
|
||||
OWSMessageCellType_DownloadingAttachment,
|
||||
// Treat invalid messages as empty text messages.
|
||||
OWSMessageCellType_Unknown = OWSMessageCellType_TextMessage,
|
||||
};
|
||||
|
||||
@class ConversationViewCell;
|
||||
@class OWSAudioMessageView;
|
||||
@class TSAttachmentPointer;
|
||||
@class TSAttachmentStream;
|
||||
@class TSInteraction;
|
||||
|
||||
@interface ConversationViewItem : NSObject <ConversationViewLayoutItem, OWSAudioAttachmentPlayerDelegate>
|
||||
|
||||
@property (nonatomic, readonly) TSInteraction *interaction;
|
||||
|
||||
@property (nonatomic) BOOL shouldShowDate;
|
||||
|
||||
//@property (nonatomic, weak) ConversationViewCell *lastCell;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithTSInteraction:(TSInteraction *)interaction;
|
||||
|
||||
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
|
||||
indexPath:(NSIndexPath *)indexPath;
|
||||
|
||||
- (void)replaceInteraction:(TSInteraction *)interaction;
|
||||
|
||||
- (void)clearCachedLayoutState;
|
||||
|
||||
#pragma mark - Audio Playback
|
||||
|
||||
@property (nonatomic, weak) OWSAudioMessageView *lastAudioMessageView;
|
||||
|
||||
@property (nonatomic, nullable) NSNumber *audioDurationSeconds;
|
||||
|
||||
- (CGFloat)audioProgressSeconds;
|
||||
|
||||
//#pragma mark - Menu Actions
|
||||
//
|
||||
//- (BOOL)canPerformEditingAction:(SEL)selector;
|
||||
//- (void)performEditingAction:(SEL)selector;
|
||||
|
||||
#pragma mark - Expiration
|
||||
|
||||
// TODO:
|
||||
//@property (nonatomic, readonly) BOOL isExpiringMessage;
|
||||
//@property (nonatomic, readonly) BOOL shouldStartExpireTimer;
|
||||
//@property (nonatomic, readonly) double expiresAtSeconds;
|
||||
//@property (nonatomic, readonly) uint32_t expiresInSeconds;
|
||||
|
||||
#pragma mark - View State Caching
|
||||
|
||||
// These methods only apply to text & attachment messages.
|
||||
- (OWSMessageCellType)messageCellType;
|
||||
- (nullable NSString *)textMessage;
|
||||
- (nullable TSAttachmentStream *)attachmentStream;
|
||||
- (nullable TSAttachmentPointer *)attachmentPointer;
|
||||
- (CGSize)contentSize;
|
||||
|
||||
// TODO:
|
||||
//// Cells will request that this adapter clear its cached media views,
|
||||
//// but the adapter should only honor requests from the last cell to
|
||||
//// use its views.
|
||||
//- (void)setLastPresentingCell:(nullable id)cell;
|
||||
//- (void)clearCachedMediaViewsIfLastPresentingCell:(id)cell;
|
||||
|
||||
#pragma mark - UIMenuController
|
||||
|
||||
- (NSArray<UIMenuItem *> *)menuControllerItems;
|
||||
- (BOOL)canPerformAction:(SEL)action;
|
||||
- (void)copyAction;
|
||||
- (void)shareAction;
|
||||
- (void)saveAction;
|
||||
- (void)deleteAction;
|
||||
- (SEL)metadataActionSelector;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,614 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewItem.h"
|
||||
#import "OWSAudioMessageView.h"
|
||||
#import "OWSContactOffersCell.h"
|
||||
#import "OWSIncomingMessageCell.h"
|
||||
#import "OWSOutgoingMessageCell.h"
|
||||
#import "OWSSystemMessageCell.h"
|
||||
#import "OWSUnreadIndicatorCell.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import <AssetsLibrary/AssetsLibrary.h>
|
||||
#import <SignalServiceKit/TSInteraction.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ConversationViewItem ()
|
||||
|
||||
@property (nonatomic, nullable) NSValue *cachedCellSize;
|
||||
|
||||
#pragma mark - OWSAudioAttachmentPlayerDelegate
|
||||
|
||||
@property (nonatomic) AudioPlaybackState audioPlaybackState;
|
||||
@property (nonatomic) CGFloat audioProgressSeconds;
|
||||
|
||||
#pragma mark - View State
|
||||
|
||||
@property (nonatomic) BOOL hasViewState;
|
||||
@property (nonatomic) OWSMessageCellType messageCellType;
|
||||
@property (nonatomic, nullable) NSString *textMessage;
|
||||
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
|
||||
@property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer;
|
||||
@property (nonatomic) CGSize contentSize;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ConversationViewItem
|
||||
|
||||
- (instancetype)initWithTSInteraction:(TSInteraction *)interaction
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_interaction = interaction;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)replaceInteraction:(TSInteraction *)interaction
|
||||
{
|
||||
OWSAssert(interaction);
|
||||
|
||||
_interaction = interaction;
|
||||
|
||||
[self clearCachedLayoutState];
|
||||
}
|
||||
|
||||
- (void)setShouldShowDate:(BOOL)shouldShowDate
|
||||
{
|
||||
if (_shouldShowDate == shouldShowDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
_shouldShowDate = shouldShowDate;
|
||||
|
||||
[self clearCachedLayoutState];
|
||||
}
|
||||
|
||||
- (void)clearCachedLayoutState
|
||||
{
|
||||
self.cachedCellSize = nil;
|
||||
}
|
||||
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
CGSize cellSize = CGSizeZero;
|
||||
if (!self.cachedCellSize) {
|
||||
ConversationViewCell *_Nullable measurementCell = [self measurementCell];
|
||||
measurementCell.viewItem = self;
|
||||
cellSize = [measurementCell cellSizeForViewWidth:viewWidth maxMessageWidth:maxMessageWidth];
|
||||
self.cachedCellSize = [NSValue valueWithCGSize:cellSize];
|
||||
[measurementCell prepareForReuse];
|
||||
|
||||
// DDLogError(@"cellSizeForViewWidth: %@ %@", self.interaction.uniqueId, self.interaction.description);
|
||||
// DDLogError(@"\t fresh cellSize: %@", NSStringFromCGSize(cellSize));
|
||||
} else {
|
||||
cellSize = [self.cachedCellSize CGSizeValue];
|
||||
// DDLogError(@"cellSizeForViewWidth: %@ %@", self.interaction.uniqueId, self.interaction.description);
|
||||
// DDLogError(@"\t cached cellSize: %@", NSStringFromCGSize(cellSize));
|
||||
}
|
||||
return cellSize;
|
||||
}
|
||||
|
||||
- (ConversationViewLayoutAlignment)layoutAlignment
|
||||
{
|
||||
switch (self.interaction.interactionType) {
|
||||
case OWSInteractionType_Unknown:
|
||||
return ConversationViewLayoutAlignment_Center;
|
||||
case OWSInteractionType_IncomingMessage:
|
||||
return ConversationViewLayoutAlignment_Incoming;
|
||||
break;
|
||||
case OWSInteractionType_OutgoingMessage:
|
||||
return ConversationViewLayoutAlignment_Outgoing;
|
||||
break;
|
||||
case OWSInteractionType_Error:
|
||||
case OWSInteractionType_Info:
|
||||
case OWSInteractionType_Call:
|
||||
return ConversationViewLayoutAlignment_Center;
|
||||
case OWSInteractionType_UnreadIndicator:
|
||||
return ConversationViewLayoutAlignment_FullWidth;
|
||||
case OWSInteractionType_Offer:
|
||||
return ConversationViewLayoutAlignment_Center;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable ConversationViewCell *)measurementCell
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
OWSAssert(self.interaction);
|
||||
|
||||
// For performance reasons, we cache one instance of each kind of
|
||||
// cell and uses these cells for measurement.
|
||||
static NSMutableDictionary<NSNumber *, ConversationViewCell *> *measurementCellCache = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
measurementCellCache = [NSMutableDictionary new];
|
||||
});
|
||||
|
||||
NSNumber *cellCacheKey = @(self.interaction.interactionType);
|
||||
ConversationViewCell *_Nullable measurementCell = measurementCellCache[cellCacheKey];
|
||||
if (!measurementCell) {
|
||||
switch (self.interaction.interactionType) {
|
||||
case OWSInteractionType_Unknown:
|
||||
OWSFail(@"%@ Unknown interaction type.", self.tag);
|
||||
return nil;
|
||||
case OWSInteractionType_IncomingMessage:
|
||||
measurementCell = [OWSIncomingMessageCell new];
|
||||
break;
|
||||
case OWSInteractionType_OutgoingMessage:
|
||||
measurementCell = [OWSOutgoingMessageCell new];
|
||||
break;
|
||||
case OWSInteractionType_Error:
|
||||
case OWSInteractionType_Info:
|
||||
case OWSInteractionType_Call:
|
||||
measurementCell = [OWSSystemMessageCell new];
|
||||
break;
|
||||
case OWSInteractionType_UnreadIndicator:
|
||||
measurementCell = [OWSUnreadIndicatorCell new];
|
||||
break;
|
||||
case OWSInteractionType_Offer:
|
||||
measurementCell = [OWSContactOffersCell new];
|
||||
break;
|
||||
}
|
||||
|
||||
OWSAssert(measurementCell);
|
||||
measurementCellCache[cellCacheKey] = measurementCell;
|
||||
}
|
||||
|
||||
return measurementCell;
|
||||
}
|
||||
|
||||
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
|
||||
indexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
OWSAssert(collectionView);
|
||||
OWSAssert(indexPath);
|
||||
OWSAssert(self.interaction);
|
||||
|
||||
switch (self.interaction.interactionType) {
|
||||
case OWSInteractionType_Unknown:
|
||||
OWSFail(@"%@ Unknown interaction type.", self.tag);
|
||||
return nil;
|
||||
case OWSInteractionType_IncomingMessage:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSIncomingMessageCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_OutgoingMessage:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSOutgoingMessageCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_Error:
|
||||
case OWSInteractionType_Info:
|
||||
case OWSInteractionType_Call:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_UnreadIndicator:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_Offer:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - OWSAudioAttachmentPlayerDelegate
|
||||
|
||||
- (void)setAudioPlaybackState:(AudioPlaybackState)audioPlaybackState
|
||||
{
|
||||
_audioPlaybackState = audioPlaybackState;
|
||||
|
||||
[self.lastAudioMessageView updateContents];
|
||||
}
|
||||
|
||||
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
self.audioProgressSeconds = progress;
|
||||
if (duration > 0) {
|
||||
self.audioDurationSeconds = @(duration);
|
||||
}
|
||||
|
||||
[self.lastAudioMessageView updateContents];
|
||||
}
|
||||
|
||||
#pragma mark - View State
|
||||
|
||||
- (NSCache *)displayableTextCache
|
||||
{
|
||||
static NSCache *cache = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
cache = [NSCache new];
|
||||
// Cache the results for up to 1,000 messages.
|
||||
cache.countLimit = 1000;
|
||||
});
|
||||
return cache;
|
||||
}
|
||||
|
||||
- (NSString *)displayableTextForText:(NSString *)text interactionId:(NSString *)interactionId
|
||||
{
|
||||
OWSAssert(text);
|
||||
OWSAssert(interactionId.length > 0);
|
||||
|
||||
NSString *_Nullable displayableText = [[self displayableTextCache] objectForKey:interactionId];
|
||||
if (!displayableText) {
|
||||
// Only show up to 2kb of text.
|
||||
const NSUInteger kMaxTextDisplayLength = 2 * 1024;
|
||||
displayableText = [[DisplayableTextFilter new] displayableText:text];
|
||||
if (displayableText.length > kMaxTextDisplayLength) {
|
||||
// Trim whitespace before _AND_ after slicing the snipper from the string.
|
||||
NSString *snippet =
|
||||
[[[displayableText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]
|
||||
substringWithRange:NSMakeRange(0, kMaxTextDisplayLength)]
|
||||
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
displayableText = [NSString stringWithFormat:NSLocalizedString(@"OVERSIZE_TEXT_DISPLAY_FORMAT",
|
||||
@"A display format for oversize text messages."),
|
||||
snippet];
|
||||
}
|
||||
if (!displayableText) {
|
||||
displayableText = @"";
|
||||
}
|
||||
[[self displayableTextCache] setObject:displayableText forKey:interactionId];
|
||||
}
|
||||
return displayableText;
|
||||
}
|
||||
|
||||
- (NSString *)displayableTextForAttachmentStream:(TSAttachmentStream *)attachmentStream
|
||||
interactionId:(NSString *)interactionId
|
||||
{
|
||||
OWSAssert(attachmentStream);
|
||||
OWSAssert(interactionId.length > 0);
|
||||
|
||||
NSString *_Nullable displayableText = [[self displayableTextCache] objectForKey:interactionId];
|
||||
if (displayableText) {
|
||||
return displayableText;
|
||||
}
|
||||
|
||||
NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL];
|
||||
NSString *text = [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding];
|
||||
return [self displayableTextForText:text interactionId:interactionId];
|
||||
}
|
||||
|
||||
- (void)ensureViewState
|
||||
{
|
||||
OWSAssert([self.interaction isKindOfClass:[TSMessage class]]);
|
||||
|
||||
if (self.hasViewState) {
|
||||
return;
|
||||
}
|
||||
self.hasViewState = YES;
|
||||
|
||||
TSMessage *interaction = (TSMessage *)self.interaction;
|
||||
if (interaction.body.length > 0) {
|
||||
self.messageCellType = OWSMessageCellType_TextMessage;
|
||||
// TODO: This can be expensive. Should we cache it on the view item?
|
||||
self.textMessage = [self displayableTextForText:interaction.body interactionId:interaction.uniqueId];
|
||||
return;
|
||||
} else {
|
||||
NSString *_Nullable attachmentId = interaction.attachmentIds.firstObject;
|
||||
if (attachmentId.length > 0) {
|
||||
TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
|
||||
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
self.attachmentStream = (TSAttachmentStream *)attachment;
|
||||
|
||||
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
|
||||
self.messageCellType = OWSMessageCellType_OversizeTextMessage;
|
||||
// TODO: This can be expensive. Should we cache it on the view item?
|
||||
self.textMessage = [self displayableTextForAttachmentStream:self.attachmentStream
|
||||
interactionId:interaction.uniqueId];
|
||||
return;
|
||||
} else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] ||
|
||||
[self.attachmentStream isVideo]) {
|
||||
if ([self.attachmentStream isAnimated]) {
|
||||
self.messageCellType = OWSMessageCellType_AnimatedImage;
|
||||
} else if ([self.attachmentStream isImage]) {
|
||||
self.messageCellType = OWSMessageCellType_StillImage;
|
||||
} else if ([self.attachmentStream isVideo]) {
|
||||
self.messageCellType = OWSMessageCellType_Video;
|
||||
} else {
|
||||
OWSFail(@"%@ unexpected attachment type.", self.tag);
|
||||
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
||||
return;
|
||||
}
|
||||
self.contentSize = [self.attachmentStream imageSizeWithoutTransaction];
|
||||
if (self.contentSize.width <= 0 || self.contentSize.height <= 0) {
|
||||
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
||||
}
|
||||
return;
|
||||
} else if ([self.attachmentStream isAudio]) {
|
||||
self.messageCellType = OWSMessageCellType_Audio;
|
||||
return;
|
||||
} else {
|
||||
self.messageCellType = OWSMessageCellType_GenericAttachment;
|
||||
return;
|
||||
}
|
||||
} else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
|
||||
self.messageCellType = OWSMessageCellType_DownloadingAttachment;
|
||||
self.attachmentPointer = (TSAttachmentPointer *)attachment;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OWSFail(@"%@ Unknown cell type", self.tag);
|
||||
|
||||
self.messageCellType = OWSMessageCellType_Unknown;
|
||||
}
|
||||
|
||||
- (OWSMessageCellType)messageCellType
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[self ensureViewState];
|
||||
|
||||
return _messageCellType;
|
||||
}
|
||||
|
||||
- (nullable NSString *)textMessage
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[self ensureViewState];
|
||||
|
||||
return _textMessage;
|
||||
}
|
||||
|
||||
- (nullable TSAttachmentStream *)attachmentStream
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[self ensureViewState];
|
||||
|
||||
return _attachmentStream;
|
||||
}
|
||||
|
||||
- (nullable TSAttachmentPointer *)attachmentPointer
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[self ensureViewState];
|
||||
|
||||
return _attachmentPointer;
|
||||
}
|
||||
|
||||
- (CGSize)contentSize
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
[self ensureViewState];
|
||||
|
||||
return _contentSize;
|
||||
}
|
||||
|
||||
#pragma mark - UIMenuController
|
||||
|
||||
- (NSArray<UIMenuItem *> *)menuControllerItems
|
||||
{
|
||||
return @[
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SHARE_ACTION",
|
||||
@"Short name for edit menu item to share contents of media message.")
|
||||
action:self.shareActionSelector],
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_MESSAGE_METADATA_ACTION",
|
||||
@"Short name for edit menu item to show message metadata.")
|
||||
action:self.metadataActionSelector],
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_COPY_ACTION",
|
||||
@"Short name for edit menu item to copy contents of media message.")
|
||||
action:self.copyActionSelector],
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_DELETE_ACTION",
|
||||
@"Short name for edit menu item to delete contents of media message.")
|
||||
action:self.deleteActionSelector],
|
||||
// TODO: Do we want a save action?
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SAVE_ACTION",
|
||||
@"Short name for edit menu item to save contents of media message.")
|
||||
action:self.saveActionSelector],
|
||||
];
|
||||
}
|
||||
|
||||
- (SEL)copyActionSelector
|
||||
{
|
||||
return NSSelectorFromString(@"copyAction:");
|
||||
}
|
||||
|
||||
- (SEL)saveActionSelector
|
||||
{
|
||||
return NSSelectorFromString(@"saveAction:");
|
||||
}
|
||||
|
||||
- (SEL)shareActionSelector
|
||||
{
|
||||
return NSSelectorFromString(@"shareAction:");
|
||||
}
|
||||
|
||||
- (SEL)deleteActionSelector
|
||||
{
|
||||
return NSSelectorFromString(@"deleteAction:");
|
||||
}
|
||||
|
||||
- (SEL)metadataActionSelector
|
||||
{
|
||||
return NSSelectorFromString(@"metadataAction:");
|
||||
}
|
||||
|
||||
// We only use custom actions in UIMenuController.
|
||||
- (BOOL)canPerformAction:(SEL)action
|
||||
{
|
||||
if (action == self.copyActionSelector) {
|
||||
return [self hasActionContent];
|
||||
} else if (action == self.saveActionSelector) {
|
||||
return [self canSave];
|
||||
} else if (action == self.shareActionSelector) {
|
||||
return [self hasActionContent];
|
||||
} else if (action == self.deleteActionSelector) {
|
||||
return YES;
|
||||
} else if (action == self.metadataActionSelector) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)copyAction
|
||||
{
|
||||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
[UIPasteboard.generalPasteboard setString:self.textMessage];
|
||||
break;
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_Video:
|
||||
case OWSMessageCellType_GenericAttachment: {
|
||||
NSString *utiType = [MIMETypeUtil utiTypeForMIMEType:self.attachmentStream.contentType];
|
||||
if (!utiType) {
|
||||
OWSFail(@"%@ Unknown MIME type: %@", self.tag, self.attachmentStream.contentType);
|
||||
utiType = (NSString *)kUTTypeGIF;
|
||||
}
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load attachment data: %@", self.tag, [self.attachmentStream mediaURL]);
|
||||
return;
|
||||
}
|
||||
[UIPasteboard.generalPasteboard setData:data forPasteboardType:utiType];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
OWSFail(@"%@ Can't copy not-yet-downloaded attachment", self.tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)shareAction
|
||||
{
|
||||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
[AttachmentSharing showShareUIForText:self.textMessage];
|
||||
break;
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_Video:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
break;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
OWSFail(@"%@ Can't share not-yet-downloaded attachment", self.tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)canSave
|
||||
{
|
||||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
return NO;
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
return YES;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_Video:
|
||||
return UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path);
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return NO;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)saveAction
|
||||
{
|
||||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
OWSFail(@"%@ Cannot save text data.", [self tag]);
|
||||
break;
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage: {
|
||||
NSData *data = [NSData dataWithContentsOfURL:[self.attachmentStream mediaURL]];
|
||||
if (!data) {
|
||||
OWSFail(@"%@ Could not load image data: %@", [self tag], [self.attachmentStream mediaURL]);
|
||||
return;
|
||||
}
|
||||
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
|
||||
[library writeImageDataToSavedPhotosAlbum:data
|
||||
metadata:nil
|
||||
completionBlock:^(NSURL *assetURL, NSError *error) {
|
||||
if (error) {
|
||||
DDLogWarn(@"Error Saving image to photo album: %@", error);
|
||||
}
|
||||
}];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_Audio:
|
||||
OWSFail(@"%@ Cannot save media data.", [self tag]);
|
||||
break;
|
||||
case OWSMessageCellType_Video:
|
||||
if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(self.attachmentStream.mediaURL.path)) {
|
||||
UISaveVideoAtPathToSavedPhotosAlbum(self.attachmentStream.mediaURL.path, self, nil, nil);
|
||||
} else {
|
||||
OWSFail(@"%@ Could not save incompatible video data.", [self tag]);
|
||||
}
|
||||
break;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
OWSFail(@"%@ Cannot save media data.", [self tag]);
|
||||
break;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
OWSFail(@"%@ Can't save not-yet-downloaded attachment", self.tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)deleteAction
|
||||
{
|
||||
[self.interaction remove];
|
||||
}
|
||||
|
||||
- (BOOL)hasActionContent
|
||||
{
|
||||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_TextMessage:
|
||||
case OWSMessageCellType_OversizeTextMessage:
|
||||
return self.textMessage.length > 0;
|
||||
case OWSMessageCellType_StillImage:
|
||||
case OWSMessageCellType_AnimatedImage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_Video:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return self.attachmentStream != nil;
|
||||
case OWSMessageCellType_DownloadingAttachment: {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSInteger, ConversationViewLayoutAlignment) {
|
||||
ConversationViewLayoutAlignment_Incoming,
|
||||
ConversationViewLayoutAlignment_Outgoing,
|
||||
ConversationViewLayoutAlignment_FullWidth,
|
||||
ConversationViewLayoutAlignment_Center,
|
||||
};
|
||||
|
||||
@protocol ConversationViewLayoutItem <NSObject>
|
||||
|
||||
// TODO: Perhaps maxMessageWidth should be an implementation detail of the
|
||||
// message cells.
|
||||
- (CGSize)cellSizeForViewWidth:(int)viewWidth maxMessageWidth:(int)maxMessageWidth;
|
||||
|
||||
- (ConversationViewLayoutAlignment)layoutAlignment;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@protocol ConversationViewLayoutDelegate <NSObject>
|
||||
|
||||
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface ConversationViewLayout : UICollectionViewLayout
|
||||
|
||||
@property (nonatomic, weak) id<ConversationViewLayoutDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewLayout.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ConversationViewLayout ()
|
||||
|
||||
@property (nonatomic) CGSize contentSize;
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSNumber *, UICollectionViewLayoutAttributes *> *itemAttributesMap;
|
||||
|
||||
// This may be redundant with logic in UICollectionViewLayout, but
|
||||
// it can't hurt and it ensures that we can safely & cheaply call
|
||||
// prepareLayout from view logic to ensure that we always have a
|
||||
// valid layout without incurring any of the (great) expense of
|
||||
// performing an unnecessary layout pass.
|
||||
@property (nonatomic) BOOL hasLayout;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation ConversationViewLayout
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_itemAttributesMap = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)invalidateLayout
|
||||
{
|
||||
[super invalidateLayout];
|
||||
|
||||
[self clearState];
|
||||
}
|
||||
|
||||
- (void)invalidateLayoutWithContext:(UICollectionViewLayoutInvalidationContext *)context
|
||||
{
|
||||
[super invalidateLayoutWithContext:context];
|
||||
|
||||
[self clearState];
|
||||
}
|
||||
|
||||
- (void)clearState
|
||||
{
|
||||
self.contentSize = CGSizeZero;
|
||||
[self.itemAttributesMap removeAllObjects];
|
||||
self.hasLayout = NO;
|
||||
}
|
||||
|
||||
- (void)prepareLayout
|
||||
{
|
||||
[super prepareLayout];
|
||||
|
||||
id<ConversationViewLayoutDelegate> delegate = self.delegate;
|
||||
if (!delegate) {
|
||||
OWSFail(@"%@ Missing delegate", self.tag);
|
||||
[self clearState];
|
||||
return;
|
||||
}
|
||||
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
|
||||
OWSFail(@"%@ Collection view has invalid size: %@", self.tag, NSStringFromCGRect(self.collectionView.bounds));
|
||||
[self clearState];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.hasLayout) {
|
||||
return;
|
||||
}
|
||||
self.hasLayout = YES;
|
||||
|
||||
// TODO: Remove this log statement after we've reduced the invalidation churn.
|
||||
DDLogVerbose(@"%@ prepareLayout", self.tag);
|
||||
|
||||
const int vInset = 15;
|
||||
const int hInset = 10;
|
||||
const int vSpacing = 5;
|
||||
const int viewWidth = (int)floor(self.collectionView.bounds.size.width);
|
||||
const int maxMessageWidth = (int)floor((viewWidth - 2 * hInset) * 0.7f);
|
||||
|
||||
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;
|
||||
|
||||
CGFloat y = vInset;
|
||||
CGFloat contentBottom = y;
|
||||
BOOL isRTL = self.collectionView.isRTL;
|
||||
|
||||
NSInteger row = 0;
|
||||
for (id<ConversationViewLayoutItem> layoutItem in layoutItems) {
|
||||
CGSize layoutSize = [layoutItem cellSizeForViewWidth:viewWidth maxMessageWidth:maxMessageWidth];
|
||||
|
||||
layoutSize.width = MIN(viewWidth, floor(layoutSize.width));
|
||||
layoutSize.height = floor(layoutSize.height);
|
||||
CGRect itemFrame;
|
||||
switch (layoutItem.layoutAlignment) {
|
||||
case ConversationViewLayoutAlignment_Incoming:
|
||||
case ConversationViewLayoutAlignment_Outgoing: {
|
||||
BOOL isLeft = ((layoutItem.layoutAlignment == ConversationViewLayoutAlignment_Incoming && !isRTL)
|
||||
|| (layoutItem.layoutAlignment == ConversationViewLayoutAlignment_Outgoing && isRTL));
|
||||
if (isLeft) {
|
||||
itemFrame = CGRectMake(hInset, y, layoutSize.width, layoutSize.height);
|
||||
} else {
|
||||
itemFrame
|
||||
= CGRectMake(viewWidth - (hInset + layoutSize.width), y, layoutSize.width, layoutSize.height);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConversationViewLayoutAlignment_FullWidth:
|
||||
itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height);
|
||||
break;
|
||||
case ConversationViewLayoutAlignment_Center:
|
||||
itemFrame = CGRectMake(
|
||||
hInset + round((viewWidth - layoutSize.width) * 0.5f), y, layoutSize.width, layoutSize.height);
|
||||
break;
|
||||
}
|
||||
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
|
||||
UICollectionViewLayoutAttributes *itemAttributes =
|
||||
[UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
|
||||
itemAttributes.frame = itemFrame;
|
||||
self.itemAttributesMap[@(row)] = itemAttributes;
|
||||
|
||||
contentBottom = itemFrame.origin.y + itemFrame.size.height;
|
||||
y = contentBottom + vSpacing;
|
||||
row++;
|
||||
}
|
||||
|
||||
contentBottom += vInset;
|
||||
self.contentSize = CGSizeMake(viewWidth, contentBottom);
|
||||
}
|
||||
|
||||
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
|
||||
{
|
||||
NSMutableArray<UICollectionViewLayoutAttributes *> *result = [NSMutableArray new];
|
||||
for (UICollectionViewLayoutAttributes *itemAttributes in self.itemAttributesMap.allValues) {
|
||||
if (CGRectIntersectsRect(rect, itemAttributes.frame)) {
|
||||
[result addObject:itemAttributes];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.itemAttributesMap[@(indexPath.row)];
|
||||
}
|
||||
|
||||
- (CGSize)collectionViewContentSize
|
||||
{
|
||||
return self.contentSize;
|
||||
}
|
||||
|
||||
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
|
||||
{
|
||||
return self.collectionView.bounds.size.width != newBounds.size.width;
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SignalAttachment;
|
||||
|
||||
@protocol OWSTextViewPasteDelegate <NSObject>
|
||||
|
||||
- (void)didPasteAttachment:(SignalAttachment *_Nullable)attachment;
|
||||
|
||||
- (void)textViewDidChangeLayout;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessagesComposerTextView : JSQMessagesComposerTextView
|
||||
|
||||
@property (weak, nonatomic) id<OWSTextViewPasteDelegate> textViewPasteDelegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessagesComposerTextView.h"
|
||||
#import "Signal-Swift.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSMessagesComposerTextView
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
BOOL becameFirstResponder = [super becomeFirstResponder];
|
||||
if (becameFirstResponder) {
|
||||
// Intercept to scroll to bottom when text view is tapped.
|
||||
[self.textViewPasteDelegate textViewDidChangeLayout];
|
||||
}
|
||||
return becameFirstResponder;
|
||||
}
|
||||
|
||||
- (BOOL)pasteboardHasPossibleAttachment
|
||||
{
|
||||
// We don't want to load/convert images more than once so we
|
||||
// only do a cursory validation pass at this time.
|
||||
return ([SignalAttachment pasteboardHasPossibleAttachment] && ![SignalAttachment pasteboardHasText]);
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
||||
{
|
||||
if (action == @selector(paste:)) {
|
||||
if ([self pasteboardHasPossibleAttachment]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return [super canPerformAction:action withSender:sender];
|
||||
}
|
||||
|
||||
- (void)paste:(nullable id)sender
|
||||
{
|
||||
if ([self pasteboardHasPossibleAttachment]) {
|
||||
SignalAttachment *attachment = [SignalAttachment attachmentFromPasteboard];
|
||||
// Note: attachment might be nil or have an error at this point; that's fine.
|
||||
[self.textViewPasteDelegate didPasteAttachment:attachment];
|
||||
return;
|
||||
}
|
||||
|
||||
[super paste:sender];
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
BOOL isNonEmpty = (self.width > 0.f && self.height > 0.f);
|
||||
BOOL didChangeSize = !CGSizeEqualToSize(frame.size, self.frame.size);
|
||||
|
||||
[super setFrame:frame];
|
||||
|
||||
if (didChangeSize && isNonEmpty) {
|
||||
[self.textViewPasteDelegate textViewDidChangeLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds
|
||||
{
|
||||
BOOL isNonEmpty = (self.width > 0.f && self.height > 0.f);
|
||||
BOOL didChangeSize = !CGSizeEqualToSize(bounds.size, self.bounds.size);
|
||||
|
||||
[super setBounds:bounds];
|
||||
|
||||
if (didChangeSize && isNonEmpty) {
|
||||
[self.textViewPasteDelegate textViewDidChangeLayout];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessagesInputToolbar : JSQMessagesInputToolbar
|
||||
|
||||
- (void)showVoiceMemoUI;
|
||||
|
||||
- (void)hideVoiceMemoUI:(BOOL)animated;
|
||||
|
||||
- (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,252 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessagesInputToolbar.h"
|
||||
#import "OWSMessagesToolbarContentView.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SignalServiceKit/NSTimer+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessagesInputToolbar () <OWSSendMessageGestureDelegate>
|
||||
|
||||
@property (nonatomic, nullable) UIView *voiceMemoUI;
|
||||
|
||||
@property (nonatomic) UIView *voiceMemoContentView;
|
||||
|
||||
@property (nonatomic) NSDate *voiceMemoStartTime;
|
||||
|
||||
@property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer;
|
||||
|
||||
@property (nonatomic) UILabel *recordingLabel;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessagesInputToolbar
|
||||
|
||||
- (void)toggleSendButtonEnabled
|
||||
{
|
||||
// Do nothing; disables JSQ's control over send button enabling.
|
||||
// Overrides a method in JSQMessagesInputToolbar.
|
||||
}
|
||||
|
||||
- (JSQMessagesToolbarContentView *)loadToolbarContentView
|
||||
{
|
||||
NSArray *views = [[OWSMessagesToolbarContentView nib] instantiateWithOwner:nil options:nil];
|
||||
OWSAssert(views.count == 1);
|
||||
OWSMessagesToolbarContentView *view = views[0];
|
||||
OWSAssert([view isKindOfClass:[OWSMessagesToolbarContentView class]]);
|
||||
view.sendMessageGestureDelegate = self;
|
||||
return view;
|
||||
}
|
||||
|
||||
- (void)showVoiceMemoUI
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
self.voiceMemoStartTime = [NSDate date];
|
||||
|
||||
[self.voiceMemoUI removeFromSuperview];
|
||||
|
||||
self.voiceMemoUI = [UIView new];
|
||||
self.voiceMemoUI.userInteractionEnabled = NO;
|
||||
self.voiceMemoUI.backgroundColor = [UIColor whiteColor];
|
||||
[self addSubview:self.voiceMemoUI];
|
||||
self.voiceMemoUI.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
|
||||
|
||||
self.voiceMemoContentView = [UIView new];
|
||||
[self.voiceMemoUI addSubview:self.voiceMemoContentView];
|
||||
[self.voiceMemoContentView autoPinToSuperviewEdges];
|
||||
|
||||
self.recordingLabel = [UILabel new];
|
||||
self.recordingLabel.textColor = [UIColor ows_destructiveRedColor];
|
||||
self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f];
|
||||
[self.voiceMemoContentView addSubview:self.recordingLabel];
|
||||
[self updateVoiceMemo];
|
||||
|
||||
UIImage *icon = [UIImage imageNamed:@"voice-memo-button"];
|
||||
OWSAssert(icon);
|
||||
UIImageView *imageView =
|
||||
[[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
|
||||
imageView.tintColor = [UIColor ows_destructiveRedColor];
|
||||
[self.voiceMemoContentView addSubview:imageView];
|
||||
|
||||
NSMutableAttributedString *cancelString = [NSMutableAttributedString new];
|
||||
const CGFloat cancelArrowFontSize = ScaleFromIPhone5To7Plus(18.4, 20.f);
|
||||
const CGFloat cancelFontSize = ScaleFromIPhone5To7Plus(14.f, 16.f);
|
||||
NSString *arrowHead = (self.isRTL ? @"\uf105" : @"\uf104");
|
||||
[cancelString
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:arrowHead
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
|
||||
NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
|
||||
NSBaselineOffsetAttributeName : @(-1.f),
|
||||
}]];
|
||||
[cancelString
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:@" "
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
|
||||
NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
|
||||
NSBaselineOffsetAttributeName : @(-1.f),
|
||||
}]];
|
||||
[cancelString
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"VOICE_MESSAGE_CANCEL_INSTRUCTIONS",
|
||||
@"Indicates how to cancel a voice message.")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont ows_mediumFontWithSize:cancelFontSize],
|
||||
NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
|
||||
}]];
|
||||
[cancelString
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:@" "
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
|
||||
NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
|
||||
NSBaselineOffsetAttributeName : @(-1.f),
|
||||
}]];
|
||||
[cancelString
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:arrowHead
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
|
||||
NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
|
||||
NSBaselineOffsetAttributeName : @(-1.f),
|
||||
}]];
|
||||
UILabel *cancelLabel = [UILabel new];
|
||||
cancelLabel.attributedText = cancelString;
|
||||
[self.voiceMemoContentView addSubview:cancelLabel];
|
||||
|
||||
const CGFloat kRedCircleSize = 100.f;
|
||||
UIView *redCircleView = [UIView new];
|
||||
redCircleView.backgroundColor = [UIColor ows_destructiveRedColor];
|
||||
redCircleView.layer.cornerRadius = kRedCircleSize * 0.5f;
|
||||
[redCircleView autoSetDimension:ALDimensionWidth toSize:kRedCircleSize];
|
||||
[redCircleView autoSetDimension:ALDimensionHeight toSize:kRedCircleSize];
|
||||
[self.voiceMemoContentView addSubview:redCircleView];
|
||||
[redCircleView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.contentView.rightBarButtonItem];
|
||||
[redCircleView autoAlignAxis:ALAxisVertical toSameAxisOfView:self.contentView.rightBarButtonItem];
|
||||
|
||||
UIImage *whiteIcon = [UIImage imageNamed:@"voice-message-large-white"];
|
||||
OWSAssert(whiteIcon);
|
||||
UIImageView *whiteIconView = [[UIImageView alloc] initWithImage:whiteIcon];
|
||||
[redCircleView addSubview:whiteIconView];
|
||||
[whiteIconView autoCenterInSuperview];
|
||||
|
||||
[imageView autoVCenterInSuperview];
|
||||
[imageView autoPinLeadingToSuperviewWithMargin:10.f];
|
||||
[self.recordingLabel autoVCenterInSuperview];
|
||||
[self.recordingLabel autoPinLeadingToTrailingOfView:imageView margin:5.f];
|
||||
[cancelLabel autoVCenterInSuperview];
|
||||
[cancelLabel autoHCenterInSuperview];
|
||||
[self.voiceMemoUI setNeedsLayout];
|
||||
[self.voiceMemoUI layoutSubviews];
|
||||
|
||||
// Slide in the "slide to cancel" label.
|
||||
CGRect cancelLabelStartFrame = cancelLabel.frame;
|
||||
CGRect cancelLabelEndFrame = cancelLabel.frame;
|
||||
cancelLabelStartFrame.origin.x
|
||||
= (self.isRTL ? -self.voiceMemoUI.bounds.size.width : self.voiceMemoUI.bounds.size.width);
|
||||
cancelLabel.frame = cancelLabelStartFrame;
|
||||
[UIView animateWithDuration:0.35f
|
||||
delay:0.f
|
||||
options:UIViewAnimationOptionCurveEaseOut
|
||||
animations:^{
|
||||
cancelLabel.frame = cancelLabelEndFrame;
|
||||
}
|
||||
completion:nil];
|
||||
|
||||
// Pulse the icon.
|
||||
imageView.layer.opacity = 1.f;
|
||||
[UIView animateWithDuration:0.5f
|
||||
delay:0.2f
|
||||
options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
|
||||
| UIViewAnimationOptionCurveEaseIn
|
||||
animations:^{
|
||||
imageView.layer.opacity = 0.f;
|
||||
}
|
||||
completion:nil];
|
||||
|
||||
// Fade in the view.
|
||||
self.voiceMemoUI.layer.opacity = 0.f;
|
||||
[UIView animateWithDuration:0.2f
|
||||
animations:^{
|
||||
self.voiceMemoUI.layer.opacity = 1.f;
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
if (finished) {
|
||||
self.voiceMemoUI.layer.opacity = 1.f;
|
||||
}
|
||||
}];
|
||||
|
||||
[self.voiceMemoUpdateTimer invalidate];
|
||||
self.voiceMemoUpdateTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.1f
|
||||
target:self
|
||||
selector:@selector(updateVoiceMemo)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
|
||||
- (void)hideVoiceMemoUI:(BOOL)animated
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
UIView *oldVoiceMemoUI = self.voiceMemoUI;
|
||||
self.voiceMemoUI = nil;
|
||||
NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer;
|
||||
self.voiceMemoUpdateTimer = nil;
|
||||
|
||||
[oldVoiceMemoUI.layer removeAllAnimations];
|
||||
|
||||
if (animated) {
|
||||
[UIView animateWithDuration:0.35f
|
||||
animations:^{
|
||||
oldVoiceMemoUI.layer.opacity = 0.f;
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
[oldVoiceMemoUI removeFromSuperview];
|
||||
[voiceMemoUpdateTimer invalidate];
|
||||
}];
|
||||
} else {
|
||||
[oldVoiceMemoUI removeFromSuperview];
|
||||
[voiceMemoUpdateTimer invalidate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
// Fade out the voice message views as the cancel gesture
|
||||
// proceeds as feedback.
|
||||
self.voiceMemoContentView.layer.opacity = MAX(0.f, MIN(1.f, 1.f - (float)cancelAlpha));
|
||||
}
|
||||
|
||||
- (void)updateVoiceMemo
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
NSTimeInterval durationSeconds = fabs([self.voiceMemoStartTime timeIntervalSinceNow]);
|
||||
self.recordingLabel.text = [ViewControllerUtils formatDurationSeconds:(long)round(durationSeconds)];
|
||||
[self.recordingLabel sizeToFit];
|
||||
}
|
||||
|
||||
#pragma mark - OWSSendMessageGestureDelegate
|
||||
|
||||
- (void)sendMessageGestureRecognized
|
||||
{
|
||||
OWSAssert(self.sendButtonOnRight);
|
||||
[self.delegate messagesInputToolbar:self didPressRightBarButton:self.contentView.rightBarButtonItem];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,45 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol OWSVoiceMemoGestureDelegate <NSObject>
|
||||
|
||||
- (void)voiceMemoGestureDidStart;
|
||||
|
||||
- (void)voiceMemoGestureDidEnd;
|
||||
|
||||
- (void)voiceMemoGestureDidCancel;
|
||||
|
||||
- (void)voiceMemoGestureDidChange:(CGFloat)cancelAlpha;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@protocol OWSSendMessageGestureDelegate <NSObject>
|
||||
|
||||
- (void)sendMessageGestureRecognized;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSMessagesToolbarContentView : JSQMessagesToolbarContentView
|
||||
|
||||
@property (nonatomic, nullable, weak) id<OWSVoiceMemoGestureDelegate> voiceMemoGestureDelegate;
|
||||
|
||||
@property (nonatomic, nullable, weak) id<OWSSendMessageGestureDelegate> sendMessageGestureDelegate;
|
||||
|
||||
- (void)ensureSubviews;
|
||||
|
||||
- (void)ensureEnabling;
|
||||
|
||||
- (void)cancelVoiceMemoIfNecessary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,225 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessagesToolbarContentView.h"
|
||||
#import "UIColor+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessagesToolbarContentView () <UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic) BOOL shouldShowVoiceMemoButton;
|
||||
|
||||
@property (nonatomic, nullable) UIButton *voiceMemoButton;
|
||||
|
||||
@property (nonatomic, nullable) UIButton *sendButton;
|
||||
|
||||
@property (nonatomic) BOOL isRecordingVoiceMemo;
|
||||
|
||||
@property (nonatomic) CGPoint voiceMemoGestureStartLocation;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessagesToolbarContentView
|
||||
|
||||
#pragma mark - Class methods
|
||||
|
||||
+ (UINib *)nib
|
||||
{
|
||||
return [UINib nibWithNibName:NSStringFromClass([OWSMessagesToolbarContentView class])
|
||||
bundle:[NSBundle bundleForClass:[OWSMessagesToolbarContentView class]]];
|
||||
}
|
||||
|
||||
- (void)ensureSubviews
|
||||
{
|
||||
if (!self.sendButton) {
|
||||
OWSAssert(self.rightBarButtonItem);
|
||||
|
||||
self.sendButton = self.rightBarButtonItem;
|
||||
}
|
||||
|
||||
if (!self.voiceMemoButton) {
|
||||
UIImage *icon = [UIImage imageNamed:@"voice-memo-button"];
|
||||
OWSAssert(icon);
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[button setImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
|
||||
forState:UIControlStateNormal];
|
||||
button.imageView.tintColor = [UIColor ows_materialBlueColor];
|
||||
|
||||
// We want to be permissive about the voice message gesture, so we:
|
||||
//
|
||||
// * Add the gesture recognizer to the button's superview instead of the button.
|
||||
// * Filter the touches that the gesture recognizer receives by serving as its
|
||||
// delegate.
|
||||
UILongPressGestureRecognizer *longPressGestureRecognizer =
|
||||
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||||
longPressGestureRecognizer.minimumPressDuration = 0;
|
||||
longPressGestureRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:longPressGestureRecognizer];
|
||||
|
||||
// We want to be permissive about taps on the send button, so we:
|
||||
//
|
||||
// * Add the gesture recognizer to the button's superview instead of the button.
|
||||
// * Filter the touches that the gesture recognizer receives by serving as its
|
||||
// delegate.
|
||||
UITapGestureRecognizer *tapGestureRecognizer =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
tapGestureRecognizer.delegate = self;
|
||||
[self addGestureRecognizer:tapGestureRecognizer];
|
||||
|
||||
self.userInteractionEnabled = YES;
|
||||
|
||||
self.voiceMemoButton = button;
|
||||
}
|
||||
|
||||
[self ensureShouldShowVoiceMemoButton];
|
||||
|
||||
[self ensureVoiceMemoButton];
|
||||
}
|
||||
|
||||
- (void)ensureEnabling
|
||||
{
|
||||
[self ensureShouldShowVoiceMemoButton];
|
||||
|
||||
OWSAssert(self.voiceMemoButton.isEnabled == YES);
|
||||
OWSAssert(self.sendButton.isEnabled == YES);
|
||||
}
|
||||
|
||||
- (void)ensureShouldShowVoiceMemoButton
|
||||
{
|
||||
self.shouldShowVoiceMemoButton = self.textView.text.length < 1;
|
||||
}
|
||||
|
||||
- (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton
|
||||
{
|
||||
if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
_shouldShowVoiceMemoButton = shouldShowVoiceMemoButton;
|
||||
|
||||
[self ensureVoiceMemoButton];
|
||||
}
|
||||
|
||||
- (void)ensureVoiceMemoButton
|
||||
{
|
||||
if (self.shouldShowVoiceMemoButton) {
|
||||
self.rightBarButtonItem = self.voiceMemoButton;
|
||||
self.rightBarButtonItemWidth = [self.voiceMemoButton sizeThatFits:CGSizeZero].width;
|
||||
} else {
|
||||
self.rightBarButtonItem = self.sendButton;
|
||||
self.rightBarButtonItemWidth = [self.sendButton sizeThatFits:CGSizeZero].width;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleLongPress:(UIGestureRecognizer *)sender
|
||||
{
|
||||
switch (sender.state) {
|
||||
case UIGestureRecognizerStatePossible:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed:
|
||||
if (self.isRecordingVoiceMemo) {
|
||||
// Cancel voice message if necessary.
|
||||
self.isRecordingVoiceMemo = NO;
|
||||
[self.voiceMemoGestureDelegate voiceMemoGestureDidCancel];
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateBegan:
|
||||
if (self.isRecordingVoiceMemo) {
|
||||
// Cancel voice message if necessary.
|
||||
self.isRecordingVoiceMemo = NO;
|
||||
[self.voiceMemoGestureDelegate voiceMemoGestureDidCancel];
|
||||
}
|
||||
// Start voice message.
|
||||
self.isRecordingVoiceMemo = YES;
|
||||
self.voiceMemoGestureStartLocation = [sender locationInView:self];
|
||||
[self.voiceMemoGestureDelegate voiceMemoGestureDidStart];
|
||||
break;
|
||||
case UIGestureRecognizerStateChanged:
|
||||
if (self.isRecordingVoiceMemo) {
|
||||
// Check for "slide to cancel" gesture.
|
||||
CGPoint location = [sender locationInView:self];
|
||||
// For LTR/RTL, swiping in either direction will cancel.
|
||||
// This is okay because there's only space on screen to perform the
|
||||
// gesture in one direction.
|
||||
CGFloat offset = fabs(self.voiceMemoGestureStartLocation.x - location.x);
|
||||
// The lower this value, the easier it is to cancel by accident.
|
||||
// The higher this value, the harder it is to cancel.
|
||||
const CGFloat kCancelOffsetPoints = 100.f;
|
||||
CGFloat cancelAlpha = offset / kCancelOffsetPoints;
|
||||
BOOL isCancelled = cancelAlpha >= 1.f;
|
||||
if (isCancelled) {
|
||||
self.isRecordingVoiceMemo = NO;
|
||||
[self.voiceMemoGestureDelegate voiceMemoGestureDidCancel];
|
||||
} else {
|
||||
[self.voiceMemoGestureDelegate voiceMemoGestureDidChange:cancelAlpha];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
if (self.isRecordingVoiceMemo) {
|
||||
// End voice message.
|
||||
self.isRecordingVoiceMemo = NO;
|
||||
[self.voiceMemoGestureDelegate voiceMemoGestureDidEnd];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTap:(UIGestureRecognizer *)sender
|
||||
{
|
||||
switch (sender.state) {
|
||||
case UIGestureRecognizerStateRecognized:
|
||||
[self.sendMessageGestureDelegate sendMessageGestureRecognized];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelVoiceMemoIfNecessary
|
||||
{
|
||||
if (self.isRecordingVoiceMemo) {
|
||||
self.isRecordingVoiceMemo = NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIGestureRecognizerDelegate
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
|
||||
{
|
||||
if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
|
||||
if (self.rightBarButtonItem != self.voiceMemoButton) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// We want to be permissive about the voice message gesture, so we accept
|
||||
// gesture that begin within N points of its bounds.
|
||||
CGFloat kVoiceMemoGestureTolerancePoints = 10;
|
||||
CGPoint location = [touch locationInView:self.voiceMemoButton];
|
||||
CGRect hitTestRect = CGRectInset(
|
||||
self.voiceMemoButton.bounds, -kVoiceMemoGestureTolerancePoints, -kVoiceMemoGestureTolerancePoints);
|
||||
return CGRectContainsPoint(hitTestRect, location);
|
||||
} else if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
|
||||
if (self.rightBarButtonItem == self.voiceMemoButton) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
UIView *sendButton = self.rightBarButtonItem;
|
||||
// We want to be permissive about taps on the send button, so we accept
|
||||
// gesture that begin within N points of its bounds.
|
||||
CGFloat kSendButtonTolerancePoints = 10;
|
||||
CGPoint location = [touch locationInView:sendButton];
|
||||
CGRect hitTestRect = CGRectInset(sendButton.bounds, -kSendButtonTolerancePoints, -kSendButtonTolerancePoints);
|
||||
return CGRectContainsPoint(hitTestRect, location);
|
||||
} else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -2,21 +2,18 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSMessageData.h"
|
||||
#import "OWSViewController.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSInteraction.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ConversationViewItem;
|
||||
@class TSAttachmentStream;
|
||||
|
||||
@interface FullImageViewController : OWSViewController
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream
|
||||
fromRect:(CGRect)rect
|
||||
forInteraction:(TSInteraction *)interaction
|
||||
messageItem:(id<OWSMessageData>)messageItem
|
||||
isAnimated:(BOOL)animated;
|
||||
viewItem:(ConversationViewItem *)viewItem;
|
||||
|
||||
- (void)presentFromViewController:(UIViewController *)viewController;
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
#import "FullImageViewController.h"
|
||||
#import "AttachmentSharing.h"
|
||||
#import "TSAnimatedAdapter.h"
|
||||
#import "TSMessageAdapter.h"
|
||||
#import "TSPhotoAdapter.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSInteraction.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIUtil.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
@ -54,12 +54,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@property (nonatomic) CGRect originRect;
|
||||
@property (nonatomic) BOOL isPresenting;
|
||||
@property (nonatomic) BOOL isAnimated;
|
||||
@property (nonatomic) NSData *fileData;
|
||||
|
||||
@property (nonatomic) TSAttachmentStream *attachment;
|
||||
@property (nonatomic) TSInteraction *interaction;
|
||||
@property (nonatomic) id<OWSMessageData> messageItem;
|
||||
@property (nonatomic) TSAttachmentStream *attachmentStream;
|
||||
@property (nonatomic) ConversationViewItem *viewItem;
|
||||
|
||||
@property (nonatomic) UIToolbar *footerBar;
|
||||
@property (nonatomic) NSArray *oldMenuItems;
|
||||
|
@ -69,27 +67,25 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@implementation FullImageViewController
|
||||
|
||||
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
|
||||
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream
|
||||
fromRect:(CGRect)rect
|
||||
forInteraction:(TSInteraction *)interaction
|
||||
messageItem:(id<OWSMessageData>)messageItem
|
||||
isAnimated:(BOOL)animated {
|
||||
viewItem:(ConversationViewItem *)viewItem
|
||||
{
|
||||
|
||||
self = [super initWithNibName:nil bundle:nil];
|
||||
|
||||
if (self) {
|
||||
self.attachment = attachment;
|
||||
self.attachmentStream = attachmentStream;
|
||||
self.originRect = rect;
|
||||
self.interaction = interaction;
|
||||
self.messageItem = messageItem;
|
||||
self.isAnimated = animated;
|
||||
self.fileData = [NSData dataWithContentsOfURL:[attachment mediaURL]];
|
||||
self.viewItem = viewItem;
|
||||
self.fileData = [NSData dataWithContentsOfURL:[attachmentStream mediaURL]];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UIImage *)image {
|
||||
return self.attachment.image;
|
||||
return self.attachmentStream.image;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
|
@ -155,7 +151,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
- (void)shareWasPressed:(id)sender {
|
||||
DDLogInfo(@"%@: sharing image.", self.tag);
|
||||
|
||||
[AttachmentSharing showShareUIForURL:[self.attachment mediaURL]];
|
||||
[AttachmentSharing showShareUIForURL:[self.attachmentStream mediaURL]];
|
||||
}
|
||||
|
||||
- (void)initializeScrollView {
|
||||
|
@ -167,6 +163,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.contentView addSubview:self.scrollView];
|
||||
}
|
||||
|
||||
- (BOOL)isAnimated
|
||||
{
|
||||
OWSAssert(self.attachmentStream);
|
||||
|
||||
return self.attachmentStream.isAnimated;
|
||||
}
|
||||
|
||||
- (void)initializeImageView {
|
||||
if (self.isAnimated) {
|
||||
if ([self.fileData ows_isValidImage]) {
|
||||
|
@ -255,14 +258,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
animated:NO];
|
||||
}
|
||||
|
||||
NSArray *menuItems = @[
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_COPY_ACTION", @"Short name for edit menu item to copy contents of media message.")
|
||||
action:@selector(copyAttachment:)],
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SAVE_ACTION", @"Short name for edit menu item to save contents of media message.")
|
||||
action:@selector(saveAttachment:)],
|
||||
[[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"EDIT_ITEM_SHARE_ACTION", @"Short name for edit menu item to share contents of media message.")
|
||||
action:@selector(shareAttachment:)],
|
||||
];
|
||||
NSArray *menuItems = self.viewItem.menuControllerItems;
|
||||
if (!self.oldMenuItems) {
|
||||
self.oldMenuItems = [UIMenuController sharedMenuController].menuItems;
|
||||
}
|
||||
|
@ -278,29 +274,39 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
- (void)performEditingActionWithSelector:(SEL)selector {
|
||||
OWSAssert(self.messageItem.messageType == TSIncomingMessageAdapter ||
|
||||
self.messageItem.messageType == TSOutgoingMessageAdapter);
|
||||
OWSAssert([self.messageItem isMediaMessage]);
|
||||
OWSAssert([self.messageItem isKindOfClass:[TSMessageAdapter class]]);
|
||||
OWSAssert([self.messageItem conformsToProtocol:@protocol(OWSMessageEditing)]);
|
||||
OWSAssert([[self.messageItem media] isKindOfClass:[TSPhotoAdapter class]] ||
|
||||
[[self.messageItem media] isKindOfClass:[TSAnimatedAdapter class]]);
|
||||
|
||||
OWSAssert([self.messageItem canPerformEditingAction:selector]);
|
||||
[self.messageItem performEditingAction:selector];
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender
|
||||
{
|
||||
if (action == self.viewItem.metadataActionSelector) {
|
||||
return NO;
|
||||
}
|
||||
return [self.viewItem canPerformAction:action];
|
||||
}
|
||||
|
||||
- (void)copyAttachment:(id)sender {
|
||||
[self performEditingActionWithSelector:NSSelectorFromString(@"copy:")];
|
||||
- (void)copyAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem copyAction];
|
||||
}
|
||||
|
||||
- (void)saveAttachment:(id)sender {
|
||||
[self performEditingActionWithSelector:NSSelectorFromString(@"save:")];
|
||||
- (void)shareAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem shareAction];
|
||||
}
|
||||
|
||||
- (void)shareAttachment:(id)sender {
|
||||
[self performEditingActionWithSelector:NSSelectorFromString(@"share:")];
|
||||
- (void)saveAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem saveAction];
|
||||
}
|
||||
|
||||
- (void)deleteAction:(nullable id)sender
|
||||
{
|
||||
[self.viewItem deleteAction];
|
||||
|
||||
[self dismiss];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Presentation
|
||||
|
|
|
@ -270,6 +270,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
|
|||
}
|
||||
|
||||
[self updateBarButtonItems];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
TSThread *thread = [self threadForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
|
||||
[self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateBarButtonItems
|
||||
|
@ -748,8 +753,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
|
|||
|
||||
// We do this synchronously if we're already on the main thread.
|
||||
DispatchMainThreadSafe(^{
|
||||
ConversationViewController *mvc =
|
||||
[[ConversationViewController alloc] initWithNibName:@"ConversationViewController" bundle:nil];
|
||||
ConversationViewController *mvc = [ConversationViewController new];
|
||||
[mvc configureForThread:thread
|
||||
keyboardOnViewAppearing:keyboardOnViewAppearing
|
||||
callOnViewAppearing:callOnViewAppearing];
|
||||
|
|
|
@ -18,8 +18,15 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
var audioPlayer: OWSAudioAttachmentPlayer?
|
||||
var audioStatusLabel: UILabel?
|
||||
var audioPlayButton: UIButton?
|
||||
var isAudioPlayingFlag = false
|
||||
var isAudioPaused = false
|
||||
var playbackState = AudioPlaybackState.stopped {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
updateAudioStatusLabel()
|
||||
ensureButtonState()
|
||||
}
|
||||
}
|
||||
|
||||
var audioProgressSeconds: CGFloat = 0
|
||||
var audioDurationSeconds: CGFloat = 0
|
||||
|
||||
|
@ -308,22 +315,20 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
|
||||
// MARK: - OWSAudioAttachmentPlayerDelegate
|
||||
|
||||
public func isAudioPlaying() -> Bool {
|
||||
return isAudioPlayingFlag
|
||||
public func audioPlaybackState() -> AudioPlaybackState {
|
||||
return playbackState
|
||||
}
|
||||
|
||||
public func setIsAudioPlaying(_ isAudioPlaying: Bool) {
|
||||
isAudioPlayingFlag = isAudioPlaying
|
||||
|
||||
updateAudioStatusLabel()
|
||||
public func setAudioPlaybackState(_ value: AudioPlaybackState) {
|
||||
playbackState = value
|
||||
}
|
||||
|
||||
public func isPaused() -> Bool {
|
||||
return isAudioPaused
|
||||
}
|
||||
|
||||
public func setIsPaused(_ isPaused: Bool) {
|
||||
isAudioPaused = isPaused
|
||||
private func ensureButtonState() {
|
||||
if playbackState == .playing {
|
||||
setAudioIconToPause()
|
||||
} else {
|
||||
setAudioIconToPlay()
|
||||
}
|
||||
}
|
||||
|
||||
public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
|
||||
|
@ -339,7 +344,8 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
return
|
||||
}
|
||||
|
||||
if isAudioPlayingFlag && audioProgressSeconds > 0 && audioDurationSeconds > 0 {
|
||||
let isAudioPlaying = playbackState == .playing
|
||||
if isAudioPlaying && audioProgressSeconds > 0 && audioDurationSeconds > 0 {
|
||||
audioStatusLabel.text = String(format:"%@ / %@",
|
||||
ViewControllerUtils.formatDurationSeconds(Int(round(self.audioProgressSeconds))),
|
||||
ViewControllerUtils.formatDurationSeconds(Int(round(self.audioDurationSeconds))))
|
||||
|
@ -348,14 +354,14 @@ class MediaMessageView: UIView, OWSAudioAttachmentPlayerDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
public func setAudioIconToPlay() {
|
||||
private 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() {
|
||||
private func setAudioIconToPause() {
|
||||
let image = UIImage(named:"audio_pause_black_large")?.withRenderingMode(.alwaysTemplate)
|
||||
assert(image != nil)
|
||||
audioPlayButton?.setImage(image, for:.normal)
|
||||
|
|
|
@ -2,30 +2,28 @@
|
|||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSVideoAttachmentAdapter;
|
||||
@class YapDatabaseConnection;
|
||||
|
||||
typedef NS_ENUM(NSInteger, AudioPlaybackState) {
|
||||
AudioPlaybackState_Stopped,
|
||||
AudioPlaybackState_Playing,
|
||||
AudioPlaybackState_Paused,
|
||||
};
|
||||
|
||||
@protocol OWSAudioAttachmentPlayerDelegate <NSObject>
|
||||
|
||||
- (BOOL)isAudioPlaying;
|
||||
- (void)setIsAudioPlaying:(BOOL)isAudioPlaying;
|
||||
|
||||
- (BOOL)isPaused;
|
||||
- (void)setIsPaused:(BOOL)isPaused;
|
||||
- (AudioPlaybackState)audioPlaybackState;
|
||||
- (void)setAudioPlaybackState:(AudioPlaybackState)state;
|
||||
|
||||
- (void)setAudioProgress:(CGFloat)progress duration:(CGFloat)duration;
|
||||
- (void)setAudioIconToPlay;
|
||||
- (void)setAudioIconToPause;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSAudioAttachmentPlayer : NSObject <AVAudioPlayerDelegate>
|
||||
@interface OWSAudioAttachmentPlayer : NSObject
|
||||
|
||||
@property (nonatomic, readonly, weak) id<OWSAudioAttachmentPlayerDelegate> delegate;
|
||||
|
||||
|
@ -33,12 +31,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// or model objects.
|
||||
@property (nonatomic, weak) id owner;
|
||||
|
||||
// A convenience initializer for MessagesViewController.
|
||||
//
|
||||
// It assumes the delegate (e.g. view) for this player will be the adapter.
|
||||
- (instancetype)initWithMediaAdapter:(TSVideoAttachmentAdapter *)mediaAdapter
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection;
|
||||
|
||||
// An generic initializer.
|
||||
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl delegate:(id<OWSAudioAttachmentPlayerDelegate>)delegate;
|
||||
|
||||
|
|
|
@ -4,19 +4,16 @@
|
|||
|
||||
#import "OWSAudioAttachmentPlayer.h"
|
||||
#import "Signal-Swift.h"
|
||||
#import "TSAttachment.h"
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSVideoAttachmentAdapter.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <SignalServiceKit/NSTimer+OWS.h>
|
||||
#import <YapDatabase/YapDatabaseConnection.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSAudioAttachmentPlayer ()
|
||||
@interface OWSAudioAttachmentPlayer () <AVAudioPlayerDelegate>
|
||||
|
||||
@property (nonatomic, readonly) NSURL *mediaUrl;
|
||||
|
||||
@property (nonatomic, nullable) AVAudioPlayer *audioPlayer;
|
||||
@property (nonatomic, nullable) NSTimer *audioPlayerPoller;
|
||||
|
||||
|
@ -26,37 +23,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@implementation OWSAudioAttachmentPlayer
|
||||
|
||||
+ (NSURL *)mediaUrlForMediaAdapter:(TSVideoAttachmentAdapter *)mediaAdapter
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
{
|
||||
OWSAssert(mediaAdapter);
|
||||
OWSAssert([mediaAdapter isAudio]);
|
||||
OWSAssert(mediaAdapter.attachmentId);
|
||||
OWSAssert(databaseConnection);
|
||||
|
||||
__block TSAttachment *attachment = nil;
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
attachment = [TSAttachment fetchObjectWithUniqueID:mediaAdapter.attachmentId transaction:transaction];
|
||||
}];
|
||||
OWSAssert(attachment);
|
||||
|
||||
TSAttachmentStream *attachmentStream = nil;
|
||||
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
attachmentStream = (TSAttachmentStream *)attachment;
|
||||
}
|
||||
OWSAssert(attachmentStream);
|
||||
|
||||
return attachmentStream.mediaURL;
|
||||
}
|
||||
|
||||
- (instancetype)initWithMediaAdapter:(TSVideoAttachmentAdapter *)mediaAdapter
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
{
|
||||
return [self initWithMediaUrl:[OWSAudioAttachmentPlayer mediaUrlForMediaAdapter:mediaAdapter
|
||||
databaseConnection:databaseConnection]
|
||||
delegate:mediaAdapter];
|
||||
}
|
||||
|
||||
- (instancetype)initWithMediaUrl:(NSURL *)mediaUrl delegate:(id<OWSAudioAttachmentPlayerDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
@ -98,15 +64,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
OWSAssert(self.mediaUrl);
|
||||
OWSAssert(![self.delegate isAudioPlaying]);
|
||||
OWSAssert([self.delegate audioPlaybackState] != AudioPlaybackState_Playing);
|
||||
|
||||
[ViewControllerUtils setAudioIgnoresHardwareMuteSwitch:YES];
|
||||
|
||||
[self.audioPlayerPoller invalidate];
|
||||
|
||||
self.delegate.isAudioPlaying = YES;
|
||||
self.delegate.isPaused = NO;
|
||||
[self.delegate setAudioIconToPause];
|
||||
self.delegate.audioPlaybackState = AudioPlaybackState_Playing;
|
||||
|
||||
if (!self.audioPlayer) {
|
||||
NSError *error;
|
||||
|
@ -144,12 +108,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
self.delegate.isAudioPlaying = NO;
|
||||
self.delegate.isPaused = YES;
|
||||
self.delegate.audioPlaybackState = AudioPlaybackState_Paused;
|
||||
[self.audioPlayer pause];
|
||||
[self.audioPlayerPoller invalidate];
|
||||
[self.delegate setAudioProgress:[self.audioPlayer currentTime] duration:[self.audioPlayer duration]];
|
||||
[self.delegate setAudioIconToPlay];
|
||||
|
||||
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];
|
||||
}
|
||||
|
@ -158,12 +120,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
self.delegate.audioPlaybackState = AudioPlaybackState_Stopped;
|
||||
[self.audioPlayer pause];
|
||||
[self.audioPlayerPoller invalidate];
|
||||
[self.delegate setAudioProgress:0 duration:0];
|
||||
[self.delegate setAudioIconToPlay];
|
||||
self.delegate.isAudioPlaying = NO;
|
||||
self.delegate.isPaused = NO;
|
||||
|
||||
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];
|
||||
}
|
||||
|
@ -172,7 +132,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
if (self.delegate.isAudioPlaying) {
|
||||
if (self.delegate.audioPlaybackState == AudioPlaybackState_Playing) {
|
||||
[self pause];
|
||||
} else {
|
||||
[self play];
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="15G1217" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="1" customClass="OWSMessagesToolbarContentView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LEq-G7-jGt" userLabel="Left button container">
|
||||
<rect key="frame" x="8" y="6" width="34" height="32"/>
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="0sE-GV-joM"/>
|
||||
<constraint firstAttribute="width" constant="34" id="eMy-Af-wwH"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Myo-1S-Vg1" userLabel="Right button container">
|
||||
<rect key="frame" x="262" y="6" width="50" height="32"/>
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="NaR-re-dJ4"/>
|
||||
<constraint firstAttribute="width" constant="50" id="yde-S9-dHe"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dm4-NT-mvr" customClass="OWSMessagesComposerTextView">
|
||||
<rect key="frame" x="50" y="7" width="204" height="30"/>
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Myo-1S-Vg1" firstAttribute="leading" secondItem="dm4-NT-mvr" secondAttribute="trailing" constant="8" id="7Ld-5r-Hp3"/>
|
||||
<constraint firstItem="dm4-NT-mvr" firstAttribute="top" secondItem="1" secondAttribute="top" constant="7" id="9Tz-Wq-xIf"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dm4-NT-mvr" secondAttribute="bottom" constant="7" id="CCb-V7-yek"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Myo-1S-Vg1" secondAttribute="bottom" constant="6" id="EaS-Oq-Qp5"/>
|
||||
<constraint firstItem="LEq-G7-jGt" firstAttribute="leading" secondItem="1" secondAttribute="leading" constant="8" id="LAU-fo-GJJ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Myo-1S-Vg1" secondAttribute="trailing" constant="8" id="ds6-61-GNv"/>
|
||||
<constraint firstAttribute="bottom" secondItem="LEq-G7-jGt" secondAttribute="bottom" constant="6" id="oG2-YD-ZZI"/>
|
||||
<constraint firstItem="dm4-NT-mvr" firstAttribute="leading" secondItem="LEq-G7-jGt" secondAttribute="trailing" constant="8" id="owo-gB-gyR"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="leftBarButtonContainerView" destination="LEq-G7-jGt" id="F0V-4N-1Mo"/>
|
||||
<outlet property="leftBarButtonContainerViewWidthConstraint" destination="eMy-Af-wwH" id="FI9-F2-2bN"/>
|
||||
<outlet property="leftHorizontalSpacingConstraint" destination="LAU-fo-GJJ" id="X2c-BI-0Q4"/>
|
||||
<outlet property="rightBarButtonContainerView" destination="Myo-1S-Vg1" id="0SR-cw-EkD"/>
|
||||
<outlet property="rightBarButtonContainerViewWidthConstraint" destination="yde-S9-dHe" id="WGu-df-M3L"/>
|
||||
<outlet property="rightHorizontalSpacingConstraint" destination="ds6-61-GNv" id="ZQh-8M-QFs"/>
|
||||
<outlet property="textView" destination="dm4-NT-mvr" id="PFw-HO-oT8"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="268" y="548"/>
|
||||
</view>
|
||||
</objects>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
|
@ -8,6 +8,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@interface OWSViewController : UIViewController
|
||||
|
||||
// We often want to pin one view to the bottom guide
|
||||
// of a view controller BUT adjust its location upward
|
||||
// if the keyboard appears.
|
||||
//
|
||||
// Use this method in lieu of autoPinToBottomLayoutGuideOfViewController:
|
||||
- (void)autoPinViewToBottomGuideOrKeyboard:(UIView *)view;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -3,15 +3,128 @@
|
|||
//
|
||||
|
||||
#import "OWSViewController.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSViewController ()
|
||||
|
||||
@property (nonatomic, weak) UIView *bottomLayoutView;
|
||||
@property (nonatomic) NSLayoutConstraint *bottomLayoutConstraint;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSViewController
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Surface memory leaks by logging the deallocation of view controllers.
|
||||
DDLogVerbose(@"Dealloc: %@", self.class);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)autoPinViewToBottomGuideOrKeyboard:(UIView *)view
|
||||
{
|
||||
OWSAssert(view);
|
||||
OWSAssert(!self.bottomLayoutConstraint);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillShow:)
|
||||
name:UIKeyboardWillShowNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardDidShow:)
|
||||
name:UIKeyboardDidShowNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillHide:)
|
||||
name:UIKeyboardWillHideNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardDidHide:)
|
||||
name:UIKeyboardDidHideNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillChangeFrame:)
|
||||
name:UIKeyboardWillChangeFrameNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardDidChangeFrame:)
|
||||
name:UIKeyboardDidChangeFrameNotification
|
||||
object:nil];
|
||||
|
||||
self.bottomLayoutView = view;
|
||||
self.bottomLayoutConstraint = [view autoPinToBottomLayoutGuideOfViewController:self withInset:0];
|
||||
}
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification
|
||||
{
|
||||
[self handleKeyboardNotification:notification];
|
||||
}
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification
|
||||
{
|
||||
[self handleKeyboardNotification:notification];
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification
|
||||
{
|
||||
[self handleKeyboardNotification:notification];
|
||||
}
|
||||
|
||||
- (void)keyboardDidHide:(NSNotification *)notification
|
||||
{
|
||||
[self handleKeyboardNotification:notification];
|
||||
}
|
||||
|
||||
- (void)keyboardWillChangeFrame:(NSNotification *)notification
|
||||
{
|
||||
[self handleKeyboardNotification:notification];
|
||||
}
|
||||
|
||||
- (void)keyboardDidChangeFrame:(NSNotification *)notification
|
||||
{
|
||||
[self handleKeyboardNotification:notification];
|
||||
}
|
||||
|
||||
- (void)handleKeyboardNotification:(NSNotification *)notification
|
||||
{
|
||||
OWSAssert([NSThread isMainThread]);
|
||||
|
||||
NSDictionary *userInfo = [notification userInfo];
|
||||
|
||||
NSValue *_Nullable keyboardEndFrameValue = userInfo[UIKeyboardFrameEndUserInfoKey];
|
||||
if (!keyboardEndFrameValue) {
|
||||
OWSFail(@"%@ Missing keyboard end frame", self.tag);
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect keyboardEndFrame = [keyboardEndFrameValue CGRectValue];
|
||||
CGRect keyboardEndFrameConverted = [self.view convertRect:keyboardEndFrame fromView:nil];
|
||||
// Adjust the position of the bottom view to account for the keyboard's
|
||||
// intrusion into the view.
|
||||
CGFloat offset = -MAX(0, (self.view.height - keyboardEndFrameConverted.origin.y));
|
||||
|
||||
// There's no need to use: [UIView animateWithDuration:...].
|
||||
// Any layout changes made during these notifications are
|
||||
// automatically animated.
|
||||
self.bottomLayoutConstraint.constant = offset;
|
||||
[self.bottomLayoutView.superview layoutIfNeeded];
|
||||
}
|
||||
|
||||
#pragma mark - Logging
|
||||
|
||||
+ (NSString *)tag
|
||||
{
|
||||
return [NSString stringWithFormat:@"[%@]", self.class];
|
||||
}
|
||||
|
||||
- (NSString *)tag
|
||||
{
|
||||
return self.class.tag;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -10,87 +10,24 @@ class OversizeTextMessageViewController: OWSViewController {
|
|||
|
||||
let TAG = "[OversizeTextMessageViewController]"
|
||||
|
||||
let message: TSMessage
|
||||
let displayableText: String
|
||||
let attachmentStream: TSAttachmentStream
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"use message: constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
message = TSMessage()
|
||||
displayableText = ""
|
||||
attachmentStream = TSAttachmentStream(contentType:"", sourceFilename:"")
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
required init(message: TSMessage) {
|
||||
self.message = message
|
||||
required init(displayableText: String, attachmentStream: TSAttachmentStream) {
|
||||
self.displayableText = displayableText
|
||||
self.attachmentStream = attachmentStream
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
// MARK: Attachment
|
||||
|
||||
private func attachmentStream() -> TSAttachmentStream? {
|
||||
guard message.hasAttachments() else {
|
||||
Logger.error("\(TAG) message has no attachments.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
guard let attachmentID = message.attachmentIds[0] as? String else {
|
||||
Logger.error("\(TAG) message attachment id is not a string.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
guard let attachment = TSAttachment.fetch(uniqueId: attachmentID) as? TSAttachmentStream else {
|
||||
Logger.error("\(TAG) could not load attachment.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
guard attachment.contentType == OWSMimeTypeOversizeTextMessage else {
|
||||
Logger.error("\(TAG) attachment has unexpected content type.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
return attachment
|
||||
}
|
||||
|
||||
private func attachmentData() -> Data? {
|
||||
guard let stream = attachmentStream() else {
|
||||
Logger.error("\(TAG) attachment has invalid stream.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
guard let mediaURL = stream.mediaURL() else {
|
||||
Logger.error("\(TAG) attachment missing URL.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let textData = try Data(contentsOf:mediaURL)
|
||||
return textData
|
||||
} catch {
|
||||
Logger.error("\(TAG) error loading data.")
|
||||
assert(false)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func displayText() -> String {
|
||||
guard let textData = attachmentData() else {
|
||||
Logger.error("\(TAG) could not load attachment data.")
|
||||
assert(false)
|
||||
return ""
|
||||
}
|
||||
guard let fullText = String(data:textData, encoding:.utf8) else {
|
||||
Logger.error("\(TAG) text is empty.")
|
||||
assert(false)
|
||||
return ""
|
||||
}
|
||||
guard let displayText = DisplayableTextFilter().displayableText(fullText) else {
|
||||
Logger.error("\(TAG) No valid text.")
|
||||
assert(false)
|
||||
return ""
|
||||
}
|
||||
return displayText
|
||||
}
|
||||
|
||||
// MARK: View Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
@ -103,7 +40,7 @@ class OversizeTextMessageViewController: OWSViewController {
|
|||
|
||||
let textView = UITextView()
|
||||
textView.textColor = UIColor.black
|
||||
textView.text = displayText()
|
||||
textView.text = displayableText
|
||||
textView.font = UIFont.ows_dynamicTypeBody()
|
||||
textView.isEditable = false
|
||||
textView.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4)
|
||||
|
@ -133,12 +70,6 @@ class OversizeTextMessageViewController: OWSViewController {
|
|||
func shareWasPressed(sender: UIButton) {
|
||||
Logger.info("\(TAG) sharing oversize text.")
|
||||
|
||||
guard let attachment = attachmentStream() else {
|
||||
Logger.error("\(TAG) attachment has invalid stream.")
|
||||
assert(false)
|
||||
return
|
||||
}
|
||||
|
||||
AttachmentSharing.showShareUI(for:attachment.mediaURL())
|
||||
AttachmentSharing.showShareUI(for:attachmentStream.mediaURL())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,6 +106,8 @@ enum GiphyRequestPriority {
|
|||
}
|
||||
|
||||
// A simple LRU cache bounded by the number of entries.
|
||||
//
|
||||
// TODO: We might want to observe memory pressure notifications.
|
||||
class LRUCache<KeyType: Hashable & Equatable, ValueType> {
|
||||
|
||||
private var cacheMap = [KeyType: ValueType]()
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewCell.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSContactOffersInteraction;
|
||||
|
||||
@protocol OWSContactOffersCellDelegate <NSObject>
|
||||
|
||||
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactOffersCell : JSQMessagesCollectionViewCell
|
||||
|
||||
@property (nonatomic, weak) id<OWSContactOffersCellDelegate> contactOffersCellDelegate;
|
||||
|
||||
@property (nonatomic, nullable, readonly) OWSContactOffersInteraction *interaction;
|
||||
|
||||
- (void)configureWithInteraction:(OWSContactOffersInteraction *)interaction;
|
||||
|
||||
- (CGSize)bubbleSizeForInteraction:(OWSContactOffersInteraction *)interaction
|
||||
collectionViewWidth:(CGFloat)collectionViewWidth;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSExpirationTimerView;
|
||||
|
||||
static const CGFloat OWSExpirableMessageViewTimerWidth = 10.0f;
|
||||
|
||||
@protocol OWSExpirableMessageView
|
||||
|
||||
@property (strong, nonatomic, readonly) IBOutlet OWSExpirationTimerView *expirationTimerView;
|
||||
@property (strong, nonatomic, readonly) IBOutlet NSLayoutConstraint *expirationTimerViewWidthConstraint;
|
||||
|
||||
- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
initialDurationSeconds:(uint32_t)initialDurationSeconds;
|
||||
|
||||
- (void)stopExpirationTimer;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "JSQMessagesCollectionViewCell+OWS.h"
|
||||
#import "OWSExpirableMessageView.h"
|
||||
#import "OWSMessageCollectionViewCell.h"
|
||||
#import "OWSMessageMediaAdapter.h"
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewCellIncoming.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class JSQMediaItem;
|
||||
|
||||
@interface OWSIncomingMessageCollectionViewCell
|
||||
: JSQMessagesCollectionViewCellIncoming <OWSExpirableMessageView, OWSMessageCollectionViewCell>
|
||||
|
||||
@property (nonatomic, nullable) id<OWSMessageMediaAdapter> mediaAdapter;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,73 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSIncomingMessageCollectionViewCell.h"
|
||||
#import "OWSExpirationTimerView.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import <JSQMessagesViewController/JSQMediaItem.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSIncomingMessageCollectionViewCell ()
|
||||
|
||||
@property (strong, nonatomic) IBOutlet OWSExpirationTimerView *expirationTimerView;
|
||||
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *expirationTimerViewWidthConstraint;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSIncomingMessageCollectionViewCell
|
||||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
[super awakeFromNib];
|
||||
self.expirationTimerViewWidthConstraint.constant = 0.0;
|
||||
}
|
||||
|
||||
- (void)prepareForReuse
|
||||
{
|
||||
[super prepareForReuse];
|
||||
self.expirationTimerViewWidthConstraint.constant = 0.0f;
|
||||
|
||||
[self.mediaAdapter setCellVisible:NO];
|
||||
|
||||
// Clear this adapter's views IFF this was the last cell to use this adapter.
|
||||
[self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
|
||||
[_mediaAdapter setLastPresentingCell:nil];
|
||||
|
||||
self.mediaAdapter = nil;
|
||||
}
|
||||
|
||||
- (void)setMediaAdapter:(nullable id<OWSMessageMediaAdapter>)mediaAdapter
|
||||
{
|
||||
_mediaAdapter = mediaAdapter;
|
||||
|
||||
// Mark this as the last cell to use this adapter.
|
||||
[_mediaAdapter setLastPresentingCell:self];
|
||||
}
|
||||
|
||||
// pragma mark - OWSMessageCollectionViewCell
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible
|
||||
{
|
||||
[self.mediaAdapter setCellVisible:isVisible];
|
||||
}
|
||||
|
||||
// pragma mark - OWSExpirableMessageView
|
||||
|
||||
- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
initialDurationSeconds:(uint32_t)initialDurationSeconds
|
||||
{
|
||||
self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
|
||||
[self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
|
||||
initialDurationSeconds:initialDurationSeconds];
|
||||
}
|
||||
|
||||
- (void)stopExpirationTimer
|
||||
{
|
||||
[self.expirationTimerView stopTimer];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,150 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11201" systemVersion="15G1004" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11161"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="" id="4lh-CK-yVn" customClass="OWSIncomingMessageCollectionViewCell">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="154"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="154"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="cell top label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="afj-rd-iNv" userLabel="Cell top label" customClass="JSQMessagesLabel">
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="fKS-MR-YPI"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="bubble top label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ufa-bF-l1Y" userLabel="Bubble top label" customClass="JSQMessagesLabel">
|
||||
<color key="backgroundColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="fal-sy-hrK"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="btS-p8-B7Z" userLabel="Bubble container">
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="OCS-Fu-acq" userLabel="Bubble Image View"/>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KYU-B8-cUW" customClass="JSQMessagesCellTextView">
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="KYU-B8-cUW" secondAttribute="trailing" id="4qS-03-PFO"/>
|
||||
<constraint firstAttribute="bottom" secondItem="KYU-B8-cUW" secondAttribute="bottom" id="B2v-Gq-Y1L"/>
|
||||
<constraint firstAttribute="trailing" secondItem="OCS-Fu-acq" secondAttribute="trailing" id="TdB-8V-aUc"/>
|
||||
<constraint firstItem="KYU-B8-cUW" firstAttribute="leading" secondItem="btS-p8-B7Z" secondAttribute="leading" constant="6" id="Tg9-9l-vr8"/>
|
||||
<constraint firstItem="KYU-B8-cUW" firstAttribute="top" secondItem="btS-p8-B7Z" secondAttribute="top" id="aEL-yH-N1p"/>
|
||||
<constraint firstAttribute="bottom" secondItem="OCS-Fu-acq" secondAttribute="bottom" id="aJ4-ZD-tk9"/>
|
||||
<constraint firstItem="OCS-Fu-acq" firstAttribute="leading" secondItem="btS-p8-B7Z" secondAttribute="leading" id="qpQ-dc-2V5"/>
|
||||
<constraint firstAttribute="width" constant="244" id="stE-iz-VHo"/>
|
||||
<constraint firstItem="OCS-Fu-acq" firstAttribute="top" secondItem="btS-p8-B7Z" secondAttribute="top" id="zTa-8g-VY4"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Da1-09-wR4" userLabel="Avatar container">
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="w23-sL-0Rh" userLabel="Avatar Image View"/>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="w23-sL-0Rh" firstAttribute="leading" secondItem="Da1-09-wR4" secondAttribute="leading" id="6zm-sH-cOT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="w23-sL-0Rh" secondAttribute="trailing" id="JgK-7W-L10"/>
|
||||
<constraint firstAttribute="bottom" secondItem="w23-sL-0Rh" secondAttribute="bottom" id="U39-Ze-JZ6"/>
|
||||
<constraint firstAttribute="width" constant="34" id="YwX-fW-Me6"/>
|
||||
<constraint firstItem="w23-sL-0Rh" firstAttribute="top" secondItem="Da1-09-wR4" secondAttribute="top" id="hxb-KR-hNK"/>
|
||||
<constraint firstAttribute="height" constant="34" id="tZk-AK-JFa"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0eu-Z9-ndP" userLabel="Footer Container">
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="cell bottom label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UPz-5x-c1T" userLabel="Cell bottom label" customClass="JSQMessagesLabel">
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="xPR-Ph-Ze9"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="UYa-ee-mie" userLabel="Expiration Timer" customClass="OWSExpirationTimerView">
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="10" id="Spx-mv-2DX"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="UPz-5x-c1T" secondAttribute="bottom" id="3hV-mq-Bcf"/>
|
||||
<constraint firstItem="UPz-5x-c1T" firstAttribute="top" secondItem="0eu-Z9-ndP" secondAttribute="top" id="7v5-pU-s1n"/>
|
||||
<constraint firstAttribute="trailing" secondItem="UPz-5x-c1T" secondAttribute="trailing" id="TlZ-tg-six"/>
|
||||
<constraint firstItem="UPz-5x-c1T" firstAttribute="leading" secondItem="UYa-ee-mie" secondAttribute="trailing" id="WVv-6q-I5l"/>
|
||||
<constraint firstAttribute="bottom" secondItem="UYa-ee-mie" secondAttribute="bottom" id="fVy-Bf-ePa"/>
|
||||
<constraint firstItem="UYa-ee-mie" firstAttribute="top" secondItem="0eu-Z9-ndP" secondAttribute="top" id="iHb-8v-vc4"/>
|
||||
<constraint firstItem="UYa-ee-mie" firstAttribute="leading" secondItem="0eu-Z9-ndP" secondAttribute="leading" id="p2g-wW-Ra6"/>
|
||||
<constraint firstAttribute="height" secondItem="UPz-5x-c1T" secondAttribute="height" id="vbI-L6-mkn"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="0eu-Z9-ndP" secondAttribute="trailing" id="JeQ-cB-BZe"/>
|
||||
<constraint firstItem="0eu-Z9-ndP" firstAttribute="top" secondItem="btS-p8-B7Z" secondAttribute="bottom" id="JwS-lV-yWY"/>
|
||||
<constraint firstAttribute="trailing" secondItem="afj-rd-iNv" secondAttribute="trailing" id="Ka4-Dy-jCS"/>
|
||||
<constraint firstItem="0eu-Z9-ndP" firstAttribute="top" secondItem="Da1-09-wR4" secondAttribute="bottom" id="Mxx-Zi-4pN"/>
|
||||
<constraint firstItem="afj-rd-iNv" firstAttribute="leading" secondItem="4lh-CK-yVn" secondAttribute="leading" id="OnD-mZ-QtR"/>
|
||||
<constraint firstAttribute="bottom" secondItem="0eu-Z9-ndP" secondAttribute="bottom" id="VPM-Vk-cDj"/>
|
||||
<constraint firstItem="Ufa-bF-l1Y" firstAttribute="leading" secondItem="4lh-CK-yVn" secondAttribute="leading" id="Z5z-m3-8Ne"/>
|
||||
<constraint firstItem="afj-rd-iNv" firstAttribute="top" secondItem="4lh-CK-yVn" secondAttribute="top" id="ZG9-2M-N52"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Ufa-bF-l1Y" secondAttribute="trailing" id="cWQ-1Q-xOA"/>
|
||||
<constraint firstItem="Ufa-bF-l1Y" firstAttribute="top" secondItem="afj-rd-iNv" secondAttribute="bottom" id="i9Y-sV-v6b"/>
|
||||
<constraint firstItem="btS-p8-B7Z" firstAttribute="leading" secondItem="Da1-09-wR4" secondAttribute="trailing" constant="2" id="j0I-pm-eex"/>
|
||||
<constraint firstItem="btS-p8-B7Z" firstAttribute="top" secondItem="Ufa-bF-l1Y" secondAttribute="bottom" id="jAu-Dn-7rN"/>
|
||||
<constraint firstItem="Da1-09-wR4" firstAttribute="leading" secondItem="4lh-CK-yVn" secondAttribute="leading" id="jiu-B4-TSD"/>
|
||||
<constraint firstItem="0eu-Z9-ndP" firstAttribute="leading" secondItem="4lh-CK-yVn" secondAttribute="leading" constant="10" id="kwA-RP-CSv"/>
|
||||
</constraints>
|
||||
<size key="customSize" width="317" height="245"/>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="Da1-09-wR4" id="tpM-P4-qUR"/>
|
||||
<outlet property="avatarContainerViewHeightConstraint" destination="tZk-AK-JFa" id="tPu-mC-ITh"/>
|
||||
<outlet property="avatarContainerViewWidthConstraint" destination="YwX-fW-Me6" id="5PN-NL-hEP"/>
|
||||
<outlet property="avatarImageView" destination="w23-sL-0Rh" id="Yf4-UR-VRC"/>
|
||||
<outlet property="cellBottomLabel" destination="UPz-5x-c1T" id="MKm-hp-T6v"/>
|
||||
<outlet property="cellBottomLabelHeightConstraint" destination="xPR-Ph-Ze9" id="nvV-Tk-AIs"/>
|
||||
<outlet property="cellTopLabel" destination="afj-rd-iNv" id="bTd-4q-U7e"/>
|
||||
<outlet property="cellTopLabelHeightConstraint" destination="fKS-MR-YPI" id="YWd-Rd-qSL"/>
|
||||
<outlet property="expirationTimerView" destination="UYa-ee-mie" id="y5V-L9-vWJ"/>
|
||||
<outlet property="expirationTimerViewWidthConstraint" destination="Spx-mv-2DX" id="ebW-8s-AfN"/>
|
||||
<outlet property="messageBubbleContainerView" destination="btS-p8-B7Z" id="2sk-5p-NEd"/>
|
||||
<outlet property="messageBubbleContainerWidthConstraint" destination="stE-iz-VHo" id="lle-iT-67d"/>
|
||||
<outlet property="messageBubbleImageView" destination="OCS-Fu-acq" id="OuN-5t-30g"/>
|
||||
<outlet property="messageBubbleTopLabel" destination="Ufa-bF-l1Y" id="VtH-te-blR"/>
|
||||
<outlet property="messageBubbleTopLabelHeightConstraint" destination="fal-sy-hrK" id="kgv-NO-Gud"/>
|
||||
<outlet property="textView" destination="KYU-B8-cUW" id="1Yv-ln-EUZ"/>
|
||||
<outlet property="textViewAvatarHorizontalSpaceConstraint" destination="Tg9-9l-vr8" id="HWn-aO-NbR"/>
|
||||
<outlet property="textViewBottomVerticalSpaceConstraint" destination="B2v-Gq-Y1L" id="oKV-Ti-Oci"/>
|
||||
<outlet property="textViewMarginHorizontalSpaceConstraint" destination="4qS-03-PFO" id="1Qe-Ee-fUO"/>
|
||||
<outlet property="textViewTopVerticalSpaceConstraint" destination="aEL-yH-N1p" id="WPl-0b-tf1"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="255" y="202"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol OWSMessageCollectionViewCell
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "JSQMessagesCollectionViewCell+OWS.h"
|
||||
#import "OWSExpirableMessageView.h"
|
||||
#import "OWSMessageCollectionViewCell.h"
|
||||
#import "OWSMessageMediaAdapter.h"
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewCellOutgoing.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class JSQMediaItem;
|
||||
|
||||
@interface OWSOutgoingMessageCollectionViewCell
|
||||
: JSQMessagesCollectionViewCellOutgoing <OWSExpirableMessageView, OWSMessageCollectionViewCell>
|
||||
|
||||
@property (nonatomic, nullable) id<OWSMessageMediaAdapter> mediaAdapter;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSOutgoingMessageCollectionViewCell.h"
|
||||
#import "OWSExpirationTimerView.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <JSQMessagesViewController/JSQMediaItem.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSOutgoingMessageCollectionViewCell ()
|
||||
|
||||
@property (strong, nonatomic) IBOutlet OWSExpirationTimerView *expirationTimerView;
|
||||
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *expirationTimerViewWidthConstraint;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSOutgoingMessageCollectionViewCell
|
||||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
[super awakeFromNib];
|
||||
self.expirationTimerViewWidthConstraint.constant = 0.0;
|
||||
|
||||
// Our text alignment needs to adapt to RTL.
|
||||
self.cellBottomLabel.textAlignment = [self.cellBottomLabel textAlignmentUnnatural];
|
||||
}
|
||||
|
||||
- (void)prepareForReuse
|
||||
{
|
||||
[super prepareForReuse];
|
||||
self.mediaView.alpha = 1.0;
|
||||
self.expirationTimerViewWidthConstraint.constant = 0.0f;
|
||||
|
||||
[self.mediaAdapter setCellVisible:NO];
|
||||
|
||||
// Clear this adapter's views IFF this was the last cell to use this adapter.
|
||||
[self.mediaAdapter clearCachedMediaViewsIfLastPresentingCell:self];
|
||||
[_mediaAdapter setLastPresentingCell:nil];
|
||||
|
||||
self.mediaAdapter = nil;
|
||||
}
|
||||
|
||||
- (void)setMediaAdapter:(nullable id<OWSMessageMediaAdapter>)mediaAdapter
|
||||
{
|
||||
_mediaAdapter = mediaAdapter;
|
||||
|
||||
// Mark this as the last cell to use this adapter.
|
||||
[_mediaAdapter setLastPresentingCell:self];
|
||||
}
|
||||
|
||||
// pragma mark - OWSMessageCollectionViewCell
|
||||
|
||||
- (void)setCellVisible:(BOOL)isVisible
|
||||
{
|
||||
[self.mediaAdapter setCellVisible:isVisible];
|
||||
}
|
||||
|
||||
- (UIColor *)ows_textColor
|
||||
{
|
||||
return [UIColor whiteColor];
|
||||
}
|
||||
|
||||
// pragma mark - OWSExpirableMessageView
|
||||
|
||||
- (void)startExpirationTimerWithExpiresAtSeconds:(double)expiresAtSeconds
|
||||
initialDurationSeconds:(uint32_t)initialDurationSeconds
|
||||
{
|
||||
self.expirationTimerViewWidthConstraint.constant = OWSExpirableMessageViewTimerWidth;
|
||||
[self.expirationTimerView startTimerWithExpiresAtSeconds:expiresAtSeconds
|
||||
initialDurationSeconds:initialDurationSeconds];
|
||||
}
|
||||
|
||||
- (void)stopExpirationTimer
|
||||
{
|
||||
[self.expirationTimerView stopTimer];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,150 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11201" systemVersion="15G1004" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11161"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="" id="23f-xH-rkY" customClass="OWSOutgoingMessageCollectionViewCell">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="154"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="154"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="cell top label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jxM-YD-sVG" userLabel="Cell top label" customClass="JSQMessagesLabel">
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="9oK-E7-iXA"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="bubble top label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p52-YN-yLu" userLabel="Bubble top label" customClass="JSQMessagesLabel">
|
||||
<color key="backgroundColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="8TB-va-f8L"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2zh-vR-QJW" userLabel="Bubble container">
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="2qm-c6-OZf" userLabel="Bubble Image View"/>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="vLY-aM-0Dr" customClass="JSQMessagesCellTextView">
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vLY-aM-0Dr" firstAttribute="leading" secondItem="2zh-vR-QJW" secondAttribute="leading" id="7rI-Nc-AK3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2qm-c6-OZf" secondAttribute="trailing" id="AEu-1l-tqh"/>
|
||||
<constraint firstItem="2qm-c6-OZf" firstAttribute="top" secondItem="2zh-vR-QJW" secondAttribute="top" id="DbW-Cx-zOW"/>
|
||||
<constraint firstItem="2qm-c6-OZf" firstAttribute="leading" secondItem="2zh-vR-QJW" secondAttribute="leading" id="H1H-yn-Raq"/>
|
||||
<constraint firstItem="vLY-aM-0Dr" firstAttribute="top" secondItem="2zh-vR-QJW" secondAttribute="top" id="RiG-21-Bqc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vLY-aM-0Dr" secondAttribute="bottom" id="UbF-Bl-Q7v"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vLY-aM-0Dr" secondAttribute="trailing" constant="6" id="aVg-yy-8K7"/>
|
||||
<constraint firstAttribute="width" constant="244" id="imD-52-K45"/>
|
||||
<constraint firstAttribute="bottom" secondItem="2qm-c6-OZf" secondAttribute="bottom" id="lts-Ve-wSh"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="X89-B1-aAd" userLabel="Avatar container">
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="y9b-D9-Q7W" userLabel="Avatar Image View"/>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="y9b-D9-Q7W" secondAttribute="bottom" id="7SX-4t-GAr"/>
|
||||
<constraint firstAttribute="width" constant="34" id="Pkm-tW-k4z"/>
|
||||
<constraint firstItem="y9b-D9-Q7W" firstAttribute="leading" secondItem="X89-B1-aAd" secondAttribute="leading" id="Pya-tL-FjE"/>
|
||||
<constraint firstItem="y9b-D9-Q7W" firstAttribute="top" secondItem="X89-B1-aAd" secondAttribute="top" id="e5w-hn-mre"/>
|
||||
<constraint firstAttribute="height" constant="34" id="tgw-aN-JJu"/>
|
||||
<constraint firstAttribute="trailing" secondItem="y9b-D9-Q7W" secondAttribute="trailing" id="w9X-3u-BNY"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wFK-dt-TWZ" userLabel="Footer Container">
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="cell bottom label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Uc-dI-YDD" userLabel="Cell bottom label" customClass="JSQMessagesLabel">
|
||||
<color key="backgroundColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="20" id="cPs-M4-tjX"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0tt-tR-3Iu" userLabel="Expiration Timer" customClass="OWSExpirationTimerView">
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="10" id="5XO-l6-PgT"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0tt-tR-3Iu" firstAttribute="leading" secondItem="7Uc-dI-YDD" secondAttribute="trailing" id="3rd-VE-2kc"/>
|
||||
<constraint firstItem="7Uc-dI-YDD" firstAttribute="leading" secondItem="wFK-dt-TWZ" secondAttribute="leading" id="8bH-5V-WeT"/>
|
||||
<constraint firstItem="7Uc-dI-YDD" firstAttribute="top" secondItem="wFK-dt-TWZ" secondAttribute="top" id="HU0-Ql-nzh"/>
|
||||
<constraint firstAttribute="bottom" secondItem="7Uc-dI-YDD" secondAttribute="bottom" id="aG2-Au-de4"/>
|
||||
<constraint firstItem="0tt-tR-3Iu" firstAttribute="top" secondItem="wFK-dt-TWZ" secondAttribute="top" id="cWj-FU-zAs"/>
|
||||
<constraint firstAttribute="bottom" secondItem="0tt-tR-3Iu" secondAttribute="bottom" id="d3u-eO-0Vh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="0tt-tR-3Iu" secondAttribute="trailing" id="ikV-vO-dvu"/>
|
||||
<constraint firstItem="7Uc-dI-YDD" firstAttribute="height" secondItem="wFK-dt-TWZ" secondAttribute="height" id="tBD-NV-24p"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstItem="wFK-dt-TWZ" firstAttribute="leading" secondItem="23f-xH-rkY" secondAttribute="leading" id="3WR-qA-BlN"/>
|
||||
<constraint firstItem="2zh-vR-QJW" firstAttribute="top" secondItem="p52-YN-yLu" secondAttribute="bottom" id="3Wx-g0-fTc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="jxM-YD-sVG" secondAttribute="trailing" id="AwY-g7-f1T"/>
|
||||
<constraint firstItem="wFK-dt-TWZ" firstAttribute="top" secondItem="2zh-vR-QJW" secondAttribute="bottom" id="Ckg-Bj-HXn"/>
|
||||
<constraint firstItem="jxM-YD-sVG" firstAttribute="top" secondItem="23f-xH-rkY" secondAttribute="top" id="HYT-Tw-whz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="X89-B1-aAd" secondAttribute="trailing" id="KLt-Ix-wDa"/>
|
||||
<constraint firstAttribute="bottom" secondItem="wFK-dt-TWZ" secondAttribute="bottom" id="g9n-w3-IXp"/>
|
||||
<constraint firstAttribute="trailing" secondItem="p52-YN-yLu" secondAttribute="trailing" id="gen-N0-uZj"/>
|
||||
<constraint firstItem="wFK-dt-TWZ" firstAttribute="top" secondItem="X89-B1-aAd" secondAttribute="bottom" id="hNe-hv-ytt"/>
|
||||
<constraint firstItem="p52-YN-yLu" firstAttribute="top" secondItem="jxM-YD-sVG" secondAttribute="bottom" id="jBD-JV-AWk"/>
|
||||
<constraint firstItem="jxM-YD-sVG" firstAttribute="leading" secondItem="23f-xH-rkY" secondAttribute="leading" id="qeB-G5-1Wq"/>
|
||||
<constraint firstAttribute="trailing" secondItem="wFK-dt-TWZ" secondAttribute="trailing" constant="10" id="r4Q-Yj-BY6"/>
|
||||
<constraint firstItem="p52-YN-yLu" firstAttribute="leading" secondItem="23f-xH-rkY" secondAttribute="leading" id="tTj-Mp-0va"/>
|
||||
<constraint firstItem="X89-B1-aAd" firstAttribute="leading" secondItem="2zh-vR-QJW" secondAttribute="trailing" constant="2" id="vMz-Yi-B0w"/>
|
||||
</constraints>
|
||||
<size key="customSize" width="317" height="245"/>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="X89-B1-aAd" id="WSI-Zc-qIE"/>
|
||||
<outlet property="avatarContainerViewHeightConstraint" destination="tgw-aN-JJu" id="pgV-tY-5Cm"/>
|
||||
<outlet property="avatarContainerViewWidthConstraint" destination="Pkm-tW-k4z" id="Cpe-d3-yiq"/>
|
||||
<outlet property="avatarImageView" destination="y9b-D9-Q7W" id="cZo-SR-S9h"/>
|
||||
<outlet property="cellBottomLabel" destination="7Uc-dI-YDD" id="gVD-C2-UcZ"/>
|
||||
<outlet property="cellBottomLabelHeightConstraint" destination="cPs-M4-tjX" id="b5k-6e-iA8"/>
|
||||
<outlet property="cellTopLabel" destination="jxM-YD-sVG" id="acH-pr-spx"/>
|
||||
<outlet property="cellTopLabelHeightConstraint" destination="9oK-E7-iXA" id="MZM-kV-2dI"/>
|
||||
<outlet property="expirationTimerView" destination="0tt-tR-3Iu" id="1Yp-Fs-DUh"/>
|
||||
<outlet property="expirationTimerViewWidthConstraint" destination="5XO-l6-PgT" id="6md-KV-QkY"/>
|
||||
<outlet property="messageBubbleContainerView" destination="2zh-vR-QJW" id="pu0-GU-eZl"/>
|
||||
<outlet property="messageBubbleContainerWidthConstraint" destination="imD-52-K45" id="Xld-Pa-yJw"/>
|
||||
<outlet property="messageBubbleImageView" destination="2qm-c6-OZf" id="bpy-Gv-jSh"/>
|
||||
<outlet property="messageBubbleTopLabel" destination="p52-YN-yLu" id="SLH-sA-Chu"/>
|
||||
<outlet property="messageBubbleTopLabelHeightConstraint" destination="8TB-va-f8L" id="FNt-BS-Wxi"/>
|
||||
<outlet property="textView" destination="vLY-aM-0Dr" id="YEp-mW-xIY"/>
|
||||
<outlet property="textViewAvatarHorizontalSpaceConstraint" destination="aVg-yy-8K7" id="CIe-Bi-eng"/>
|
||||
<outlet property="textViewBottomVerticalSpaceConstraint" destination="UbF-Bl-Q7v" id="KHP-49-3u4"/>
|
||||
<outlet property="textViewMarginHorizontalSpaceConstraint" destination="7rI-Nc-AK3" id="ciu-j6-IpH"/>
|
||||
<outlet property="textViewTopVerticalSpaceConstraint" destination="RiG-21-Bqc" id="i3j-z0-feE"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="371" y="145"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
|
@ -1,35 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewCell.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSInteraction;
|
||||
@class OWSSystemMessageCell;
|
||||
|
||||
@protocol OWSSystemMessageCellDelegate <NSObject>
|
||||
|
||||
- (void)didTapSystemMessageWithInteraction:(TSInteraction *)interaction;
|
||||
- (void)didLongPressSystemMessageCell:(OWSSystemMessageCell *)systemMessageCell;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSSystemMessageCell : JSQMessagesCollectionViewCell
|
||||
|
||||
@property (nonatomic, weak) id<OWSSystemMessageCellDelegate> systemMessageCellDelegate;
|
||||
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, nullable, readonly) TSInteraction *interaction;
|
||||
|
||||
- (void)configureWithInteraction:(TSInteraction *)interaction;
|
||||
|
||||
- (CGSize)bubbleSizeForInteraction:(TSInteraction *)interaction collectionViewWidth:(CGFloat)collectionViewWidth;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewCell.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class TSUnreadIndicatorInteraction;
|
||||
|
||||
@interface OWSUnreadIndicatorCell : JSQMessagesCollectionViewCell
|
||||
|
||||
@property (nonatomic, nullable, readonly) TSUnreadIndicatorInteraction *interaction;
|
||||
|
||||
- (void)configureWithInteraction:(TSUnreadIndicatorInteraction *)interaction;
|
||||
|
||||
- (CGSize)bubbleSizeForInteraction:(TSUnreadIndicatorInteraction *)interaction
|
||||
collectionViewWidth:(CGFloat)collectionViewWidth;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Signal-Bridging-Header.h"
|
||||
#import "OWSMessagesBubblesSizeCalculator.h"
|
||||
#import <JSQMessagesViewController/JSQMessageData.h>
|
||||
#import <JSQMessagesViewController/JSQMessagesCollectionViewFlowLayout.h>
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "TSAttachmentStream.h"
|
||||
#import "TSContentAdapters.h"
|
||||
#import "TSOutgoingMessage.h"
|
||||
#import <MobileCoreServices/MobileCoreServices.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue