Remove JSQ.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-10-10 16:13:54 -04:00
parent 796be18c56
commit fb408f980c
110 changed files with 5871 additions and 5879 deletions

View File

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

View File

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

View File

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

View File

@ -47,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN
return YES;
}
- (OWSInteractionType)interactionType
{
return OWSInteractionType_Offer;
}
@end
NS_ASSUME_NONNULL_END

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,6 +50,11 @@ NS_ASSUME_NONNULL_BEGIN
return YES;
}
- (OWSInteractionType)interactionType
{
return OWSInteractionType_UnreadIndicator;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

@ -10,4 +10,6 @@
+ (void)showShareUIForURL:(NSURL *)url;
+ (void)showShareUIForText:(NSString *)text;
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
// TODO:

View File

@ -9,7 +9,6 @@
NS_ASSUME_NONNULL_BEGIN
double const OWSExpirationTimerViewBlinkingSeconds = 2;
@interface OWSExpirationTimerView ()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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())
}
}

View File

@ -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]()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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