Remove old conversation screen

This commit is contained in:
Niels Andriesse 2021-02-19 15:46:52 +11:00
parent af05f876d4
commit 49c825eb43
73 changed files with 250 additions and 15699 deletions

View File

@ -12,13 +12,10 @@
340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; };
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87E204DAC8C007AEB0F /* PrivacySettingsTableViewController.m */; };
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC883204DAC8C007AEB0F /* OWSSoundSettingsViewController.m */; };
340FC8B0204DAC8D007AEB0F /* AddToBlockListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC886204DAC8C007AEB0F /* AddToBlockListViewController.m */; };
340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */; };
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */; };
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */; };
34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34129B8521EF8779005457A8 /* LinkPreviewView.swift */; };
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 341341EE2187467900192D59 /* ConversationViewModel.m */; };
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */; };
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3427C64220F500DF00EEC730 /* OWSMessageTimerView.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 */; };
@ -26,7 +23,6 @@
34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; };
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */; };
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; };
34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; };
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; };
@ -41,9 +37,7 @@
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850561FD86544007B8332 /* SAEFailedViewController.swift */; };
348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 348570A620F67574004FF32B /* OWSMessageHeaderView.m */; };
3488F9362191CC4000E524CC /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3488F9352191CC4000E524CC /* MediaView.swift */; };
3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496744C2076768700080B5F /* OWSMessageBubbleView.m */; };
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; };
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34969559219B605E00DCFE74 /* ImagePickerController.swift */; };
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */; };
@ -59,10 +53,8 @@
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; };
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumView.swift */; };
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */; };
34AC0A23211C829F00997B47 /* OWSLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 34AC0A21211C829E00997B47 /* OWSLabel.m */; };
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B0796B1FCF46B000E248C2 /* MainAppContext.m */; };
34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */; };
34B6A907218B5241007C4606 /* TypingIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A906218B5240007C4606 /* TypingIndicatorCell.swift */; };
34B6A90B218BA1D1007C4606 /* typing-animation.gif in Resources */ = {isa = PBXBuildFile; fileRef = 34B6A90A218BA1D0007C4606 /* typing-animation.gif */; };
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */; };
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */; };
@ -73,28 +65,12 @@
34CF078A203E6B78005C4D61 /* end_call_tone_cept.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0786203E6B78005C4D61 /* end_call_tone_cept.caf */; };
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */; };
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */; };
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 */; };
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */; };
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */; };
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */; };
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */; };
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; };
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; };
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; };
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
34D920E720E179C200D51158 /* OWSMessageFooterView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D920E620E179C200D51158 /* OWSMessageFooterView.m */; };
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */; };
34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */; };
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF001206BD5A500025978 /* OWSBubbleView.m */; };
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */; };
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; };
34EA69402194933900702471 /* MediaDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EA693F2194933900702471 /* MediaDownloadView.swift */; };
34EA69422194DE8000702471 /* MediaUploadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34EA69412194DE7F00702471 /* MediaUploadView.swift */; };
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
3681EBBAC430992520DBD9AC /* Pods_SessionShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 200605FD180CB8B89F566B41 /* Pods_SessionShareExtension.framework */; };
4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; };
@ -114,7 +90,6 @@
454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454A84032059C787008B8C75 /* MediaTileViewController.swift */; };
455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DB1F1FEA0000F86704 /* Metal.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DC1F1FEA0000F86704 /* MetalKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 457F671A20746193000EABCD /* QuotedReplyPreview.swift */; };
45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45847E861E4283C30080EAB3 /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
45A2F005204473A3002E978A /* NewMessage.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45A2F004204473A3002E978A /* NewMessage.aifc */; };
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; };
@ -151,8 +126,6 @@
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; };
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */; };
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */; };
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */; };
4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */; };
4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; };
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */; };
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C21D5D5223A9DC500EF8A77 /* UIAlerts+iOS9.m */; };
@ -166,8 +139,6 @@
4C9CA25D217E676900607C63 /* ZXingObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C9CA25C217E676900607C63 /* ZXingObjC.framework */; };
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F4B219CCC630038ABDE /* CaptionView.swift */; };
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; };
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; };
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; };
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC613352227A00400E21A3A /* ConversationSearch.swift */; };
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; };
@ -229,7 +200,6 @@
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408B239A068800A248E7 /* RegisterVC.swift */; };
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408D239DC00D00A248E7 /* DisplayNameVC.swift */; };
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B408F239DD75000A248E7 /* RestoreVC.swift */; };
B82B4094239DF15900A248E7 /* ConversationTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82B4093239DF15900A248E7 /* ConversationTitleView.swift */; };
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B835246D25C38ABF0089A44F /* ConversationVC.swift */; };
B835247925C38D880089A44F /* MessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B835247825C38D880089A44F /* MessageCell.swift */; };
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B835249A25C3AB650089A44F /* VisibleMessageCell.swift */; };
@ -290,7 +260,6 @@
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */; };
B8AE761425ABFBB9001A84D2 /* GeneralUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
B8B32021258B1A650020074B /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32020258B1A650020074B /* Contact.swift */; };
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32032258B235D0020074B /* Storage+Contacts.swift */; };
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
@ -312,6 +281,7 @@
B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
B8D64FC725BA78520029CFC0 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84EA225DF745A005A043E /* LinkPreviewState.swift */; };
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; };
@ -562,7 +532,6 @@
C35D0DAB25AE5BDE00B6BF49 /* SettingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DAA25AE5BDE00B6BF49 /* SettingRow.swift */; };
C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */; };
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35E8AAD2485E51D00ACB629 /* IP2Country.swift */; };
C3645350252449260045C478 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364534F252449260045C478 /* VoiceMessageView.swift */; };
C364535C252467900045C478 /* AudioUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364535B252467900045C478 /* AudioUtilities.swift */; };
C374EEE225DA26740073A857 /* LinkPreviewModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEE125DA26740073A857 /* LinkPreviewModal.swift */; };
C374EEEB25DA3CA70073A857 /* ConversationTitleViewV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C374EEEA25DA3CA70073A857 /* ConversationTitleViewV2.swift */; };
@ -991,23 +960,18 @@
340FC87E204DAC8C007AEB0F /* PrivacySettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivacySettingsTableViewController.m; sourceTree = "<group>"; };
340FC87F204DAC8C007AEB0F /* OWSBackupSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupSettingsViewController.h; sourceTree = "<group>"; };
340FC883204DAC8C007AEB0F /* OWSSoundSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSoundSettingsViewController.m; sourceTree = "<group>"; };
340FC886204DAC8C007AEB0F /* AddToBlockListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddToBlockListViewController.m; sourceTree = "<group>"; };
340FC888204DAC8C007AEB0F /* OWSQRCodeScanningViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQRCodeScanningViewController.h; sourceTree = "<group>"; };
340FC88A204DAC8C007AEB0F /* NotificationSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationSettingsViewController.h; sourceTree = "<group>"; };
340FC88B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationSettingsOptionsViewController.h; sourceTree = "<group>"; };
340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupSettingsViewController.m; sourceTree = "<group>"; };
340FC88F204DAC8C007AEB0F /* PrivacySettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivacySettingsTableViewController.h; sourceTree = "<group>"; };
340FC892204DAC8C007AEB0F /* AddToBlockListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddToBlockListViewController.h; sourceTree = "<group>"; };
340FC894204DAC8C007AEB0F /* OWSSoundSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSoundSettingsViewController.h; sourceTree = "<group>"; };
340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQRCodeScanningViewController.m; sourceTree = "<group>"; };
340FC899204DAC8D007AEB0F /* OWSConversationSettingsViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsViewDelegate.h; sourceTree = "<group>"; };
340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSConversationSettingsViewController.m; sourceTree = "<group>"; };
340FC8A0204DAC8D007AEB0F /* OWSConversationSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsViewController.h; sourceTree = "<group>"; };
34129B8521EF8779005457A8 /* LinkPreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; };
341341ED2187467900192D59 /* ConversationViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationViewModel.h; sourceTree = "<group>"; };
341341EE2187467900192D59 /* ConversationViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationViewModel.m; sourceTree = "<group>"; };
34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQuotedMessageView.m; sourceTree = "<group>"; };
34277A5D20751BDC006049F2 /* OWSQuotedMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQuotedMessageView.h; sourceTree = "<group>"; };
3427C64120F500DE00EEC730 /* OWSMessageTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTimerView.h; sourceTree = "<group>"; };
3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTimerView.m; sourceTree = "<group>"; };
3430FE171F7751D4000EC51B /* GiphyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyAPI.swift; sourceTree = "<group>"; };
@ -1017,8 +981,6 @@
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>"; };
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationScrollButton.m; sourceTree = "<group>"; };
343A65971FC4CFE7000477A1 /* ConversationScrollButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationScrollButton.h; sourceTree = "<group>"; };
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupRestoreViewController.swift; sourceTree = "<group>"; };
34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareAppExtensionContext.h; sourceTree = "<group>"; };
34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareAppExtensionContext.m; sourceTree = "<group>"; };
@ -1034,11 +996,7 @@
34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; };
346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = "<group>"; };
347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = "<group>"; };
348570A620F67574004FF32B /* OWSMessageHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageHeaderView.m; sourceTree = "<group>"; };
348570A720F67574004FF32B /* OWSMessageHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageHeaderView.h; sourceTree = "<group>"; };
3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
3496744B2076768600080B5F /* OWSMessageBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageBubbleView.h; sourceTree = "<group>"; };
3496744C2076768700080B5F /* OWSMessageBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageBubbleView.m; sourceTree = "<group>"; };
3496744E2076ACCE00080B5F /* LongTextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongTextViewController.swift; sourceTree = "<group>"; };
34969559219B605E00DCFE74 /* ImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = "<group>"; };
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerController.swift; sourceTree = "<group>"; };
@ -1059,64 +1017,30 @@
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = "<group>"; };
34A8B3502190A40E00218A25 /* MediaAlbumView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaAlbumView.swift; sourceTree = "<group>"; };
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationMessageMapping.swift; sourceTree = "<group>"; };
34AC0A21211C829E00997B47 /* OWSLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSLabel.m; sourceTree = "<group>"; };
34AC0A22211C829E00997B47 /* OWSLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSLabel.h; sourceTree = "<group>"; };
34B0796B1FCF46B000E248C2 /* MainAppContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainAppContext.m; sourceTree = "<group>"; };
34B0796C1FCF46B000E248C2 /* MainAppContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainAppContext.h; sourceTree = "<group>"; };
34B0796E1FD07B1E00E248C2 /* SignalShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SignalShareExtension.entitlements; sourceTree = "<group>"; };
34B6A902218B3F62007C4606 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = "<group>"; };
34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorInteraction.swift; sourceTree = "<group>"; };
34B6A906218B5240007C4606 /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = "<group>"; };
34B6A90A218BA1D0007C4606 /* typing-animation.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "typing-animation.gif"; sourceTree = "<group>"; };
34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerViewController.swift; sourceTree = "<group>"; };
34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerLayout.swift; sourceTree = "<group>"; };
34C3C78C20409F320000134C /* Opening.m4r */ = {isa = PBXFileReference; lastKnownFileType = file; path = Opening.m4r; sourceTree = "<group>"; };
34C3C78E2040A4F70000134C /* sonarping.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = sonarping.mp3; path = Session/Meta/AudioFiles/sonarping.mp3; sourceTree = SOURCE_ROOT; };
34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDetailViewController.swift; sourceTree = "<group>"; };
34CF0783203E6B77005C4D61 /* busy_tone_ansi.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = busy_tone_ansi.caf; path = Session/Meta/AudioFiles/busy_tone_ansi.caf; sourceTree = SOURCE_ROOT; };
34CF0784203E6B77005C4D61 /* ringback_tone_ansi.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = ringback_tone_ansi.caf; path = Session/Meta/AudioFiles/ringback_tone_ansi.caf; sourceTree = SOURCE_ROOT; };
34CF0786203E6B78005C4D61 /* end_call_tone_cept.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = end_call_tone_cept.caf; path = Session/Meta/AudioFiles/end_call_tone_cept.caf; sourceTree = SOURCE_ROOT; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = "<group>"; };
34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = "<group>"; };
34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; 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>"; };
34D920E520E179C100D51158 /* OWSMessageFooterView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageFooterView.h; sourceTree = "<group>"; };
34D920E620E179C200D51158 /* OWSMessageFooterView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageFooterView.m; sourceTree = "<group>"; };
34D99CE3217509C1000AFB39 /* AppEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTextView.m; sourceTree = "<group>"; };
34DBF000206BD5A400025978 /* OWSMessageTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTextView.h; sourceTree = "<group>"; };
34DBF001206BD5A500025978 /* OWSBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleView.m; sourceTree = "<group>"; };
34DBF002206BD5A500025978 /* OWSBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleView.h; sourceTree = "<group>"; };
34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleShapeView.h; sourceTree = "<group>"; };
34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleShapeView.m; sourceTree = "<group>"; };
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProgressView.swift; sourceTree = "<group>"; };
34EA693F2194933900702471 /* MediaDownloadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaDownloadView.swift; sourceTree = "<group>"; };
34EA69412194DE7F00702471 /* MediaUploadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUploadView.swift; sourceTree = "<group>"; };
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>"; };
36098A00B2C7DB91D85A4AE3 /* Pods-Session.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Session.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Session/Pods-Session.debug.xcconfig"; sourceTree = "<group>"; };
@ -1141,7 +1065,6 @@
454A84032059C787008B8C75 /* MediaTileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTileViewController.swift; sourceTree = "<group>"; };
455A16DB1F1FEA0000F86704 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
455A16DC1F1FEA0000F86704 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
457F671A20746193000EABCD /* QuotedReplyPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReplyPreview.swift; sourceTree = "<group>"; };
45847E861E4283C30080EAB3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
45A2F004204473A3002E978A /* NewMessage.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; name = NewMessage.aifc; path = Session/Meta/AudioFiles/NewMessage.aifc; sourceTree = SOURCE_ROOT; };
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
@ -1180,7 +1103,6 @@
45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPushTokensJob.swift; sourceTree = "<group>"; };
45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarqueeLabel.swift; sourceTree = "<group>"; };
45F32C1D205718B000A300D5 /* MediaPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaPageViewController.swift; path = "Session/Media Viewing & Editing/MediaPageViewController.swift"; sourceTree = SOURCE_ROOT; };
4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceNoteLock.swift; sourceTree = "<group>"; };
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = "<group>"; };
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = "<group>"; };
4C1D2337218B6BA000A0598F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -1196,11 +1118,9 @@
4C9CA25C217E676900607C63 /* ZXingObjC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZXingObjC.framework; path = ThirdParty/Carthage/Build/iOS/ZXingObjC.framework; sourceTree = "<group>"; };
4CA46F4B219CCC630038ABDE /* CaptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptionView.swift; sourceTree = "<group>"; };
4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = "<group>"; };
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = "<group>"; };
4CC613352227A00400E21A3A /* ConversationSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearch.swift; sourceTree = "<group>"; };
4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotificationsAdaptee.swift; sourceTree = "<group>"; };
4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = "<group>"; };
53D547348A367C8A14D37FC0 /* Pods_SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5F3070F3395081DD0EB4F933 /* Pods-SignalUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUtilitiesKit/Pods-SignalUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
62ED73E38E0EC8506A9131AD /* Pods_SessionNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1285,7 +1205,6 @@
B82B408B239A068800A248E7 /* RegisterVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterVC.swift; sourceTree = "<group>"; };
B82B408D239DC00D00A248E7 /* DisplayNameVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayNameVC.swift; sourceTree = "<group>"; };
B82B408F239DD75000A248E7 /* RestoreVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreVC.swift; sourceTree = "<group>"; };
B82B4093239DF15900A248E7 /* ConversationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTitleView.swift; sourceTree = "<group>"; };
B835246D25C38ABF0089A44F /* ConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationVC.swift; sourceTree = "<group>"; };
B835247825C38D880089A44F /* MessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCell.swift; sourceTree = "<group>"; };
B835249A25C3AB650089A44F /* VisibleMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleMessageCell.swift; sourceTree = "<group>"; };
@ -1325,7 +1244,6 @@
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = "<group>"; };
B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeneralUtilities.m; sourceTree = "<group>"; };
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = "<group>"; };
B8B32020258B1A650020074B /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
B8B32032258B235D0020074B /* Storage+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Contacts.swift"; sourceTree = "<group>"; };
B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = "<group>"; };
@ -1352,6 +1270,8 @@
B8CCF638239721E20091D419 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
B8CCF63E23975CFB0091D419 /* JoinOpenGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinOpenGroupVC.swift; sourceTree = "<group>"; };
B8CCF6422397711F0091D419 /* SettingsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsVC.swift; sourceTree = "<group>"; };
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConversationViewAction.h; sourceTree = "<group>"; };
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = "<group>"; };
B8D8F1372566120F0092EF10 /* Storage+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+ClosedGroups.swift"; sourceTree = "<group>"; };
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Jobs.swift"; sourceTree = "<group>"; };
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OpenGroups.swift"; sourceTree = "<group>"; };
@ -1610,7 +1530,6 @@
C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEdgeInsets.swift; sourceTree = "<group>"; };
C35E8AA22485C72300ACB629 /* SwiftCSV.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftCSV.framework; path = ThirdParty/Carthage/Build/iOS/SwiftCSV.framework; sourceTree = "<group>"; };
C35E8AAD2485E51D00ACB629 /* IP2Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP2Country.swift; sourceTree = "<group>"; };
C364534F252449260045C478 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
C364535B252467900045C478 /* AudioUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioUtilities.swift; sourceTree = "<group>"; };
C374EEE125DA26740073A857 /* LinkPreviewModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewModal.swift; sourceTree = "<group>"; };
C374EEEA25DA3CA70073A857 /* ConversationTitleViewV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTitleViewV2.swift; sourceTree = "<group>"; };
@ -2247,6 +2166,7 @@
C328250E25CA06020062D0A7 /* VoiceMessageViewV2.swift */,
B8569AE225CBB19A00DBA3DB /* DocumentView.swift */,
B849789525D4A2F500D0D0B3 /* LinkPreviewViewV2.swift */,
B8D84EA225DF745A005A043E /* LinkPreviewState.swift */,
);
path = "Content Views";
sourceTree = "<group>";
@ -2274,12 +2194,12 @@
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
B8D84E9325DF72AF005A043E /* ConversationViewAction.h */,
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
34D1F0711F8678AA0066283D /* ConversationViewLayout.h */,
34D1F0721F8678AA0066283D /* ConversationViewLayout.m */,
341341ED2187467900192D59 /* ConversationViewModel.h */,
341341EE2187467900192D59 /* ConversationViewModel.m */,
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
B887C38125C7C79700E11DAE /* Input View */,
B835247725C38D190089A44F /* Message Cells */,
C328252E25CA54F70062D0A7 /* Context Menu */,
@ -2944,31 +2864,6 @@
path = "Basic Chats";
sourceTree = "<group>";
};
C36096AE25AD1909008B62B2 /* Conversations */ = {
isa = PBXGroup;
children = (
C36096B825AD196A008B62B2 /* Views & Cells */,
340FC892204DAC8C007AEB0F /* AddToBlockListViewController.h */,
340FC886204DAC8C007AEB0F /* AddToBlockListViewController.m */,
34D1F0B21F86D31D0066283D /* ConversationCollectionView.h */,
34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */,
34D1F0671F8678AA0066283D /* ConversationInputTextView.h */,
34D1F0681F8678AA0066283D /* ConversationInputTextView.m */,
34D1F0691F8678AA0066283D /* ConversationInputToolbar.h */,
34D1F06A1F8678AA0066283D /* ConversationInputToolbar.m */,
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */,
343A65971FC4CFE7000477A1 /* ConversationScrollButton.h */,
343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */,
B82B4093239DF15900A248E7 /* ConversationTitleView.swift */,
34D1F06D1F8678AA0066283D /* ConversationViewController.h */,
34D1F06E1F8678AA0066283D /* ConversationViewController.m */,
4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */,
4CB5F26820F7D060004D1B42 /* MessageActions.swift */,
34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */,
);
path = Conversations;
sourceTree = "<group>";
};
C36096AF25AD1932008B62B2 /* Sheets & Modals */ = {
isa = PBXGroup;
children = (
@ -2980,47 +2875,6 @@
path = "Sheets & Modals";
sourceTree = "<group>";
};
C36096B825AD196A008B62B2 /* Views & Cells */ = {
isa = PBXGroup;
children = (
457F671A20746193000EABCD /* QuotedReplyPreview.swift */,
4C043929220A9EC800BAEA63 /* VoiceNoteLock.swift */,
34129B8521EF8779005457A8 /* LinkPreviewView.swift */,
34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */,
34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */,
34D1F0961F867BFC0066283D /* ConversationViewCell.h */,
34D1F0971F867BFC0066283D /* ConversationViewCell.m */,
34EA693F2194933900702471 /* MediaDownloadView.swift */,
34EA69412194DE7F00702471 /* MediaUploadView.swift */,
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
34DBF005206C3CB100025978 /* OWSBubbleShapeView.h */,
34DBF006206C3CB200025978 /* OWSBubbleShapeView.m */,
34DBF002206BD5A500025978 /* OWSBubbleView.h */,
34DBF001206BD5A500025978 /* OWSBubbleView.m */,
34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */,
34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */,
34AC0A22211C829E00997B47 /* OWSLabel.h */,
34AC0A21211C829E00997B47 /* OWSLabel.m */,
3496744B2076768600080B5F /* OWSMessageBubbleView.h */,
3496744C2076768700080B5F /* OWSMessageBubbleView.m */,
34D1F0A11F867BFC0066283D /* OWSMessageCell.h */,
34D1F0A21F867BFC0066283D /* OWSMessageCell.m */,
34D920E520E179C100D51158 /* OWSMessageFooterView.h */,
34D920E620E179C200D51158 /* OWSMessageFooterView.m */,
348570A720F67574004FF32B /* OWSMessageHeaderView.h */,
348570A620F67574004FF32B /* OWSMessageHeaderView.m */,
34DBF000206BD5A400025978 /* OWSMessageTextView.h */,
34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */,
34277A5D20751BDC006049F2 /* OWSQuotedMessageView.h */,
34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */,
34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */,
34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */,
34B6A906218B5240007C4606 /* TypingIndicatorCell.swift */,
C364534F252449260045C478 /* VoiceMessageView.swift */,
);
path = "Views & Cells";
sourceTree = "<group>";
};
C36096B925AD1ACF008B62B2 /* GIFs */ = {
isa = PBXGroup;
children = (
@ -3723,7 +3577,6 @@
C36096BC25AD1C3E008B62B2 /* Backups */,
C36096A525AD18D7008B62B2 /* Basic Chats */,
C360969C25AD18BA008B62B2 /* Closed Groups */,
C36096AE25AD1909008B62B2 /* Conversations */,
B835246C25C38AA20089A44F /* Conversations V2 */,
C32C5D49256DD522003C73A2 /* Database */,
C32B405424A961E1001117B5 /* Dependencies */,
@ -5001,10 +4854,7 @@
files = (
B8041AA725C90927003C2166 /* TypingIndicatorCellV2.swift in Sources */,
B8CCF63723961D6D0091D419 /* NewPrivateChatVC.swift in Sources */,
34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */,
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
34DBF007206C3CB200025978 /* OWSBubbleShapeView.m in Sources */,
4C04392A220A9EC800BAEA63 /* VoiceNoteLock.swift in Sources */,
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */,
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
@ -5020,18 +4870,13 @@
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */,
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */,
C328255225CA64470062D0A7 /* ContextMenuVC+ActionView.swift in Sources */,
348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */,
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */,
34B6A907218B5241007C4606 /* TypingIndicatorCell.swift in Sources */,
C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */,
B82B408A2399EC0600A248E7 /* FakeChatView.swift in Sources */,
343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */,
B82B40882399EB0E00A248E7 /* LandingVC.swift in Sources */,
34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */,
EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */,
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */,
34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */,
B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */,
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */,
451166C01FD86B98000739BA /* AccountManager.swift in Sources */,
@ -5046,9 +4891,7 @@
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */,
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */,
34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */,
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
B85A68B12587141A008CC492 /* Storage+Resetting.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */,
@ -5075,9 +4918,9 @@
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */,
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */,
34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */,
B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */,
B8214A2B25D63EB9009C0F2A /* MessagesTableView.swift in Sources */,
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
@ -5089,8 +4932,6 @@
C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */,
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */,
34D920E720E179C200D51158 /* OWSMessageFooterView.m in Sources */,
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */,
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */,
@ -5098,20 +4939,15 @@
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */,
34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */,
B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */,
34EA69402194933900702471 /* MediaDownloadView.swift in Sources */,
B8544E3323D50E4900299F14 /* SNAppearance.swift in Sources */,
4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */,
3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */,
C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */,
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */,
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */,
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */,
B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */,
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */,
34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */,
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */,
@ -5123,7 +4959,6 @@
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
B82B4094239DF15900A248E7 /* ConversationTitleView.swift in Sources */,
34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */,
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */,
@ -5131,30 +4966,21 @@
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,
C328250F25CA06020062D0A7 /* VoiceMessageViewV2.swift in Sources */,
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */,
3488F9362191CC4000E524CC /* MediaView.swift in Sources */,
45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */,
B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */,
B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */,
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */,
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */,
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */,
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */,
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */,
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */,
34AC0A23211C829F00997B47 /* OWSLabel.m in Sources */,
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */,
34EA69422194DE8000702471 /* MediaUploadView.swift in Sources */,
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */,
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */,
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */,
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */,
C33100082558FF6D00070591 /* NewConversationButtonSet.swift in Sources */,
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
@ -5170,23 +4996,19 @@
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */,
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */,
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */,
340FC8B0204DAC8D007AEB0F /* AddToBlockListViewController.m in Sources */,
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */,
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */,
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */,
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */,
34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */,
C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */,
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */,
B90418E6183E9DD40038554A /* DateUtil.m in Sources */,
C33100092558FF6D00070591 /* UserCell.swift in Sources */,
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */,
C3645350252449260045C478 /* VoiceMessageView.swift in Sources */,
C374EEE225DA26740073A857 /* LinkPreviewModal.swift in Sources */,
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */,
);

View File

@ -247,7 +247,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega
private func commitChanges() {
let popToConversationVC: (EditClosedGroupVC) -> Void = { editVC in
if let conversationVC = editVC.navigationController!.viewControllers.first(where: { $0 is ConversationViewController }) {
if let conversationVC = editVC.navigationController!.viewControllers.first(where: { $0 is ConversationVC }) {
editVC.navigationController!.popToViewController(conversationVC, animated: true)
} else {
editVC.navigationController!.popViewController(animated: true)

View File

@ -35,7 +35,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
var audioSession: OWSAudioSession { Environment.shared.audioSession }
var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection }
var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems }
func conversationStyle() -> ConversationStyle { return ConversationStyle(thread: thread) }
override var inputAccessoryView: UIView? { isShowingSearchUI ? searchController.resultsBar : snInputView }
override var canBecomeFirstResponder: Bool { true }
@ -175,7 +174,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
let viewItem = viewItems[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: MessageCell.getCellType(for: viewItem).identifier) as! MessageCell
cell.delegate = self
cell.conversationStyle = conversationStyle()
cell.viewItem = viewItem
return cell
}

View File

@ -0,0 +1,8 @@
@import Foundation;
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
ConversationViewActionNone,
ConversationViewActionCompose,
ConversationViewActionAudioCall,
ConversationViewActionVideoCall,
};

View File

@ -2,7 +2,6 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewLayout.h"
#import <SessionMessagingKit/OWSAudioPlayer.h>
NS_ASSUME_NONNULL_BEGIN
@ -54,14 +53,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
#pragma mark -
// This is a ViewModel for cells in the conversation view.
//
// The lifetime of this class is the lifetime of that cell
// in the load window of the conversation view.
//
// Critically, this class implements ConversationViewLayoutItem
// and does caching of the cell's size.
@protocol ConversationViewItem <NSObject, ConversationViewLayoutItem, OWSAudioPlayerDelegate>
@protocol ConversationViewItem <NSObject, OWSAudioPlayerDelegate>
@property (nonatomic, readonly) TSInteraction *interaction;
@ -91,9 +83,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
@property (nonatomic, nullable) OWSUnreadIndicator *unreadIndicator;
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
indexPath:(NSIndexPath *)indexPath;
- (void)replaceInteraction:(TSInteraction *)interaction transaction:(YapDatabaseReadTransaction *)transaction;
- (void)clearCachedLayoutState;
@ -161,13 +150,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
#pragma mark -
@interface ConversationInteractionViewItem
: NSObject <ConversationViewItem, ConversationViewLayoutItem, OWSAudioPlayerDelegate>
: NSObject <ConversationViewItem, OWSAudioPlayerDelegate>
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithInteraction:(TSInteraction *)interaction
isGroupThread:(BOOL)isGroupThread
transaction:(YapDatabaseReadTransaction *)transaction
conversationStyle:(ConversationStyle *)conversationStyle;
transaction:(YapDatabaseReadTransaction *)transaction;
@end

View File

@ -4,16 +4,11 @@
#import <CoreServices/CoreServices.h>
#import "ConversationViewItem.h"
#import "OWSMessageCell.h"
#import "OWSMessageHeaderView.h"
#import "OWSSystemMessageCell.h"
#import "Session-Swift.h"
#import "AnyPromise.h"
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
#import <SessionUtilitiesKit/NSData+Image.h>
#import <SessionUtilitiesKit/NSString+SSK.h>
#import <SessionMessagingKit/TSInteraction.h>
#import <SessionMessagingKit/SSKEnvironment.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
@ -104,7 +99,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
@property (nonatomic, nullable) NSString *systemMessageText;
@property (nonatomic, nullable) TSThread *incomingMessageAuthorThread;
@property (nonatomic, nullable) NSString *authorConversationColorName;
@property (nonatomic, nullable) ConversationStyle *conversationStyle;
@end
@ -129,11 +123,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
- (instancetype)initWithInteraction:(TSInteraction *)interaction
isGroupThread:(BOOL)isGroupThread
transaction:(YapDatabaseReadTransaction *)transaction
conversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(interaction);
OWSAssertDebug(transaction);
OWSAssertDebug(conversationStyle);
self = [super init];
@ -143,7 +135,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
_interaction = interaction;
_isGroupThread = isGroupThread;
_conversationStyle = conversationStyle;
[self ensureViewState:transaction];
@ -311,114 +302,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return self.cachedCellSize != nil;
}
- (CGSize)cellSize
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.conversationStyle);
if (!self.cachedCellSize) {
ConversationViewCell *_Nullable measurementCell = [self measurementCell];
measurementCell.viewItem = self;
measurementCell.conversationStyle = self.conversationStyle;
CGSize cellSize = [measurementCell cellSize];
self.cachedCellSize = [NSValue valueWithCGSize:cellSize];
[measurementCell prepareForReuse];
}
return [self.cachedCellSize CGSizeValue];
}
- (nullable ConversationViewCell *)measurementCell
{
OWSAssertIsOnMainThread();
OWSAssertDebug(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:
OWSFailDebug(@"Unknown interaction type.");
return nil;
case OWSInteractionType_IncomingMessage:
case OWSInteractionType_OutgoingMessage:
measurementCell = [OWSMessageCell new];
break;
case OWSInteractionType_Error:
case OWSInteractionType_Info:
case OWSInteractionType_Call:
measurementCell = [OWSSystemMessageCell new];
break;
case OWSInteractionType_TypingIndicator:
measurementCell = [OWSTypingIndicatorCell new];
break;
}
OWSAssertDebug(measurementCell);
measurementCellCache[cellCacheKey] = measurementCell;
}
return measurementCell;
}
- (CGFloat)vSpacingWithPreviousLayoutItem:(id<ConversationViewItem>)previousLayoutItem
{
OWSAssertDebug(previousLayoutItem);
if (self.hasCellHeader) {
return OWSMessageHeaderViewDateHeaderVMargin;
}
// "Bubble Collapse". Adjacent messages with the same author should be close together.
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage
&& previousLayoutItem.interaction.interactionType == OWSInteractionType_IncomingMessage) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction;
TSIncomingMessage *previousIncomingMessage = (TSIncomingMessage *)previousLayoutItem.interaction;
if ([incomingMessage.authorId isEqualToString:previousIncomingMessage.authorId]) {
return 2.f;
}
} else if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage
&& previousLayoutItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
return 2.f;
}
return 12.f;
}
- (ConversationViewCell *)dequeueCellForCollectionView:(UICollectionView *)collectionView
indexPath:(NSIndexPath *)indexPath
{
OWSAssertIsOnMainThread();
OWSAssertDebug(collectionView);
OWSAssertDebug(indexPath);
OWSAssertDebug(self.interaction);
switch (self.interaction.interactionType) {
case OWSInteractionType_Unknown:
OWSFailDebug(@"Unknown interaction type.");
return nil;
case OWSInteractionType_IncomingMessage:
case OWSInteractionType_OutgoingMessage:
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSMessageCell cellReuseIdentifier]
forIndexPath:indexPath];
case OWSInteractionType_Error:
case OWSInteractionType_Info:
case OWSInteractionType_Call:
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
forIndexPath:indexPath];
case OWSInteractionType_TypingIndicator:
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSTypingIndicatorCell cellReuseIdentifier]
forIndexPath:indexPath];
}
}
- (nullable TSAttachmentStream *)firstValidAlbumAttachment
{
OWSAssertDebug(self.mediaAlbumItems.count > 0);

View File

@ -1,44 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@protocol ConversationViewLayoutItem <NSObject>
- (CGSize)cellSize;
- (CGFloat)vSpacingWithPreviousLayoutItem:(id<ConversationViewLayoutItem>)previousLayoutItem;
@end
#pragma mark -
@protocol ConversationViewLayoutDelegate <NSObject>
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems;
- (CGFloat)layoutHeaderHeight;
@end
#pragma mark -
// A new lean and efficient layout for conversation view designed to
// handle our edge cases (e.g. full-width unread indicators, etc.).
@interface ConversationViewLayout : UICollectionViewLayout
@property (nonatomic, weak) id<ConversationViewLayoutDelegate> delegate;
@property (nonatomic, readonly) BOOL hasLayout;
@property (nonatomic, readonly) BOOL hasEverHadLayout;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,168 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewLayout.h"
#import "Session-Swift.h"
#import "UIView+OWS.h"
NS_ASSUME_NONNULL_BEGIN
@interface ConversationViewLayout ()
@property (nonatomic) CGFloat lastViewWidth;
@property (nonatomic) CGSize contentSize;
@property (nonatomic, readonly) NSMutableDictionary<NSNumber *, UICollectionViewLayoutAttributes *> *itemAttributesMap;
// This dirty flag 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;
@property (nonatomic) BOOL hasEverHadLayout;
@end
#pragma mark -
@implementation ConversationViewLayout
- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle
{
if (self = [super init]) {
_itemAttributesMap = [NSMutableDictionary new];
_conversationStyle = conversationStyle;
}
return self;
}
- (void)setHasLayout:(BOOL)hasLayout
{
_hasLayout = hasLayout;
if (hasLayout) {
self.hasEverHadLayout = YES;
}
}
- (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;
self.lastViewWidth = 0.f;
}
- (void)prepareLayout
{
[super prepareLayout];
id<ConversationViewLayoutDelegate> delegate = self.delegate;
if (!delegate) {
OWSFailDebug(@"Missing delegate");
[self clearState];
return;
}
if (self.collectionView.bounds.size.width <= 0.f || self.collectionView.bounds.size.height <= 0.f) {
OWSFailDebug(@"Collection view has invalid size: %@", NSStringFromCGRect(self.collectionView.bounds));
[self clearState];
return;
}
if (self.hasLayout) {
return;
}
self.hasLayout = YES;
[self prepareLayoutOfItems];
}
- (void)prepareLayoutOfItems
{
const CGFloat viewWidth = self.conversationStyle.viewWidth;
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;
CGFloat y = self.conversationStyle.contentMarginTop + self.delegate.layoutHeaderHeight;
CGFloat contentBottom = y;
NSInteger row = 0;
id<ConversationViewLayoutItem> _Nullable previousLayoutItem = nil;
for (id<ConversationViewLayoutItem> layoutItem in layoutItems) {
if (previousLayoutItem) {
y += [layoutItem vSpacingWithPreviousLayoutItem:previousLayoutItem];
}
CGSize layoutSize = CGSizeCeil([layoutItem cellSize]);
// Ensure cell fits within view.
OWSAssertDebug(layoutSize.width <= viewWidth);
layoutSize.width = MIN(viewWidth, layoutSize.width);
// All cells are "full width" and are responsible for aligning their own content.
CGRect itemFrame = CGRectMake(0, y, viewWidth, layoutSize.height);
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;
row++;
previousLayoutItem = layoutItem;
}
contentBottom += self.conversationStyle.contentMarginBottom;
self.contentSize = CGSizeMake(viewWidth, contentBottom);
self.lastViewWidth = viewWidth;
}
- (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.lastViewWidth != newBounds.size.width;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -87,8 +87,6 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
// to prod the view to reset its scroll state, etc.
- (void)conversationViewModelDidReset;
- (ConversationStyle *)conversationStyle;
@end
#pragma mark -

View File

@ -5,7 +5,6 @@
#import "ConversationViewModel.h"
#import "ConversationViewItem.h"
#import "DateUtil.h"
#import "OWSMessageBubbleView.h"
#import "OWSQuotedReplyModel.h"
#import "Session-Swift.h"
#import <SignalCoreKit/NSDate+OWS.h>
@ -1042,7 +1041,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
NSArray<NSString *> *loadedUniqueIds = [self.messageMapping loadedUniqueIds];
BOOL isGroupThread = self.thread.isGroupThread;
ConversationStyle *conversationStyle = self.delegate.conversationStyle;
[self ensureConversationProfileState];
@ -1055,8 +1053,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
if (!viewItem) {
viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction
isGroupThread:isGroupThread
transaction:transaction
conversationStyle:conversationStyle];
transaction:transaction];
}
OWSAssertDebug(!viewItemCache[interaction.uniqueId]);
viewItemCache[interaction.uniqueId] = viewItem;

View File

@ -0,0 +1,220 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
extension CGPoint {
public func offsetBy(dx: CGFloat) -> CGPoint {
return CGPoint(x: x + dx, y: y)
}
public func offsetBy(dy: CGFloat) -> CGPoint {
return CGPoint(x: x, y: y + dy)
}
}
// MARK: -
@objc
public enum LinkPreviewImageState: Int {
case none
case loading
case loaded
case invalid
}
// MARK: -
@objc
public protocol LinkPreviewState {
func isLoaded() -> Bool
func urlString() -> String?
func displayDomain() -> String?
func title() -> String?
func imageState() -> LinkPreviewImageState
func image() -> UIImage?
}
// MARK: -
@objc
public class LinkPreviewLoading: NSObject, LinkPreviewState {
override init() {
}
public func isLoaded() -> Bool {
return false
}
public func urlString() -> String? {
return nil
}
public func displayDomain() -> String? {
return nil
}
public func title() -> String? {
return nil
}
public func imageState() -> LinkPreviewImageState {
return .none
}
public func image() -> UIImage? {
return nil
}
}
// MARK: -
@objc
public class LinkPreviewDraft: NSObject, LinkPreviewState {
private let linkPreviewDraft: OWSLinkPreviewDraft
@objc
public required init(linkPreviewDraft: OWSLinkPreviewDraft) {
self.linkPreviewDraft = linkPreviewDraft
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
return linkPreviewDraft.urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreviewDraft.displayDomain() else {
owsFailDebug("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
guard let value = linkPreviewDraft.title,
value.count > 0 else {
return nil
}
return value
}
public func imageState() -> LinkPreviewImageState {
if linkPreviewDraft.jpegImageData != nil {
return .loaded
} else {
return .none
}
}
public func image() -> UIImage? {
guard let jpegImageData = linkPreviewDraft.jpegImageData else {
return nil
}
guard let image = UIImage(data: jpegImageData) else {
owsFailDebug("Could not load image: \(jpegImageData.count)")
return nil
}
return image
}
}
// MARK: -
@objc
public class LinkPreviewSent: NSObject, LinkPreviewState {
private let linkPreview: OWSLinkPreview
private let imageAttachment: TSAttachment?
@objc
public var imageSize: CGSize {
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return CGSize.zero
}
return attachmentStream.imageSize()
}
@objc
public required init(linkPreview: OWSLinkPreview,
imageAttachment: TSAttachment?) {
self.linkPreview = linkPreview
self.imageAttachment = imageAttachment
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url")
return nil
}
return urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreview.displayDomain() else {
Logger.error("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
guard let value = linkPreview.title,
value.count > 0 else {
return nil
}
return value
}
public func imageState() -> LinkPreviewImageState {
guard linkPreview.imageAttachmentId != nil else {
return .none
}
guard let imageAttachment = imageAttachment else {
owsFailDebug("Missing imageAttachment.")
return .none
}
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return .loading
}
guard attachmentStream.isImage,
attachmentStream.isValidImage else {
return .invalid
}
return .loaded
}
public func image() -> UIImage? {
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return nil
}
guard attachmentStream.isImage,
attachmentStream.isValidImage else {
return nil
}
guard let imageFilepath = attachmentStream.originalFilePath else {
owsFailDebug("Attachment is missing file path.")
return nil
}
guard let image = UIImage(contentsOfFile: imageFilepath) else {
owsFailDebug("Could not load image: \(imageFilepath)")
return nil
}
return image
}
}
// MARK: -
@objc
public protocol LinkPreviewViewDraftDelegate {
func linkPreviewCanCancel() -> Bool
func linkPreviewDidCancel()
}

View File

@ -2,7 +2,6 @@ import UIKit
class MessageCell : UITableViewCell {
var delegate: MessageCellDelegate?
var conversationStyle: ConversationStyle?
var viewItem: ConversationViewItem? { didSet { update() } }
// MARK: Settings

View File

@ -306,8 +306,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewV2Delegate {
let maxWidth = VisibleMessageCell.getMaxWidth(for: viewItem) - 2 * inset
if let linkPreview = viewItem.linkPreview {
let linkPreviewView = LinkPreviewViewV2(for: viewItem, maxWidth: maxWidth, delegate: self)
let conversationStyle = self.conversationStyle ?? ConversationStyle(thread: viewItem.interaction.thread)
linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment, conversationStyle:conversationStyle)
linkPreviewView.linkPreviewState = LinkPreviewSent(linkPreview: linkPreview, imageAttachment: viewItem.linkPreviewAttachment)
snContentView.addSubview(linkPreviewView)
linkPreviewView.pin(to: snContentView)
} else {

View File

@ -3,7 +3,6 @@
//
#import "OWSMessageTimerView.h"
#import "ConversationViewController.h"
#import "OWSMath.h"
#import "UIColor+OWS.h"
#import "UIView+OWS.h"

View File

@ -1,9 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "SelectRecipientViewController.h"
@interface AddToBlockListViewController : SelectRecipientViewController
@end

View File

@ -1,110 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "AddToBlockListViewController.h"
#import "BlockListUIUtils.h"
#import <SessionMessagingKit/SSKEnvironment.h>
#import <SessionMessagingKit/OWSBlockingManager.h>
#import <SignalUtilitiesKit/SignalAccount.h>
NS_ASSUME_NONNULL_BEGIN
@interface AddToBlockListViewController () <SelectRecipientViewControllerDelegate>
@end
#pragma mark -
@implementation AddToBlockListViewController
- (void)loadView
{
self.delegate = self;
[super loadView];
self.title = NSLocalizedString(@"SETTINGS_ADD_TO_BLOCK_LIST_TITLE", @"Title for the 'add to block list' view.");
}
- (NSString *)phoneNumberSectionTitle
{
return NSLocalizedString(@"SETTINGS_ADD_TO_BLOCK_LIST_BLOCK_PHONE_NUMBER_TITLE",
@"Title for the 'block phone number' section of the 'add to block list' view.");
}
- (NSString *)phoneNumberButtonText
{
return NSLocalizedString(@"BLOCK_LIST_VIEW_BLOCK_BUTTON", @"A label for the block button in the block list view");
}
- (NSString *)contactsSectionTitle
{
return NSLocalizedString(@"SETTINGS_ADD_TO_BLOCK_LIST_BLOCK_CONTACT_TITLE",
@"Title for the 'block contact' section of the 'add to block list' view.");
}
- (void)phoneNumberWasSelected:(NSString *)phoneNumber
{
OWSAssertDebug(phoneNumber.length > 0);
__weak AddToBlockListViewController *weakSelf = self;
[BlockListUIUtils showBlockPhoneNumberActionSheet:phoneNumber
fromViewController:self
blockingManager:SSKEnvironment.shared.blockingManager
completionBlock:^(BOOL isBlocked) {
if (isBlocked) {
[weakSelf.navigationController popViewControllerAnimated:YES];
}
}];
}
- (BOOL)canSignalAccountBeSelected:(SignalAccount *)signalAccount
{
OWSAssertDebug(signalAccount);
return ![SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId];
}
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount
{
OWSAssertDebug(signalAccount);
__weak AddToBlockListViewController *weakSelf = self;
if ([SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId]) {
OWSFailDebug(@"Cannot add already blocked user to block list.");
return;
}
[BlockListUIUtils showBlockSignalAccountActionSheet:signalAccount
fromViewController:self
blockingManager:SSKEnvironment.shared.blockingManager
completionBlock:^(BOOL isBlocked) {
if (isBlocked) {
[weakSelf.navigationController popViewControllerAnimated:YES];
}
}];
}
- (BOOL)shouldHideLocalNumber
{
return YES;
}
- (BOOL)shouldHideContacts
{
return NO;
}
- (BOOL)shouldValidatePhoneNumbers
{
return NO;
}
- (nullable NSString *)accessoryMessageForSignalAccount:(SignalAccount *)signalAccount
{
return nil;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,22 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@protocol ConversationCollectionViewDelegate <NSObject>
- (void)collectionViewWillChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize;
- (void)collectionViewDidChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize;
@end
#pragma mark -
@interface ConversationCollectionView : UICollectionView
@property (weak, nonatomic) id<ConversationCollectionViewDelegate> layoutDelegate;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,90 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationCollectionView.h"
NS_ASSUME_NONNULL_BEGIN
#pragma mark -
@implementation ConversationCollectionView
- (void)setFrame:(CGRect)frame
{
if (frame.size.width == 0 || frame.size.height == 0) {
// Ignore iOS Auto Layout's tendency to temporarily zero out the
// frame of this view during the layout process.
//
// The conversation view has an invariant that the collection view
// should always have a "reasonable" (correct width, non-zero height)
// size. This lets us manipulate scroll state at all times, especially
// before the view has been presented for the first time. This
// invariant also saves us from needing all sorts of ugly and incomplete
// hacks in the conversation view's code.
return;
}
CGSize oldSize = self.frame.size;
CGSize newSize = frame.size;
BOOL isChanging = !CGSizeEqualToSize(oldSize, newSize);
if (isChanging) {
[self.layoutDelegate collectionViewWillChangeSizeFrom:oldSize to:newSize];
}
[super setFrame:frame];
if (isChanging) {
[self.layoutDelegate collectionViewDidChangeSizeFrom:oldSize to:newSize];
}
}
- (void)setBounds:(CGRect)bounds
{
if (bounds.size.width == 0 || bounds.size.height == 0) {
// Ignore iOS Auto Layout's tendency to temporarily zero out the
// frame of this view during the layout process.
//
// The conversation view has an invariant that the collection view
// should always have a "reasonable" (correct width, non-zero height)
// size. This lets us manipulate scroll state at all times, especially
// before the view has been presented for the first time. This
// invariant also saves us from needing all sorts of ugly and incomplete
// hacks in the conversation view's code.
return;
}
CGSize oldSize = self.bounds.size;
CGSize newSize = bounds.size;
BOOL isChanging = !CGSizeEqualToSize(oldSize, newSize);
if (isChanging) {
[self.layoutDelegate collectionViewWillChangeSizeFrom:oldSize to:newSize];
}
[super setBounds:bounds];
if (isChanging) {
[self.layoutDelegate collectionViewDidChangeSizeFrom:oldSize to:newSize];
}
}
- (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

@ -1,43 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <SignalUtilitiesKit/OWSTextView.h>
NS_ASSUME_NONNULL_BEGIN
@class SignalAttachment;
@protocol ConversationInputTextViewDelegate <NSObject>
- (void)didPasteAttachment:(SignalAttachment *_Nullable)attachment;
- (void)inputTextViewSendMessagePressed;
- (void)textViewDidChange:(UITextView *)textView;
@end
#pragma mark -
@protocol ConversationTextViewToolbarDelegate <NSObject>
- (void)textViewDidChange:(UITextView *)textView;
- (void)textViewDidChangeSelection:(UITextView *)textView;
@end
#pragma mark -
@interface ConversationInputTextView : OWSTextView
@property (weak, nonatomic) id<ConversationInputTextViewDelegate> inputTextViewDelegate;
@property (weak, nonatomic) id<ConversationTextViewToolbarDelegate> textViewToolbarDelegate;
- (NSString *)trimmedText;
- (void)setPlaceholderText:(NSString *)placeholderText;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,248 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationInputTextView.h"
#import "Session-Swift.h"
#import <SessionUtilitiesKit/NSString+SSK.h>
#import <SignalCoreKit/NSString+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface ConversationInputTextView () <UITextViewDelegate>
@property (nonatomic) UILabel *placeholderView;
@property (nonatomic) NSArray<NSLayoutConstraint *> *placeholderConstraints;
@end
#pragma mark -
@implementation ConversationInputTextView
- (instancetype)init
{
self = [super init];
if (self) {
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
self.delegate = self;
self.backgroundColor = nil;
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
self.scrollEnabled = YES;
self.scrollsToTop = NO;
self.userInteractionEnabled = YES;
self.font = [UIFont systemFontOfSize:LKValues.mediumFontSize];
self.textColor = LKColors.text;
self.textAlignment = NSTextAlignmentNatural;
self.tintColor = LKColors.accent;
self.contentMode = UIViewContentModeRedraw;
self.dataDetectorTypes = UIDataDetectorTypeNone;
self.text = nil;
self.placeholderView = [UILabel new];
self.placeholderView.text = NSLocalizedString(@"Message", @"");
self.placeholderView.textColor = [LKColors.text colorWithAlphaComponent:LKValues.lowOpacity];
self.placeholderView.userInteractionEnabled = NO;
[self addSubview:self.placeholderView];
// We need to do these steps _after_ placeholderView is configured.
self.font = [UIFont systemFontOfSize:LKValues.mediumFontSize];
CGFloat hMarginLeading = 16.f;
CGFloat hMarginTrailing = 16.f;
self.textContainerInset = UIEdgeInsetsMake(11.f,
CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading,
11.f,
CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing);
self.textContainer.lineFragmentPadding = 0;
self.contentInset = UIEdgeInsetsZero;
[self ensurePlaceholderConstraints];
[self updatePlaceholderVisibility];
}
return self;
}
#pragma mark -
- (void)setFont:(UIFont *_Nullable)font
{
[super setFont:font];
self.placeholderView.font = font;
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)isAnimated
{
// When creating new lines, contentOffset is animated, but because because
// we are simultaneously resizing the text view, this can cause the
// text in the textview to be "too high" in the text view.
// Solution is to disable animation for setting content offset.
[super setContentOffset:contentOffset animated:NO];
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
[super setContentInset:contentInset];
[self ensurePlaceholderConstraints];
}
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
{
[super setTextContainerInset:textContainerInset];
[self ensurePlaceholderConstraints];
}
- (void)ensurePlaceholderConstraints
{
OWSAssertDebug(self.placeholderView);
if (self.placeholderConstraints) {
[NSLayoutConstraint deactivateConstraints:self.placeholderConstraints];
}
// We align the location of our placeholder with the text content of
// this view. The only safe way to do that is by measuring the
// beginning position.
UITextRange *beginningTextRange =
[self textRangeFromPosition:self.beginningOfDocument toPosition:self.beginningOfDocument];
CGRect beginningTextRect = [self firstRectForRange:beginningTextRange];
CGFloat topInset = beginningTextRect.origin.y;
CGFloat leftInset = beginningTextRect.origin.x;
// we use Left instead of Leading, since it's based on the prior CGRect offset
self.placeholderConstraints = @[
[self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:leftInset],
[self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeRight],
[self.placeholderView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:topInset],
];
}
- (void)updatePlaceholderVisibility
{
self.placeholderView.hidden = self.text.length > 0;
}
- (void)setText:(NSString *_Nullable)text
{
[super setText:text];
[self updatePlaceholderVisibility];
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (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];
}
- (NSString *)trimmedText
{
return [self.text ows_stripped];
}
- (void)setPlaceholderText:(NSString *)placeholderText
{
[self.placeholderView setText:placeholderText];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView
{
OWSAssertDebug(self.inputTextViewDelegate);
OWSAssertDebug(self.textViewToolbarDelegate);
[self updatePlaceholderVisibility];
[self.inputTextViewDelegate textViewDidChange:self];
[self.textViewToolbarDelegate textViewDidChange:self];
}
- (void)textViewDidChangeSelection:(UITextView *)textView
{
[self.textViewToolbarDelegate textViewDidChangeSelection:self];
}
#pragma mark - Key Commands
- (nullable NSArray<UIKeyCommand *> *)keyCommands
{
// We're permissive about what modifier key we accept for the "send message" hotkey.
// We accept command-return, option-return.
//
// We don't support control-return because it doesn't work.
//
// We don't support shift-return because it is often used for "newline" in other
// messaging apps.
return @[
[self keyCommandWithInput:@"\r"
modifierFlags:UIKeyModifierCommand
action:@selector(modifiedReturnPressed:)
discoverabilityTitle:@"Send Message"],
// "Alternate" is option.
[self keyCommandWithInput:@"\r"
modifierFlags:UIKeyModifierAlternate
action:@selector(modifiedReturnPressed:)
discoverabilityTitle:@"Send Message"],
];
}
- (UIKeyCommand *)keyCommandWithInput:(NSString *)input
modifierFlags:(UIKeyModifierFlags)modifierFlags
action:(SEL)action
discoverabilityTitle:(NSString *)discoverabilityTitle
{
return [UIKeyCommand keyCommandWithInput:input
modifierFlags:modifierFlags
action:action
discoverabilityTitle:discoverabilityTitle];
}
- (void)modifiedReturnPressed:(UIKeyCommand *)sender
{
OWSLogInfo(@"modifiedReturnPressed: %@", sender.input);
[self.inputTextViewDelegate inputTextViewSendMessagePressed];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,97 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class LKMention;
@class LKMentionCandidateSelectionView;
@class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel;
@class SignalAttachment;
@class TSThread;
@protocol ConversationInputToolbarDelegate <NSObject>
- (void)sendButtonPressed;
- (void)attachmentButtonPressed;
#pragma mark - Voice Memo
- (void)voiceMemoGestureDidStart;
- (void)voiceMemoGestureDidLock;
- (void)voiceMemoGestureDidComplete;
- (void)voiceMemoGestureDidCancel;
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
- (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView;
@end
#pragma mark -
@class ConversationInputTextView;
@protocol ConversationInputTextViewDelegate;
@interface ConversationInputToolbar : UIView
- (instancetype)initWithConversationStyle:(ConversationStyle *)conversationStyle NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
@property (nonatomic, weak) id<ConversationInputToolbarDelegate> inputToolbarDelegate;
- (void)beginEditingTextMessage;
- (void)endEditingTextMessage;
- (BOOL)isInputTextViewFirstResponder;
- (void)setInputTextViewDelegate:(id<ConversationInputTextViewDelegate>)value;
- (NSString *)messageText;
- (void)setMessageText:(NSString *_Nullable)value animated:(BOOL)isAnimated;
- (void)setPlaceholderText:(NSString *)placeholderText;
- (void)clearTextMessageAnimated:(BOOL)isAnimated;
- (void)toggleDefaultKeyboard;
- (void)setAttachmentButtonHidden:(BOOL)isHidden;
- (void)updateFontSizes;
- (void)updateLayoutWithSafeAreaInsets:(UIEdgeInsets)safeAreaInsets;
- (void)ensureTextViewHeight;
#pragma mark - Voice Memo
- (void)lockVoiceMemoUI;
- (void)showVoiceMemoUI;
- (void)hideVoiceMemoUI:(BOOL)animated;
- (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha;
- (void)cancelVoiceMemoIfNecessary;
#pragma mark -
@property (nonatomic, nullable) OWSQuotedReplyModel *quotedReply;
@property (nonatomic, nullable, readonly) OWSLinkPreviewDraft *linkPreviewDraft;
- (void)hideInputMethod;
#pragma mark - Mention Candidate Selection View
- (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread;
- (void)hideMentionCandidateSelectionView;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface ConversationScrollButton : UIButton
@property (nonatomic) BOOL hasUnreadMessages;
+ (CGFloat)buttonSize;
- (nullable instancetype)initWithIconText:(NSString *)iconText;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,99 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationScrollButton.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalUtilitiesKit/Theme.h>
#import "Session-Swift.h"
NS_ASSUME_NONNULL_BEGIN
@interface ConversationScrollButton ()
@property (nonatomic) NSString *iconText;
@property (nonatomic) UILabel *iconLabel;
@property (nonatomic) UIView *circleView;
@end
#pragma mark -
@implementation ConversationScrollButton
- (nullable instancetype)initWithIconText:(NSString *)iconText
{
self = [super initWithFrame:CGRectZero];
if (!self) {
return self;
}
self.iconText = iconText;
[self createContents];
return self;
}
+ (CGFloat)circleSize
{
return ScaleFromIPhone5To7Plus(35.f, 40.f);
}
+ (CGFloat)buttonSize
{
return self.circleSize + 2 * 15.f;
}
- (void)createContents
{
UILabel *iconLabel = [UILabel new];
self.iconLabel = iconLabel;
iconLabel.userInteractionEnabled = NO;
const CGFloat circleSize = self.class.circleSize;
UIView *circleView = [UIView new];
self.circleView = circleView;
circleView.userInteractionEnabled = NO;
circleView.layer.cornerRadius = circleSize * 0.5f;
circleView.layer.borderColor = [LKColors.text colorWithAlphaComponent:LKValues.lowOpacity].CGColor;
circleView.layer.borderWidth = LKValues.composeViewTextFieldBorderThickness;
[circleView autoSetDimension:ALDimensionWidth toSize:circleSize];
[circleView autoSetDimension:ALDimensionHeight toSize:circleSize];
[self addSubview:circleView];
[self addSubview:iconLabel];
[circleView autoCenterInSuperview];
[iconLabel autoCenterInSuperview];
[self updateColors];
}
- (void)setHasUnreadMessages:(BOOL)hasUnreadMessages
{
_hasUnreadMessages = hasUnreadMessages;
[self updateColors];
}
- (void)updateColors
{
UIColor *foregroundColor = LKColors.text;
UIColor *backgroundColor = LKColors.composeViewBackground;
const CGFloat circleSize = self.class.circleSize;
self.circleView.backgroundColor = backgroundColor;
self.iconLabel.attributedText =
[[NSAttributedString alloc] initWithString:self.iconText
attributes:@{
NSFontAttributeName : [UIFont ows_fontAwesomeFont:circleSize * 0.75f],
NSForegroundColorAttributeName : foregroundColor,
NSBaselineOffsetAttributeName : @(-0.5f),
}];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,210 +0,0 @@
@objc(LKConversationTitleView)
final class ConversationTitleView : UIView {
private let thread: TSThread
private var currentStatus: Status? { didSet { updateSubtitleForCurrentStatus() } }
private var handledMessageTimestamps: Set<NSNumber> = []
// MARK: Types
private enum Status : Int {
case calculatingPoW = 1
case routing = 2
case messageSending = 3
case messageSent = 4
case messageFailed = 5
}
// MARK: Components
private lazy var profilePictureView: ProfilePictureView = {
let result = ProfilePictureView()
let size: CGFloat = 40
result.set(.width, to: size)
result.set(.height, to: size)
result.size = size
return result
}()
private lazy var titleLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.lineBreakMode = .byTruncatingTail
return result
}()
private lazy var subtitleLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: 13)
result.lineBreakMode = .byTruncatingTail
return result
}()
// MARK: Lifecycle
@objc init(thread: TSThread) {
self.thread = thread
super.init(frame: CGRect.zero)
setUpViewHierarchy()
updateTitle()
updateProfilePicture()
updateSubtitleForCurrentStatus()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
notificationCenter.addObserver(self, selector: #selector(handleCalculatingMessagePoWNotification(_:)), name: .calculatingMessagePoW, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleEncryptingMessageNotification(_:)), name: .encryptingMessage, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingNotification(_:)), name: .messageSending, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingFailedNotification(_:)), name: .messageSendingFailed, object: nil)
}
override init(frame: CGRect) {
preconditionFailure("Use init(thread:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(thread:) instead.")
}
private func setUpViewHierarchy() {
let labelStackView = UIStackView(arrangedSubviews: [ titleLabel, subtitleLabel ])
labelStackView.axis = .vertical
labelStackView.alignment = .leading
let stackView = UIStackView(arrangedSubviews: [ profilePictureView, labelStackView ])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 12
addSubview(stackView)
stackView.pin(to: self)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Updating
private func updateTitle() {
let title: String
if thread.isGroupThread() {
if thread.name().isEmpty {
title = GroupDisplayNameUtilities.getDefaultDisplayName(for: thread as! TSGroupThread)
} else {
title = thread.name()
}
} else {
if thread.isNoteToSelf() {
title = NSLocalizedString("Note to Self", comment: "")
} else {
let hexEncodedPublicKey = thread.contactIdentifier()!
title = UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? hexEncodedPublicKey
}
}
titleLabel.text = title
}
private func updateProfilePicture() {
profilePictureView.update(for: thread)
}
@objc private func handleProfileChangedNotification(_ notification: Notification) {
guard let hexEncodedPublicKey = notification.userInfo?[kNSNotificationKey_ProfileRecipientId] as? String, let thread = self.thread as? TSContactThread,
hexEncodedPublicKey == thread.contactIdentifier() else { return }
updateTitle()
updateProfilePicture()
}
@objc private func handleCalculatingMessagePoWNotification(_ notification: Notification) {
guard let timestamp = notification.object as? NSNumber else { return }
setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
}
@objc private func handleEncryptingMessageNotification(_ notification: Notification) {
guard let timestamp = notification.object as? NSNumber else { return }
setStatusIfNeeded(to: .routing, forMessageWithTimestamp: timestamp)
}
@objc private func handleMessageSendingNotification(_ notification: Notification) {
guard let timestamp = notification.object as? NSNumber else { return }
setStatusIfNeeded(to: .messageSending, forMessageWithTimestamp: timestamp)
}
@objc private func handleMessageSentNotification(_ notification: Notification) {
guard let timestamp = notification.object as? NSNumber else { return }
setStatusIfNeeded(to: .messageSent, forMessageWithTimestamp: timestamp)
handledMessageTimestamps.insert(timestamp)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.clearStatusIfNeededForMessageWithTimestamp(timestamp)
}
}
@objc private func handleMessageSendingFailedNotification(_ notification: Notification) {
guard let timestamp = notification.object as? NSNumber else { return }
clearStatusIfNeededForMessageWithTimestamp(timestamp)
}
private func setStatusIfNeeded(to status: Status, forMessageWithTimestamp timestamp: NSNumber) {
guard !handledMessageTimestamps.contains(timestamp) else { return }
var uncheckedTargetInteraction: TSInteraction? = nil
thread.enumerateInteractions { interaction in
guard interaction.timestamp == timestamp.uint64Value else { return }
uncheckedTargetInteraction = interaction
}
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage,
status.rawValue > (currentStatus?.rawValue ?? 0) else { return }
currentStatus = status
}
private func clearStatusIfNeededForMessageWithTimestamp(_ timestamp: NSNumber) {
var uncheckedTargetInteraction: TSInteraction? = nil
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
guard let interactionsByThread = transaction.ext(TSMessageDatabaseViewExtensionName) as? YapDatabaseViewTransaction else { return }
interactionsByThread.enumerateKeysAndObjects(inGroup: self.thread.uniqueId!) { _, _, object, _, _ in
guard let interaction = object as? TSInteraction, interaction.timestamp == timestamp.uint64Value else { return }
uncheckedTargetInteraction = interaction
}
}
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage else { return }
self.currentStatus = nil
}
@objc func updateSubtitleForCurrentStatus() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.subtitleLabel.isHidden = false
let subtitle = NSMutableAttributedString()
if let muteEndDate = self.thread.mutedUntilDate, self.thread.isMuted {
subtitle.append(NSAttributedString(string: "\u{e067} ", attributes: [ .font : UIFont.ows_elegantIconsFont(10), .foregroundColor : Colors.unimportant ]))
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale.current
dateFormatter.timeStyle = .medium
dateFormatter.dateStyle = .medium
subtitle.append(NSAttributedString(string: "Muted until " + dateFormatter.string(from: muteEndDate)))
} else if let thread = self.thread as? TSGroupThread {
var userCount: Int?
if thread.groupModel.groupType == .closedGroup {
userCount = GroupUtilities.getClosedGroupMemberCount(thread)
} else if thread.groupModel.groupType == .openGroup {
if let openGroup = Storage.shared.getOpenGroup(for: self.thread.uniqueId!) {
userCount = Storage.shared.getUserCount(forOpenGroupWithID: openGroup.id)
}
}
if let userCount = userCount {
subtitle.append(NSAttributedString(string: "\(userCount) members"))
} else if let hexEncodedPublicKey = (self.thread as? TSContactThread)?.contactIdentifier(), ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
subtitle.append(NSAttributedString(string: hexEncodedPublicKey))
} else {
self.subtitleLabel.isHidden = true
}
}
else {
self.subtitleLabel.isHidden = true
}
self.subtitleLabel.attributedText = subtitle
self.titleLabel.font = .boldSystemFont(ofSize: self.subtitleLabel.isHidden ? Values.veryLargeFontSize : Values.mediumFontSize)
}
}
// MARK: Layout
public override var intrinsicContentSize: CGSize {
return UIView.layoutFittingExpandedSize
}
}

View File

@ -1,37 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <SignalUtilitiesKit/OWSViewController.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
ConversationViewActionNone,
ConversationViewActionCompose,
ConversationViewActionAudioCall,
ConversationViewActionVideoCall,
};
@class TSThread;
@interface ConversationViewController : OWSViewController
@property (nonatomic, readonly) TSThread *thread;
- (void)configureForThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId;
- (void)popKeyBoard;
- (void)scrollToFirstUnreadMessage:(BOOL)isAnimated;
#pragma mark 3D Touch Methods
- (void)peekSetup;
- (void)popped;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@ -1,467 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class MenuAction: NSObject {
let block: (MenuAction) -> Void
let image: UIImage
let title: String
let subtitle: String?
public init(image: UIImage, title: String, subtitle: String?, block: @escaping (MenuAction) -> Void) {
self.image = image
self.title = title
self.subtitle = subtitle
self.block = block
}
}
@objc
protocol MenuActionsViewControllerDelegate: class {
func menuActionsWillPresent(_ menuActionsViewController: MenuActionsViewController)
func menuActionsIsPresenting(_ menuActionsViewController: MenuActionsViewController)
func menuActionsDidPresent(_ menuActionsViewController: MenuActionsViewController)
func menuActionsIsDismissing(_ menuActionsViewController: MenuActionsViewController)
func menuActionsDidDismiss(_ menuActionsViewController: MenuActionsViewController)
}
@objc
class MenuActionsViewController: UIViewController, MenuActionSheetDelegate {
@objc
weak var delegate: MenuActionsViewControllerDelegate?
@objc
public let focusedInteraction: TSInteraction
private let focusedView: UIView
private let actionSheetView: MenuActionSheetView
deinit {
Logger.verbose("")
}
@objc
required init(focusedInteraction: TSInteraction, focusedView: UIView, actions: [MenuAction]) {
self.focusedView = focusedView
self.focusedInteraction = focusedInteraction
self.actionSheetView = MenuActionSheetView(actions: actions)
super.init(nibName: nil, bundle: nil)
actionSheetView.delegate = self
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
// MARK: View LifeCycle
var actionSheetViewVerticalConstraint: NSLayoutConstraint?
override func loadView() {
self.view = UIView()
view.addSubview(actionSheetView)
actionSheetView.autoPinWidthToSuperview()
actionSheetView.setContentHuggingVerticalHigh()
actionSheetView.setCompressionResistanceHigh()
self.actionSheetViewVerticalConstraint = actionSheetView.autoPinEdge(.top, to: .bottom, of: self.view)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapBackground))
self.view.addGestureRecognizer(tapGesture)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.animatePresentation()
}
override func viewDidDisappear(_ animated: Bool) {
Logger.debug("")
super.viewDidDisappear(animated)
// When the user has manually dismissed the menu, we do a nice animation
// but if the view otherwise disappears (e.g. due to resigning active),
// we still want to give the delegate the information it needs to restore it's UI.
delegate?.menuActionsDidDismiss(self)
}
// MARK: Orientation
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return DefaultUIInterfaceOrientationMask()
}
// MARK: Present / Dismiss animations
var snapshotView: UIView?
private func addSnapshotFocusedView() -> UIView? {
guard let snapshotView = self.focusedView.snapshotView(afterScreenUpdates: false) else {
owsFailDebug("snapshotView was unexpectedly nil")
return nil
}
view.addSubview(snapshotView)
guard let focusedViewSuperview = focusedView.superview else {
owsFailDebug("focusedViewSuperview was unexpectedly nil")
return nil
}
let convertedFrame = view.convert(focusedView.frame, from: focusedViewSuperview)
snapshotView.frame = convertedFrame
return snapshotView
}
private func animatePresentation() {
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
owsFailDebug("actionSheetViewVerticalConstraint was unexpectedly nil")
return
}
guard let focusedViewSuperview = focusedView.superview else {
owsFailDebug("focusedViewSuperview was unexpectedly nil")
return
}
// darken background
guard let snapshotView = addSnapshotFocusedView() else {
owsFailDebug("snapshotView was unexpectedly nil")
return
}
self.snapshotView = snapshotView
snapshotView.superview?.layoutIfNeeded()
let backgroundDuration: TimeInterval = 0.1
UIView.animate(withDuration: backgroundDuration) {
let alpha: CGFloat = isDarkMode ? 0.7 : 0.4
self.view.backgroundColor = UIColor.black.withAlphaComponent(alpha)
}
self.actionSheetView.superview?.layoutIfNeeded()
let oldFocusFrame = self.view.convert(focusedView.frame, from: focusedViewSuperview)
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(toSuperviewEdge: .bottom)
self.delegate?.menuActionsWillPresent(self)
UIView.animate(withDuration: 0.2,
delay: backgroundDuration,
options: .curveEaseOut,
animations: {
self.actionSheetView.superview?.layoutIfNeeded()
let newSheetFrame = self.actionSheetView.frame
var newFocusFrame = oldFocusFrame
// Position focused item just over the action sheet.
let overlap: CGFloat = (oldFocusFrame.maxY + self.vSpacing) - newSheetFrame.minY
newFocusFrame.origin.y = oldFocusFrame.origin.y - overlap
snapshotView.frame = newFocusFrame
self.delegate?.menuActionsIsPresenting(self)
},
completion: { (_) in
self.delegate?.menuActionsDidPresent(self)
})
}
@objc
public let vSpacing: CGFloat = 10
@objc
public var focusUI: UIView {
return actionSheetView
}
private func animateDismiss(action: MenuAction?) {
guard let actionSheetViewVerticalConstraint = self.actionSheetViewVerticalConstraint else {
owsFailDebug("actionSheetVerticalConstraint was unexpectedly nil")
delegate?.menuActionsDidDismiss(self)
return
}
guard let snapshotView = self.snapshotView else {
owsFailDebug("snapshotView was unexpectedly nil")
delegate?.menuActionsDidDismiss(self)
return
}
self.actionSheetView.superview?.layoutIfNeeded()
NSLayoutConstraint.deactivate([actionSheetViewVerticalConstraint])
let dismissDuration: TimeInterval = 0.2
self.actionSheetViewVerticalConstraint = self.actionSheetView.autoPinEdge(.top, to: .bottom, of: self.view)
UIView.animate(withDuration: dismissDuration,
delay: 0,
options: .curveEaseOut,
animations: {
self.view.backgroundColor = UIColor.clear
self.actionSheetView.superview?.layoutIfNeeded()
// this helps when focused view is above navbars, etc.
snapshotView.alpha = 0
self.delegate?.menuActionsIsDismissing(self)
},
completion: { _ in
self.view.isHidden = true
self.delegate?.menuActionsDidDismiss(self)
if let action = action {
action.block(action)
}
})
}
// MARK: Actions
@objc
func didTapBackground() {
animateDismiss(action: nil)
}
// MARK: MenuActionSheetDelegate
func actionSheet(_ actionSheet: MenuActionSheetView, didSelectAction action: MenuAction) {
animateDismiss(action: action)
}
}
protocol MenuActionSheetDelegate: class {
func actionSheet(_ actionSheet: MenuActionSheetView, didSelectAction action: MenuAction)
}
class MenuActionSheetView: UIView, MenuActionViewDelegate {
private let actionStackView: UIStackView
private var actions: [MenuAction]
private var actionViews: [MenuActionView]
private var hapticFeedback: SelectionHapticFeedback
private var hasEverHighlightedAction = false
weak var delegate: MenuActionSheetDelegate?
override var bounds: CGRect {
didSet {
updateMask()
}
}
convenience init(actions: [MenuAction]) {
self.init(frame: CGRect.zero)
actions.forEach { self.addAction($0) }
}
override init(frame: CGRect) {
actionStackView = UIStackView()
actionStackView.axis = .vertical
actionStackView.spacing = CGHairlineWidth()
actions = []
actionViews = []
hapticFeedback = SelectionHapticFeedback()
super.init(frame: frame)
backgroundColor = (isDarkMode
? UIColor.ows_gray90
: UIColor.ows_gray05)
addSubview(actionStackView)
actionStackView.autoPinEdgesToSuperviewEdges()
self.clipsToBounds = true
let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(didTouch(gesture:)))
touchGesture.minimumPressDuration = 0.0
touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
self.addGestureRecognizer(touchGesture)
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
@objc
public func didTouch(gesture: UIGestureRecognizer) {
switch gesture.state {
case .possible:
break
case .began:
let location = gesture.location(in: self)
highlightActionView(location: location, fromView: self)
case .changed:
let location = gesture.location(in: self)
highlightActionView(location: location, fromView: self)
case .ended:
Logger.debug("ended")
let location = gesture.location(in: self)
selectActionView(location: location, fromView: self)
case .cancelled:
Logger.debug("canceled")
unhighlightAllActionViews()
case .failed:
Logger.debug("failed")
unhighlightAllActionViews()
default: break
}
}
public func addAction(_ action: MenuAction) {
actions.append(action)
let actionView = MenuActionView(action: action)
actionView.delegate = self
actionViews.append(actionView)
self.actionStackView.addArrangedSubview(actionView)
}
// MARK: MenuActionViewDelegate
func actionView(_ actionView: MenuActionView, didSelectAction action: MenuAction) {
self.delegate?.actionSheet(self, didSelectAction: action)
}
// MARK:
private func updateMask() {
let cornerRadius: CGFloat = 16
let path: UIBezierPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
private func unhighlightAllActionViews() {
for actionView in actionViews {
actionView.isHighlighted = false
}
}
private func actionView(touchedBy touchPoint: CGPoint, fromView: UIView) -> MenuActionView? {
for actionView in actionViews {
let convertedPoint = actionView.convert(touchPoint, from: fromView)
if actionView.point(inside: convertedPoint, with: nil) {
return actionView
}
}
return nil
}
private func highlightActionView(location: CGPoint, fromView: UIView) {
guard let touchedView = actionView(touchedBy: location, fromView: fromView) else {
unhighlightAllActionViews()
return
}
if hasEverHighlightedAction, !touchedView.isHighlighted {
self.hapticFeedback.selectionChanged()
}
touchedView.isHighlighted = true
hasEverHighlightedAction = true
self.actionViews.filter { $0 != touchedView }.forEach { $0.isHighlighted = false }
}
private func selectActionView(location: CGPoint, fromView: UIView) {
guard let selectedView: MenuActionView = actionView(touchedBy: location, fromView: fromView) else {
unhighlightAllActionViews()
return
}
selectedView.isHighlighted = true
self.actionViews.filter { $0 != selectedView }.forEach { $0.isHighlighted = false }
delegate?.actionSheet(self, didSelectAction: selectedView.action)
}
}
protocol MenuActionViewDelegate: class {
func actionView(_ actionView: MenuActionView, didSelectAction action: MenuAction)
}
class MenuActionView: UIButton {
public weak var delegate: MenuActionViewDelegate?
public let action: MenuAction
required init(action: MenuAction) {
self.action = action
super.init(frame: CGRect.zero)
isUserInteractionEnabled = true
backgroundColor = defaultBackgroundColor
let textColor = isLightMode ? UIColor.black : UIColor.white
var image = action.image
image = image.withRenderingMode(.alwaysTemplate)
let imageView = UIImageView(image: image)
imageView.tintColor = textColor.withAlphaComponent(Values.mediumOpacity)
let imageWidth: CGFloat = 24
imageView.autoSetDimensions(to: CGSize(width: imageWidth, height: imageWidth))
imageView.isUserInteractionEnabled = false
let titleLabel = UILabel()
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
titleLabel.textColor = textColor
titleLabel.text = action.title
titleLabel.isUserInteractionEnabled = false
let subtitleLabel = UILabel()
subtitleLabel.font = .systemFont(ofSize: Values.smallFontSize)
subtitleLabel.textColor = textColor.withAlphaComponent(Values.mediumOpacity)
subtitleLabel.text = action.subtitle
subtitleLabel.isUserInteractionEnabled = false
let textColumn = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
textColumn.axis = .vertical
textColumn.alignment = .leading
textColumn.isUserInteractionEnabled = false
let contentRow = UIStackView(arrangedSubviews: [imageView, textColumn])
contentRow.axis = .horizontal
contentRow.alignment = .center
contentRow.spacing = 12
contentRow.isLayoutMarginsRelativeArrangement = true
contentRow.layoutMargins = UIEdgeInsets(top: 7, left: 16, bottom: 7, right: 16)
contentRow.isUserInteractionEnabled = false
self.addSubview(contentRow)
contentRow.autoPinEdgesToSuperviewMargins()
contentRow.autoSetDimension(.height, toSize: 56, relation: .greaterThanOrEqual)
self.isUserInteractionEnabled = false
}
private var defaultBackgroundColor: UIColor {
return isLightMode ? UIColor(hex: 0xFCFCFC) : UIColor(hex: 0x1B1B1B)
}
private var highlightedBackgroundColor: UIColor {
return isLightMode ? UIColor(hex: 0xDFDFDF) : UIColor(hex: 0x0C0C0C)
}
override var isHighlighted: Bool {
didSet {
self.backgroundColor = isHighlighted ? highlightedBackgroundColor : defaultBackgroundColor
}
}
@objc
func didPress(sender: Any) {
Logger.debug("")
self.delegate?.actionView(self, didSelectAction: action)
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
}

View File

@ -1,202 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
protocol MessageActionsDelegate: class {
func banUser(_ conversationViewItem: ConversationViewItem)
func messageActionsShowDetailsForItem(_ conversationViewItem: ConversationViewItem)
func messageActionsReplyToItem(_ conversationViewItem: ConversationViewItem)
func copyPublicKey(for conversationViewItem: ConversationViewItem)
}
struct MessageActionBuilder {
static func reply(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_reply"),
title: NSLocalizedString("MESSAGE_ACTION_REPLY", comment: "Action sheet button title"),
subtitle: nil,
block: { [weak delegate] _ in delegate?.messageActionsReplyToItem(conversationViewItem) }
)
}
static func copyText(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_copy"),
title: NSLocalizedString("MESSAGE_ACTION_COPY_TEXT", comment: "Action sheet button title"),
subtitle: nil,
block: { _ in conversationViewItem.copyTextAction() }
)
}
static func copyPublicKey(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "Key").scaled(to: CGSize(width: 24, height: 24)),
title: NSLocalizedString("Copy Session ID", comment: ""),
subtitle: nil,
block: { [weak delegate] _ in delegate?.copyPublicKey(for: conversationViewItem) }
)
}
static func showDetails(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_info"),
title: NSLocalizedString("MESSAGE_ACTION_DETAILS", comment: "Action sheet button title"),
subtitle: nil,
block: { [weak delegate] _ in delegate?.messageActionsShowDetailsForItem(conversationViewItem) }
)
}
static func deleteMessage(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_trash"),
title: NSLocalizedString("MESSAGE_ACTION_DELETE_MESSAGE", comment: "Action sheet button title"),
subtitle: nil,
block: { _ in conversationViewItem.deleteAction() }
)
}
static func banUser(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_block"),
title: "Ban User",
subtitle: nil,
block: { [weak delegate] _ in delegate?.banUser(conversationViewItem) }
)
}
static func copyMedia(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_copy"),
title: NSLocalizedString("MESSAGE_ACTION_COPY_MEDIA", comment: "Action sheet button title"),
subtitle: nil,
block: { _ in conversationViewItem.copyMediaAction() }
)
}
static func saveMedia(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
return MenuAction(image: #imageLiteral(resourceName: "ic_download"),
title: NSLocalizedString("MESSAGE_ACTION_SAVE_MEDIA", comment: "Action sheet button title"),
subtitle: nil,
block: { _ in conversationViewItem.saveMediaAction() }
)
}
}
@objc
class ConversationViewItemActions: NSObject {
@objc
class func textActions(conversationViewItem: ConversationViewItem, shouldAllowReply: Bool, delegate: MessageActionsDelegate) -> [MenuAction] {
var actions: [MenuAction] = []
let isGroup = conversationViewItem.isGroupThread;
if shouldAllowReply {
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(replyAction)
}
if conversationViewItem.hasBodyTextActionContent {
let copyTextAction = MessageActionBuilder.copyText(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyTextAction)
}
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyPublicKeyAction)
}
if !isGroup || conversationViewItem.userCanDeleteGroupMessage {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(deleteAction)
}
if isGroup && conversationViewItem.interaction is TSIncomingMessage && conversationViewItem.userHasModerationPermission {
let banAction = MessageActionBuilder.banUser(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(banAction)
}
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(showDetailsAction)
return actions
}
@objc
class func mediaActions(conversationViewItem: ConversationViewItem, shouldAllowReply: Bool, delegate: MessageActionsDelegate) -> [MenuAction] {
var actions: [MenuAction] = []
let isGroup = conversationViewItem.isGroupThread;
if shouldAllowReply {
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(replyAction)
}
if conversationViewItem.hasMediaActionContent {
if conversationViewItem.canCopyMedia() {
let copyMediaAction = MessageActionBuilder.copyMedia(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyMediaAction)
}
if conversationViewItem.canSaveMedia() {
let saveMediaAction = MessageActionBuilder.saveMedia(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(saveMediaAction)
}
}
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyPublicKeyAction)
}
if !isGroup || conversationViewItem.userCanDeleteGroupMessage {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(deleteAction)
}
if isGroup && conversationViewItem.interaction is TSIncomingMessage && conversationViewItem.userHasModerationPermission {
let banAction = MessageActionBuilder.banUser(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(banAction)
}
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(showDetailsAction)
return actions
}
@objc
class func quotedMessageActions(conversationViewItem: ConversationViewItem, shouldAllowReply: Bool, delegate: MessageActionsDelegate) -> [MenuAction] {
var actions: [MenuAction] = []
if shouldAllowReply {
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(replyAction)
}
let isGroup = conversationViewItem.isGroupThread;
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(copyPublicKeyAction)
}
if !isGroup || conversationViewItem.userCanDeleteGroupMessage {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(deleteAction)
}
if isGroup && conversationViewItem.interaction is TSIncomingMessage && conversationViewItem.userHasModerationPermission {
let banAction = MessageActionBuilder.banUser(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(banAction)
}
let showDetailsAction = MessageActionBuilder.showDetails(conversationViewItem: conversationViewItem, delegate: delegate)
actions.append(showDetailsAction)
return actions
}
@objc
class func infoMessageActions(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> [MenuAction] {
let deleteAction = MessageActionBuilder.deleteMessage(conversationViewItem: conversationViewItem, delegate: delegate)
return [deleteAction ]
}
}

View File

@ -1,728 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalUtilitiesKit
import SignalUtilitiesKit
@objc
enum MessageMetadataViewMode: UInt {
case focusOnMessage
case focusOnMetadata
}
@objc
protocol MessageDetailViewDelegate: AnyObject {
func detailViewMessageWasDeleted(_ messageDetailViewController: MessageDetailViewController)
}
@objc
class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDelegate, OWSMessageBubbleViewDelegate {
@objc
weak var delegate: MessageDetailViewDelegate?
// MARK: Properties
let uiDatabaseConnection: YapDatabaseConnection
var bubbleView: UIView?
let mode: MessageMetadataViewMode
let viewItem: ConversationViewItem
var message: TSMessage
var wasDeleted: Bool = false
var messageBubbleView: OWSMessageBubbleView?
var messageBubbleViewWidthLayoutConstraint: NSLayoutConstraint?
var messageBubbleViewHeightLayoutConstraint: NSLayoutConstraint?
var scrollView: UIScrollView!
var contentView: UIView?
var attachment: TSAttachment?
var dataSource: DataSource?
var attachmentStream: TSAttachmentStream?
var messageBody: String?
lazy var shouldShowUD: Bool = {
return self.preferences.shouldShowUnidentifiedDeliveryIndicators()
}()
var conversationStyle: ConversationStyle
// MARK: Dependencies
var preferences: OWSPreferences {
return Environment.shared.preferences
}
// MARK: Initializers
@available(*, unavailable, message:"use other constructor instead.")
required init?(coder aDecoder: NSCoder) {
notImplemented()
}
@objc
required init(viewItem: ConversationViewItem, message: TSMessage, thread: TSThread, mode: MessageMetadataViewMode) {
self.viewItem = viewItem
self.message = message
self.mode = mode
self.uiDatabaseConnection = OWSPrimaryStorage.shared().uiDatabaseConnection
self.conversationStyle = ConversationStyle(thread: thread)
super.init(nibName: nil, bundle: nil)
}
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
do {
try updateMessageToLatest()
} catch DetailViewError.messageWasDeleted {
self.delegate?.detailViewMessageWasDeleted(self)
} catch {
owsFailDebug("unexpected error")
}
self.conversationStyle.viewWidth = view.width()
ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_METADATA_VIEW_TITLE", comment: "Title for the 'message metadata' view."), hasCustomBackButton: false)
createViews()
self.view.layoutIfNeeded()
NotificationCenter.default.addObserver(self,
selector: #selector(uiDatabaseDidUpdate),
name: .OWSUIDatabaseConnectionDidUpdate,
object: OWSPrimaryStorage.shared().dbNotificationObject)
}
override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
Logger.debug("")
super.viewWillTransition(to: size, with: coordinator)
self.conversationStyle.viewWidth = size.width
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateMessageBubbleViewLayout()
if mode == .focusOnMetadata {
if let bubbleView = self.bubbleView {
// Force layout.
view.setNeedsLayout()
view.layoutIfNeeded()
let contentHeight = scrollView.contentSize.height
let scrollViewHeight = scrollView.frame.size.height
guard contentHeight >= scrollViewHeight else {
// All content is visible within the scroll view. No need to offset.
return
}
// We want to include at least a little portion of the message, but scroll no farther than necessary.
let showAtLeast: CGFloat = 50
let bubbleViewBottom = bubbleView.superview!.convert(bubbleView.frame, to: scrollView).maxY
let maxOffset = bubbleViewBottom - showAtLeast
let lastPage = contentHeight - scrollViewHeight
let offset = CGPoint(x: 0, y: min(maxOffset, lastPage))
scrollView.setContentOffset(offset, animated: false)
}
}
}
// MARK: - Create Views
private func createViews() {
view.backgroundColor = .clear
let scrollView = UIScrollView()
self.scrollView = scrollView
view.addSubview(scrollView)
scrollView.autoPinWidthToSuperview(withMargin: 0)
if scrollView.applyInsetsFix() {
scrollView.autoPinEdge(.top, to: .top, of: view)
} else {
scrollView.autoPinEdge(toSuperviewEdge: .top)
}
let contentView = UIView.container()
self.contentView = contentView
scrollView.addSubview(contentView)
contentView.autoPinLeadingToSuperviewMargin()
contentView.autoPinTrailingToSuperviewMargin()
contentView.autoPinEdge(toSuperviewEdge: .top)
contentView.autoPinEdge(toSuperviewEdge: .bottom)
scrollView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
if hasMediaAttachment {
let footer = UIToolbar()
view.addSubview(footer)
footer.autoPinWidthToSuperview(withMargin: 0)
footer.autoPinEdge(.top, to: .bottom, of: scrollView)
footer.autoPinEdge(.bottom, to: .bottom, of: view)
footer.items = [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareButtonPressed)),
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
]
} else {
scrollView.autoPinEdge(toSuperviewEdge: .bottom)
}
updateContent()
}
lazy var thread: TSThread = {
var thread: TSThread?
self.uiDatabaseConnection.read { transaction in
thread = self.message.thread(with: transaction)
}
return thread!
}()
private func updateContent() {
guard let contentView = contentView else {
owsFailDebug("Missing contentView")
return
}
// Remove any existing content views.
for subview in contentView.subviews {
subview.removeFromSuperview()
}
var rows = [UIView]()
// Content
rows += contentRows()
// Sender?
if let incomingMessage = message as? TSIncomingMessage {
let senderId = incomingMessage.authorId
let threadID = thread.uniqueId!
var senderName: String!
Storage.writeSync { transaction in
senderName = DisplayNameUtilities2.getDisplayName(for: senderId, inThreadWithID: threadID, using: transaction)
}
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENDER",
comment: "Label for the 'sender' field of the 'message metadata' view."),
value: senderName))
}
// Recipient(s)
if let outgoingMessage = message as? TSOutgoingMessage {
func getSeparator() -> UIView {
let result = UIView()
result.set(.height, to: Values.separatorThickness)
result.backgroundColor = Colors.separator
return result
}
if !outgoingMessage.recipientIds().isEmpty {
rows += [ getSeparator() ]
}
rows += outgoingMessage.recipientIds().flatMap { publicKey -> [UIView] in
// We use ContactCellView, not ContactTableViewCell.
// Table view cells don't layout properly outside the
// context of a table view.
let cellView = ContactCellView()
cellView.configure(withRecipientId: publicKey)
let wrapper = UIView()
wrapper.layoutMargins = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20)
wrapper.addSubview(cellView)
cellView.autoPinEdgesToSuperviewMargins()
return [ wrapper, getSeparator() ]
}
if !outgoingMessage.recipientIds().isEmpty {
rows += [ UIView.vSpacer(10) ]
}
}
let sentText = DateUtil.formatPastTimestampRelativeToNow(message.timestamp)
let sentRow: UIStackView = valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENT_DATE_TIME",
comment: "Label for the 'sent date & time' field of the 'message metadata' view."),
value: sentText)
if let incomingMessage = message as? TSIncomingMessage {
if self.shouldShowUD, incomingMessage.wasReceivedByUD {
let icon = #imageLiteral(resourceName: "ic_secret_sender_indicator").withRenderingMode(.alwaysTemplate)
let iconView = UIImageView(image: icon)
iconView.tintColor = Theme.secondaryColor
iconView.setContentHuggingHigh()
sentRow.addArrangedSubview(iconView)
// keep the icon close to the label.
let spacerView = UIView()
spacerView.setContentHuggingLow()
sentRow.addArrangedSubview(spacerView)
}
}
sentRow.isUserInteractionEnabled = true
sentRow.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongPressSent)))
rows.append(sentRow)
if message is TSIncomingMessage {
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_RECEIVED_DATE_TIME",
comment: "Label for the 'received date & time' field of the 'message metadata' view."),
value: DateUtil.formatPastTimestampRelativeToNow(message.receivedAtTimestamp)))
}
rows += addAttachmentMetadataRows()
// TODO: We could include the "disappearing messages" state here.
let rowStack = UIStackView(arrangedSubviews: rows)
rowStack.axis = .vertical
rowStack.spacing = 5
contentView.addSubview(rowStack)
rowStack.autoPinEdgesToSuperviewMargins()
contentView.layoutIfNeeded()
updateMessageBubbleViewLayout()
}
private func displayableTextIfText() -> String? {
guard viewItem.hasBodyText else {
return nil
}
guard let displayableText = viewItem.displayableBodyText else {
return nil
}
let messageBody = displayableText.fullText
guard messageBody.count > 0 else {
return nil
}
return messageBody
}
let bubbleViewHMargin: CGFloat = 10
private func contentRows() -> [UIView] {
var rows = [UIView]()
let messageBubbleView = OWSMessageBubbleView(frame: CGRect.zero)
messageBubbleView.delegate = self
messageBubbleView.addTapGestureHandler()
self.messageBubbleView = messageBubbleView
messageBubbleView.viewItem = viewItem
messageBubbleView.cellMediaCache = NSCache()
messageBubbleView.conversationStyle = conversationStyle
messageBubbleView.configureViews()
messageBubbleView.loadContent()
assert(messageBubbleView.isUserInteractionEnabled)
let row = UIView()
row.addSubview(messageBubbleView)
messageBubbleView.autoPinHeightToSuperview()
let isIncoming = self.message as? TSIncomingMessage != nil
messageBubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin)
self.messageBubbleViewWidthLayoutConstraint = messageBubbleView.autoSetDimension(.width, toSize: 0)
self.messageBubbleViewHeightLayoutConstraint = messageBubbleView.autoSetDimension(.height, toSize: 0)
rows.append(row)
if rows.isEmpty {
// Neither attachment nor body.
owsFailDebug("Message has neither attachment nor body.")
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_NO_ATTACHMENT_OR_BODY",
comment: "Label for messages without a body or attachment in the 'message metadata' view."),
value: ""))
}
let spacer = UIView()
spacer.autoSetDimension(.height, toSize: 15)
rows.append(spacer)
return rows
}
private func fetchAttachment(transaction: YapDatabaseReadTransaction) -> TSAttachment? {
// TODO: Support multi-image messages.
guard let attachmentId = message.attachmentIds.firstObject as? String else {
return nil
}
guard let attachment = TSAttachment.fetch(uniqueId: attachmentId, transaction: transaction) else {
Logger.warn("Missing attachment. Was it deleted?")
return nil
}
return attachment
}
var hasMediaAttachment: Bool {
guard let attachment = self.attachment else {
return false
}
guard attachment.contentType != OWSMimeTypeOversizeTextMessage else {
// to the user, oversized text attachments should behave
// just like regular text messages.
return false
}
return true
}
private func addAttachmentMetadataRows() -> [UIView] {
guard hasMediaAttachment else {
return []
}
var rows = [UIView]()
if let attachment = self.attachment {
// Only show MIME types in DEBUG builds.
if _isDebugAssertConfiguration() {
let contentType = attachment.contentType
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_MIME_TYPE",
comment: "Label for the MIME type of attachments in the 'message metadata' view."),
value: contentType))
}
if let sourceFilename = attachment.sourceFilename {
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SOURCE_FILENAME",
comment: "Label for the original filename of any attachment in the 'message metadata' view."),
value: sourceFilename))
}
}
if let dataSource = self.dataSource {
let fileSize = dataSource.dataLength()
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_FILE_SIZE",
comment: "Label for file size of attachments in the 'message metadata' view."),
value: OWSFormat.formatFileSize(UInt(fileSize))))
}
return rows
}
private func buildUDAccessoryView(text: String) -> UIView {
let label = UILabel()
label.textColor = Theme.secondaryColor
label.text = text
label.textAlignment = .right
label.font = UIFont.ows_mediumFont(withSize: 13)
let image = #imageLiteral(resourceName: "ic_secret_sender_indicator").withRenderingMode(.alwaysTemplate)
let imageView = UIImageView(image: image)
imageView.tintColor = Theme.middleGrayColor
let hStack = UIStackView(arrangedSubviews: [imageView, label])
hStack.axis = .horizontal
hStack.spacing = 8
return hStack
}
private func nameLabel(text: String) -> UILabel {
let label = UILabel()
label.textColor = Theme.primaryColor
label.font = UIFont.ows_mediumFont(withSize: 14)
label.text = text
label.setContentHuggingHorizontalHigh()
return label
}
private func valueLabel(text: String) -> UILabel {
let label = UILabel()
label.textColor = Theme.primaryColor
label.font = UIFont.ows_regularFont(withSize: 14)
label.text = text
label.setContentHuggingHorizontalLow()
return label
}
private func valueRow(name: String, value: String, subtitle: String = "") -> UIStackView {
let nameLabel = self.nameLabel(text: name)
let valueLabel = self.valueLabel(text: value)
let hStackView = UIStackView(arrangedSubviews: [nameLabel, valueLabel])
hStackView.axis = .horizontal
hStackView.spacing = 10
hStackView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
hStackView.isLayoutMarginsRelativeArrangement = true
if subtitle.count > 0 {
let subtitleLabel = self.valueLabel(text: subtitle)
subtitleLabel.textColor = Theme.secondaryColor
hStackView.addArrangedSubview(subtitleLabel)
}
return hStackView
}
// MARK: - Actions
@objc func shareButtonPressed() {
guard let attachmentStream = attachmentStream else {
Logger.error("Share button should only be shown with attachment, but no attachment found.")
return
}
AttachmentSharing.showShareUI(forAttachment: attachmentStream)
}
// MARK: - Actions
enum DetailViewError: Error {
case messageWasDeleted
}
// This method should be called after self.databaseConnection.beginLongLivedReadTransaction().
private func updateMessageToLatest() throws {
AssertIsOnMainThread()
try self.uiDatabaseConnection.read { transaction in
guard let uniqueId = self.message.uniqueId else {
Logger.error("Message is missing uniqueId.")
return
}
guard let newMessage = TSInteraction.fetch(uniqueId: uniqueId, transaction: transaction) as? TSMessage else {
Logger.error("Message was deleted")
throw DetailViewError.messageWasDeleted
}
self.message = newMessage
self.attachment = self.fetchAttachment(transaction: transaction)
self.attachmentStream = self.attachment as? TSAttachmentStream
}
}
@objc internal func uiDatabaseDidUpdate(notification: NSNotification) {
AssertIsOnMainThread()
guard !wasDeleted else {
// Item was deleted in the tile view gallery.
// Don't bother re-rendering, it will fail and we'll soon be dismissed.
return
}
guard let notifications = notification.userInfo?[OWSUIDatabaseConnectionNotificationsKey] as? [Notification] else {
owsFailDebug("notifications was unexpectedly nil")
return
}
guard let uniqueId = self.message.uniqueId else {
Logger.error("Message is missing uniqueId.")
return
}
guard self.uiDatabaseConnection.hasChange(forKey: uniqueId,
inCollection: TSInteraction.collection(),
in: notifications) else {
Logger.debug("No relevant changes.")
return
}
do {
try updateMessageToLatest()
} catch DetailViewError.messageWasDeleted {
DispatchQueue.main.async {
self.delegate?.detailViewMessageWasDeleted(self)
}
return
} catch {
owsFailDebug("unexpected error: \(error)")
}
updateContent()
}
private func string(for messageReceiptStatus: MessageReceiptStatus) -> String {
switch messageReceiptStatus {
case .uploading:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING",
comment: "Status label for messages which are uploading.")
case .sending:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SENDING",
comment: "Status label for messages which are sending.")
case .sent:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SENT",
comment: "Status label for messages which are sent.")
case .delivered:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_DELIVERED",
comment: "Status label for messages which are delivered.")
case .read:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_READ",
comment: "Status label for messages which are read.")
case .failed:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_FAILED",
comment: "Status label for messages which are failed.")
case .skipped:
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SKIPPED",
comment: "Status label for messages which were skipped.")
}
}
// MARK: - Message Bubble Layout
private func updateMessageBubbleViewLayout() {
guard let messageBubbleView = messageBubbleView else {
return
}
guard let messageBubbleViewWidthLayoutConstraint = messageBubbleViewWidthLayoutConstraint else {
return
}
guard let messageBubbleViewHeightLayoutConstraint = messageBubbleViewHeightLayoutConstraint else {
return
}
let messageBubbleSize = messageBubbleView.measureSize()
messageBubbleViewWidthLayoutConstraint.constant = messageBubbleSize.width
messageBubbleViewHeightLayoutConstraint.constant = messageBubbleSize.height
}
// MARK: OWSMessageBubbleViewDelegate
func didTapImageViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream, imageView: UIView) {
let mediaGallery = MediaGallery(thread: self.thread)
mediaGallery.addDataSourceDelegate(self)
mediaGallery.presentDetailView(fromViewController: self, mediaAttachment: attachmentStream, replacingView: imageView)
}
func didTapVideoViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream, imageView: UIView) {
let mediaGallery = MediaGallery(thread: self.thread)
mediaGallery.addDataSourceDelegate(self)
mediaGallery.presentDetailView(fromViewController: self, mediaAttachment: attachmentStream, replacingView: imageView)
}
var audioAttachmentPlayer: OWSAudioPlayer?
func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {
AssertIsOnMainThread()
guard let mediaURL = attachmentStream.originalMediaURL else {
owsFailDebug("mediaURL was unexpectedly nil for attachment: \(attachmentStream)")
return
}
guard FileManager.default.fileExists(atPath: mediaURL.path) else {
owsFailDebug("audio file missing at path: \(mediaURL)")
return
}
if let audioAttachmentPlayer = self.audioAttachmentPlayer {
// Is this player associated with this media adapter?
if audioAttachmentPlayer.owner === viewItem {
// Tap to pause & unpause.
audioAttachmentPlayer.togglePlayState()
return
}
audioAttachmentPlayer.stop()
self.audioAttachmentPlayer = nil
}
let audioAttachmentPlayer = OWSAudioPlayer(mediaUrl: mediaURL, audioBehavior: .audioMessagePlayback, delegate: viewItem)
self.audioAttachmentPlayer = audioAttachmentPlayer
// Associate the player with this media adapter.
audioAttachmentPlayer.owner = viewItem
audioAttachmentPlayer.play()
}
func didPanAudioViewItem(toCurrentTime currentTime: TimeInterval) {
// TODO: Implement
}
func didTapTruncatedTextMessage(_ conversationItem: ConversationViewItem) {
guard let navigationController = self.navigationController else {
owsFailDebug("navigationController was unexpectedly nil")
return
}
let viewController = LongTextViewController(viewItem: viewItem)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
func didTapFailedIncomingAttachment(_ viewItem: ConversationViewItem) {
// no - op
}
func didTapFailedOutgoingMessage(_ message: TSOutgoingMessage) {
// no - op
}
func didTapConversationItem(_ viewItem: ConversationViewItem, quotedReply: OWSQuotedReplyModel) {
// no - op
}
func didTapConversationItem(_ viewItem: ConversationViewItem, quotedReply: OWSQuotedReplyModel, failedThumbnailDownloadAttachmentPointer attachmentPointer: TSAttachmentPointer) {
// no - op
}
func didTapConversationItem(_ viewItem: ConversationViewItem, linkPreview: OWSLinkPreview) {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url.")
return
}
guard let url = URL(string: urlString) else {
owsFailDebug("Invalid url: \(urlString).")
return
}
UIApplication.shared.openURL(url)
}
@objc func didLongPressSent(sender: UIGestureRecognizer) {
guard sender.state == .began else {
return
}
let messageTimestamp = "\(message.timestamp)"
UIPasteboard.general.string = messageTimestamp
}
var lastSearchedText: String? {
return nil
}
// MediaGalleryDataSourceDelegate
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject) {
Logger.info("")
guard (items.map({ $0.message }) == [self.message]) else {
// Should only be one message we can delete when viewing message details
owsFailDebug("Unexpectedly informed of irrelevant message deletion")
return
}
self.wasDeleted = true
}
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath]) {
self.dismiss(animated: true) {
self.navigationController?.popViewController(animated: true)
}
}
// MARK: - ContactShareViewHelperDelegate
public func didCreateOrEditContact() {
updateContent()
self.dismiss(animated: true)
}
}
extension MessageDetailViewController: LongTextViewDelegate {
func longTextViewMessageWasDeleted(_ longTextViewController: LongTextViewController) {
self.delegate?.detailViewMessageWasDeleted(self)
}
}

View File

@ -1,23 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentStream;
// This entity is used to display upload progress for outgoing
// attachments in conversation view cells.
//
// During attachment uploads we want to:
//
// * Dim the media view using a mask layer.
// * Show and update a progress bar.
// * Disable any media view controls using a callback.
@interface AttachmentUploadView : UIView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,118 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "AttachmentUploadView.h"
#import "OWSBezierPathView.h"
#import "OWSProgressView.h"
#import <SignalUtilitiesKit/UIFont+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
#import <SessionUtilitiesKit/AppContext.h>
#import <SessionMessagingKit/TSAttachmentStream.h>
NS_ASSUME_NONNULL_BEGIN
@interface AttachmentUploadView ()
@property (nonatomic) TSAttachmentStream *attachment;
@property (nonatomic) OWSProgressView *progressView;
@property (nonatomic) UILabel *progressLabel;
@property (nonatomic) BOOL isAttachmentReady;
@property (nonatomic) CGFloat lastProgress;
@end
#pragma mark -
@implementation AttachmentUploadView
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachment
{
self = [super init];
if (self) {
OWSAssertDebug(attachment);
self.attachment = attachment;
[self createContents];
_isAttachmentReady = self.attachment.isUploaded;
[self ensureViewState];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)createContents
{
// The progress view is white. It will only be shown
// while the mask layer is visible, so it will show up
// even against all-white attachments.
_progressView = [OWSProgressView new];
self.progressView.color = [UIColor whiteColor];
[self.progressView autoSetDimension:ALDimensionWidth toSize:80.f];
[self.progressView autoSetDimension:ALDimensionHeight toSize:6.f];
self.progressLabel = [UILabel new];
self.progressLabel.text = NSLocalizedString(
@"MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING", @"Status label for messages which are uploading.")
.localizedUppercaseString;
self.progressLabel.textColor = UIColor.whiteColor;
self.progressLabel.font = [UIFont ows_dynamicTypeCaption1Font];
self.progressLabel.textAlignment = NSTextAlignmentCenter;
UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.progressView,
self.progressLabel,
]];
stackView.axis = UILayoutConstraintAxisVertical;
stackView.spacing = 4;
stackView.layoutMargins = UIEdgeInsetsMake(4, 4, 4, 4);
stackView.layoutMarginsRelativeArrangement = YES;
[self addSubview:stackView];
[stackView autoCenterInSuperview];
[stackView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual];
[stackView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual];
[stackView autoPinEdgeToSuperviewMargin:ALEdgeLeading relation:NSLayoutRelationGreaterThanOrEqual];
[stackView autoPinEdgeToSuperviewMargin:ALEdgeTrailing relation:NSLayoutRelationGreaterThanOrEqual];
}
- (void)setIsAttachmentReady:(BOOL)isAttachmentReady
{
if (_isAttachmentReady == isAttachmentReady) {
return;
}
_isAttachmentReady = isAttachmentReady;
[self ensureViewState];
}
- (void)setLastProgress:(CGFloat)lastProgress
{
_lastProgress = lastProgress;
[self ensureViewState];
}
- (void)ensureViewState
{
BOOL isUploading = !self.isAttachmentReady && self.lastProgress != 0;
self.backgroundColor = (isUploading ? [UIColor colorWithWhite:0.f alpha:0.2f] : nil);
self.progressView.hidden = !isUploading;
self.progressLabel.hidden = !isUploading;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,81 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class ConversationViewCell;
@class OWSContactOffersInteraction;
@class OWSContactsManager;
@class TSAttachmentStream;
@class TSCall;
@class TSErrorMessage;
@class TSInteraction;
@class TSInvalidIdentityKeyErrorMessage;
@class TSMessage;
@class TSOutgoingMessage;
@class TSQuotedMessage;
@protocol ConversationViewItem;
@protocol ConversationViewCellDelegate <NSObject>
- (void)conversationCell:(ConversationViewCell *)cell
shouldAllowReply:(BOOL)shouldAllowReply
didLongpressTextViewItem:(id<ConversationViewItem>)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell
shouldAllowReply:(BOOL)shouldAllowReply
didLongpressMediaViewItem:(id<ConversationViewItem>)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell
shouldAllowReply:(BOOL)shouldAllowReply
didLongpressQuoteViewItem:(id<ConversationViewItem>)viewItem;
- (void)conversationCell:(ConversationViewCell *)cell
didLongpressSystemMessageViewItem:(id<ConversationViewItem>)viewItem;
#pragma mark - System Cell
- (void)tappedCorruptedMessage:(TSErrorMessage *)message;
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message;
- (void)showConversationSettings;
#pragma mark - Caching
- (NSCache *)cellMediaCache;
#pragma mark - Messages
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message;
@end
#pragma mark -
// TODO: Consider making this a protocol.
@interface ConversationViewCell : UICollectionViewCell
@property (nonatomic, nullable, weak) id<ConversationViewCellDelegate> delegate;
@property (nonatomic, nullable) id<ConversationViewItem> viewItem;
// Cells are prefetched but expensive cells (e.g. media) should only load
// when visible and unload when no longer visible. Non-visible cells can
// cache their contents on their ConversationViewItem, but that cache may
// be evacuated before the cell becomes visible again.
//
// ConversationViewController also uses this property to evacuate the cell's
// meda views when:
//
// * App enters background.
// * Users enters another view (e.g. conversation settings view, call screen, etc.).
@property (nonatomic) BOOL isCellVisible;
@property (nonatomic, nullable) ConversationStyle *conversationStyle;
- (void)loadForDisplay;
- (CGSize)cellSize;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,43 +0,0 @@
//
// Copyright (c) 2018 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.delegate = nil;
self.isCellVisible = NO;
self.conversationStyle = nil;
}
- (void)loadForDisplay
{
OWSAbstractMethod();
}
- (CGSize)cellSize
{
OWSAbstractMethod();
return CGSizeZero;
}
// For perf reasons, skip the default implementation which is only relevant for self-sizing cells.
- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:
(UICollectionViewLayoutAttributes *)layoutAttributes
{
return layoutAttributes;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,839 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
extension CGPoint {
public func offsetBy(dx: CGFloat) -> CGPoint {
return CGPoint(x: x + dx, y: y)
}
public func offsetBy(dy: CGFloat) -> CGPoint {
return CGPoint(x: x, y: y + dy)
}
}
// MARK: -
@objc
public enum LinkPreviewImageState: Int {
case none
case loading
case loaded
case invalid
}
// MARK: -
@objc
public protocol LinkPreviewState {
func isLoaded() -> Bool
func urlString() -> String?
func displayDomain() -> String?
func title() -> String?
func imageState() -> LinkPreviewImageState
func image() -> UIImage?
}
// MARK: -
@objc
public class LinkPreviewLoading: NSObject, LinkPreviewState {
override init() {
}
public func isLoaded() -> Bool {
return false
}
public func urlString() -> String? {
return nil
}
public func displayDomain() -> String? {
return nil
}
public func title() -> String? {
return nil
}
public func imageState() -> LinkPreviewImageState {
return .none
}
public func image() -> UIImage? {
return nil
}
}
// MARK: -
@objc
public class LinkPreviewDraft: NSObject, LinkPreviewState {
private let linkPreviewDraft: OWSLinkPreviewDraft
@objc
public required init(linkPreviewDraft: OWSLinkPreviewDraft) {
self.linkPreviewDraft = linkPreviewDraft
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
return linkPreviewDraft.urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreviewDraft.displayDomain() else {
owsFailDebug("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
guard let value = linkPreviewDraft.title,
value.count > 0 else {
return nil
}
return value
}
public func imageState() -> LinkPreviewImageState {
if linkPreviewDraft.jpegImageData != nil {
return .loaded
} else {
return .none
}
}
public func image() -> UIImage? {
guard let jpegImageData = linkPreviewDraft.jpegImageData else {
return nil
}
guard let image = UIImage(data: jpegImageData) else {
owsFailDebug("Could not load image: \(jpegImageData.count)")
return nil
}
return image
}
}
// MARK: -
@objc
public class LinkPreviewSent: NSObject, LinkPreviewState {
private let linkPreview: OWSLinkPreview
private let imageAttachment: TSAttachment?
@objc public let conversationStyle: ConversationStyle
@objc
public var imageSize: CGSize {
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return CGSize.zero
}
return attachmentStream.imageSize()
}
@objc
public required init(linkPreview: OWSLinkPreview,
imageAttachment: TSAttachment?,
conversationStyle: ConversationStyle) {
self.linkPreview = linkPreview
self.imageAttachment = imageAttachment
self.conversationStyle = conversationStyle
}
public func isLoaded() -> Bool {
return true
}
public func urlString() -> String? {
guard let urlString = linkPreview.urlString else {
owsFailDebug("Missing url")
return nil
}
return urlString
}
public func displayDomain() -> String? {
guard let displayDomain = linkPreview.displayDomain() else {
Logger.error("Missing display domain")
return nil
}
return displayDomain
}
public func title() -> String? {
guard let value = linkPreview.title,
value.count > 0 else {
return nil
}
return value
}
public func imageState() -> LinkPreviewImageState {
guard linkPreview.imageAttachmentId != nil else {
return .none
}
guard let imageAttachment = imageAttachment else {
owsFailDebug("Missing imageAttachment.")
return .none
}
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return .loading
}
guard attachmentStream.isImage,
attachmentStream.isValidImage else {
return .invalid
}
return .loaded
}
public func image() -> UIImage? {
guard let attachmentStream = imageAttachment as? TSAttachmentStream else {
return nil
}
guard attachmentStream.isImage,
attachmentStream.isValidImage else {
return nil
}
guard let imageFilepath = attachmentStream.originalFilePath else {
owsFailDebug("Attachment is missing file path.")
return nil
}
guard let image = UIImage(contentsOfFile: imageFilepath) else {
owsFailDebug("Could not load image: \(imageFilepath)")
return nil
}
return image
}
}
// MARK: -
@objc
public protocol LinkPreviewViewDraftDelegate {
func linkPreviewCanCancel() -> Bool
func linkPreviewDidCancel()
}
// MARK: -
@objc
public class LinkPreviewImageView: UIImageView {
private let maskLayer = CAShapeLayer()
private let hasAsymmetricalRounding: Bool
@objc
public init(hasAsymmetricalRounding: Bool) {
self.hasAsymmetricalRounding = hasAsymmetricalRounding
super.init(frame: .zero)
self.layer.mask = maskLayer
}
public required init?(coder aDecoder: NSCoder) {
self.hasAsymmetricalRounding = false
super.init(coder: aDecoder)
}
public override var bounds: CGRect {
didSet {
updateMaskLayer()
}
}
public override var frame: CGRect {
didSet {
updateMaskLayer()
}
}
public override var center: CGPoint {
didSet {
updateMaskLayer()
}
}
private func updateMaskLayer() {
let layerBounds = self.bounds
// One of the corners has assymetrical rounding to match the input toolbar border.
// This is somewhat inconvenient.
let upperLeft = CGPoint(x: 0, y: 0)
let upperRight = CGPoint(x: layerBounds.size.width, y: 0)
let lowerRight = CGPoint(x: layerBounds.size.width, y: layerBounds.size.height)
let lowerLeft = CGPoint(x: 0, y: layerBounds.size.height)
let bigRounding: CGFloat = 14
let smallRounding: CGFloat = 4
let upperLeftRounding: CGFloat
let upperRightRounding: CGFloat
if hasAsymmetricalRounding {
upperLeftRounding = CurrentAppContext().isRTL ? smallRounding : bigRounding
upperRightRounding = CurrentAppContext().isRTL ? bigRounding : smallRounding
} else {
upperLeftRounding = smallRounding
upperRightRounding = smallRounding
}
let lowerRightRounding = smallRounding
let lowerLeftRounding = smallRounding
let path = UIBezierPath()
// It's sufficient to "draw" the rounded corners and not the edges that connect them.
path.addArc(withCenter: upperLeft.offsetBy(dx: +upperLeftRounding).offsetBy(dy: +upperLeftRounding),
radius: upperLeftRounding,
startAngle: CGFloat.pi * 1.0,
endAngle: CGFloat.pi * 1.5,
clockwise: true)
path.addArc(withCenter: upperRight.offsetBy(dx: -upperRightRounding).offsetBy(dy: +upperRightRounding),
radius: upperRightRounding,
startAngle: CGFloat.pi * 1.5,
endAngle: CGFloat.pi * 0.0,
clockwise: true)
path.addArc(withCenter: lowerRight.offsetBy(dx: -lowerRightRounding).offsetBy(dy: -lowerRightRounding),
radius: lowerRightRounding,
startAngle: CGFloat.pi * 0.0,
endAngle: CGFloat.pi * 0.5,
clockwise: true)
path.addArc(withCenter: lowerLeft.offsetBy(dx: +lowerLeftRounding).offsetBy(dy: -lowerLeftRounding),
radius: lowerLeftRounding,
startAngle: CGFloat.pi * 0.5,
endAngle: CGFloat.pi * 1.0,
clockwise: true)
maskLayer.path = path.cgPath
}
}
// MARK: -
@objc
public class LinkPreviewView: UIStackView {
private weak var draftDelegate: LinkPreviewViewDraftDelegate?
@objc
public var state: LinkPreviewState? {
didSet {
AssertIsOnMainThread()
assert(state == nil || oldValue == nil)
updateContents()
}
}
@objc
public var hasAsymmetricalRounding: Bool = false {
didSet {
AssertIsOnMainThread()
if hasAsymmetricalRounding != oldValue {
updateContents()
}
}
}
@available(*, unavailable, message:"use other constructor instead.")
required init(coder aDecoder: NSCoder) {
notImplemented()
}
@available(*, unavailable, message:"use other constructor instead.")
override init(frame: CGRect) {
notImplemented()
}
private var cancelButton: UIButton?
private weak var heroImageView: UIView?
private weak var sentBodyView: UIView?
private var layoutConstraints = [NSLayoutConstraint]()
@objc
public init(draftDelegate: LinkPreviewViewDraftDelegate?) {
self.draftDelegate = draftDelegate
super.init(frame: .zero)
if let draftDelegate = draftDelegate,
draftDelegate.linkPreviewCanCancel() {
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(wasTapped)))
}
}
private var isDraft: Bool {
return draftDelegate != nil
}
private func resetContents() {
for subview in subviews {
subview.removeFromSuperview()
}
self.axis = .horizontal
self.alignment = .center
self.distribution = .fill
self.spacing = 0
self.isLayoutMarginsRelativeArrangement = false
self.layoutMargins = .zero
cancelButton = nil
heroImageView = nil
sentBodyView = nil
NSLayoutConstraint.deactivate(layoutConstraints)
layoutConstraints = []
}
private func updateContents() {
resetContents()
guard let state = state else {
return
}
guard isDraft else {
createSentContents()
return
}
guard state.isLoaded() else {
createDraftLoadingContents()
return
}
createDraftContents(state: state)
}
private func createSentContents() {
guard let state = state as? LinkPreviewSent else {
owsFailDebug("Invalid state")
return
}
self.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
if let imageView = createImageView(state: state) {
if sentIsHero(state: state) {
createHeroSentContents(state: state,
imageView: imageView)
} else {
createNonHeroSentContents(state: state,
imageView: imageView)
}
} else {
createNonHeroSentContents(state: state,
imageView: nil)
}
}
private func sentHeroImageSize(state: LinkPreviewSent) -> CGSize {
let maxMessageWidth = state.conversationStyle.maxMessageWidth
let imageSize = state.imageSize
let minImageHeight: CGFloat = maxMessageWidth * 0.5
let maxImageHeight: CGFloat = maxMessageWidth
let rawImageHeight = maxMessageWidth * imageSize.height / imageSize.width
let imageHeight: CGFloat = min(maxImageHeight, max(minImageHeight, rawImageHeight))
return CGSizeCeil(CGSize(width: maxMessageWidth, height: imageHeight))
}
private func createHeroSentContents(state: LinkPreviewSent,
imageView: UIImageView) {
self.layoutMargins = .zero
self.axis = .vertical
self.alignment = .fill
let heroImageSize = sentHeroImageSize(state: state)
imageView.autoSetDimensions(to: heroImageSize)
imageView.contentMode = .scaleAspectFill
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView)
let textStack = createSentTextStack(state: state)
textStack.isLayoutMarginsRelativeArrangement = true
textStack.layoutMargins = UIEdgeInsets(top: sentHeroVMargin, left: sentHeroHMargin, bottom: sentHeroVMargin, right: sentHeroHMargin)
addArrangedSubview(textStack)
heroImageView = imageView
sentBodyView = textStack
}
private func createNonHeroSentContents(state: LinkPreviewSent,
imageView: UIImageView?) {
self.layoutMargins = .zero
self.axis = .horizontal
self.isLayoutMarginsRelativeArrangement = true
self.layoutMargins = UIEdgeInsets(top: sentNonHeroVMargin, left: sentNonHeroHMargin, bottom: sentNonHeroVMargin, right: sentNonHeroHMargin)
self.spacing = sentNonHeroHSpacing
if let imageView = imageView {
imageView.autoSetDimensions(to: CGSize(width: sentNonHeroImageSize, height: sentNonHeroImageSize))
imageView.contentMode = .scaleAspectFill
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
// TODO: Cropping, stroke.
addArrangedSubview(imageView)
}
let textStack = createSentTextStack(state: state)
addArrangedSubview(textStack)
sentBodyView = self
}
private func createSentTextStack(state: LinkPreviewSent) -> UIStackView {
let textStack = UIStackView()
textStack.axis = .vertical
textStack.spacing = sentVSpacing
if let titleLabel = sentTitleLabel(state: state) {
textStack.addArrangedSubview(titleLabel)
}
let domainLabel = sentDomainLabel(state: state)
textStack.addArrangedSubview(domainLabel)
return textStack
}
private let sentMinimumHeroSize: CGFloat = 200
private let sentTitleFontSizePoints: CGFloat = Values.mediumFontSize
private let sentDomainFontSizePoints: CGFloat = Values.verySmallFontSize
private let sentVSpacing: CGFloat = 4
// The "sent message" mode has two submodes: "hero" and "non-hero".
private let sentNonHeroHMargin: CGFloat = 12
private let sentNonHeroVMargin: CGFloat = 12
private let sentNonHeroImageSize: CGFloat = 72
private let sentNonHeroHSpacing: CGFloat = 8
private let sentHeroHMargin: CGFloat = 12
private let sentHeroVMargin: CGFloat = 12
private func sentIsHero(state: LinkPreviewSent) -> Bool {
let imageSize = state.imageSize
return imageSize.width >= sentMinimumHeroSize && imageSize.height >= sentMinimumHeroSize
}
private let sentTitleLineCount: Int = 2
private func sentTitleLabel(state: LinkPreviewState) -> UILabel? {
guard let text = state.title() else {
return nil
}
let label = UILabel()
label.text = text
label.font = UIFont.systemFont(ofSize: sentTitleFontSizePoints).ows_mediumWeight()
label.textColor = Theme.primaryColor
label.numberOfLines = sentTitleLineCount
label.lineBreakMode = .byWordWrapping
return label
}
private func sentDomainLabel(state: LinkPreviewState) -> UILabel {
let label = UILabel()
if let displayDomain = state.displayDomain(),
displayDomain.count > 0 {
label.text = displayDomain
} else {
label.text = NSLocalizedString("LINK_PREVIEW_UNKNOWN_DOMAIN", comment: "Label for link previews with an unknown host.")
}
label.font = UIFont.systemFont(ofSize: sentDomainFontSizePoints)
label.textColor = Theme.secondaryColor
return label
}
private let draftHeight: CGFloat = 72
private let draftMarginTop: CGFloat = 6
private func createDraftContents(state: LinkPreviewState) {
self.axis = .horizontal
self.alignment = .fill
self.distribution = .fill
self.spacing = 8
self.isLayoutMarginsRelativeArrangement = true
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: draftHeight + draftMarginTop))
// Image
let draftImageView = createDraftImageView(state: state)
if let imageView = draftImageView {
imageView.contentMode = .scaleAspectFill
imageView.autoPinToSquareAspectRatio()
let imageSize = draftHeight
imageView.autoSetDimensions(to: CGSize(width: imageSize, height: imageSize))
imageView.setContentHuggingHigh()
imageView.setCompressionResistanceHigh()
imageView.clipsToBounds = true
addArrangedSubview(imageView)
}
let hasImage = draftImageView != nil
let hMarginLeading: CGFloat = hasImage ? 6 : 12
let hMarginTrailing: CGFloat = 12
self.layoutMargins = UIEdgeInsets(top: draftMarginTop,
leading: hMarginLeading,
bottom: 0,
trailing: hMarginTrailing)
// Right
let rightStack = UIStackView()
rightStack.axis = .horizontal
rightStack.alignment = .fill
rightStack.distribution = .equalSpacing
rightStack.spacing = 8
rightStack.setContentHuggingHorizontalLow()
rightStack.setCompressionResistanceHorizontalLow()
addArrangedSubview(rightStack)
// Text
let textStack = UIStackView()
textStack.axis = .vertical
textStack.alignment = .leading
textStack.spacing = 2
textStack.setContentHuggingHorizontalLow()
textStack.setCompressionResistanceHorizontalLow()
if let title = state.title(),
title.count > 0 {
let label = UILabel()
label.text = title
label.textColor = Colors.text
label.font = .systemFont(ofSize: Values.mediumFontSize)
textStack.addArrangedSubview(label)
}
if let displayDomain = state.displayDomain(),
displayDomain.count > 0 {
let label = UILabel()
label.text = displayDomain
label.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity)
label.font = .systemFont(ofSize: Values.verySmallFontSize)
textStack.addArrangedSubview(label)
}
let textWrapper = UIStackView(arrangedSubviews: [textStack])
textWrapper.axis = .horizontal
textWrapper.alignment = .center
textWrapper.setContentHuggingHorizontalLow()
textWrapper.setCompressionResistanceHorizontalLow()
rightStack.addArrangedSubview(textWrapper)
// Cancel
let cancelStack = UIStackView()
cancelStack.axis = .horizontal
cancelStack.alignment = .top
cancelStack.setContentHuggingHigh()
cancelStack.setCompressionResistanceHigh()
let cancelImage = UIImage(named: "compose-cancel")?.withRenderingMode(.alwaysTemplate)
let cancelButton = UIButton(type: .custom)
cancelButton.setImage(cancelImage, for: .normal)
cancelButton.addTarget(self, action: #selector(didTapCancel(sender:)), for: .touchUpInside)
self.cancelButton = cancelButton
cancelButton.tintColor = Theme.secondaryColor
cancelButton.setContentHuggingHigh()
cancelButton.setCompressionResistanceHigh()
cancelButton.isHidden = false
cancelStack.addArrangedSubview(cancelButton)
rightStack.addArrangedSubview(cancelStack)
// Stroke
let strokeView = UIView()
strokeView.backgroundColor = Theme.secondaryColor
rightStack.addSubview(strokeView)
strokeView.autoPinWidthToSuperview()
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
strokeView.autoSetDimension(.height, toSize: CGHairlineWidth())
}
private func createImageView(state: LinkPreviewState) -> UIImageView? {
guard state.isLoaded() else {
owsFailDebug("State not loaded.")
return nil
}
guard state.imageState() == .loaded else {
return nil
}
guard let image = state.image() else {
owsFailDebug("Could not load image.")
return nil
}
let imageView = UIImageView()
imageView.image = image
return imageView
}
private func createDraftImageView(state: LinkPreviewState) -> UIImageView? {
guard state.isLoaded() else {
owsFailDebug("State not loaded.")
return nil
}
guard state.imageState() == .loaded else {
return nil
}
guard let image = state.image() else {
owsFailDebug("Could not load image.")
return nil
}
let imageView = LinkPreviewImageView(hasAsymmetricalRounding: self.hasAsymmetricalRounding)
imageView.image = image
return imageView
}
private func createDraftLoadingContents() {
self.axis = .vertical
self.alignment = .center
self.layoutConstraints.append(self.autoSetDimension(.height, toSize: draftHeight + draftMarginTop))
let activityIndicatorStyle: UIActivityIndicatorView.Style = (Theme.isDarkThemeEnabled
? .white
: .gray)
let activityIndicator = UIActivityIndicatorView(style: activityIndicatorStyle)
activityIndicator.startAnimating()
addArrangedSubview(activityIndicator)
let activityIndicatorSize: CGFloat = 25
activityIndicator.autoSetDimensions(to: CGSize(width: activityIndicatorSize, height: activityIndicatorSize))
// Stroke
let strokeView = UIView()
strokeView.backgroundColor = Theme.secondaryColor
self.addSubview(strokeView)
strokeView.autoPinWidthToSuperview(withMargin: 12)
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
strokeView.autoSetDimension(.height, toSize: CGHairlineWidth())
}
// MARK: Events
@objc func wasTapped(sender: UIGestureRecognizer) {
guard sender.state == .recognized else {
return
}
if let cancelButton = cancelButton {
let cancelLocation = sender.location(in: cancelButton)
// Permissive hot area to make it very easy to cancel the link preview.
let hotAreaInset: CGFloat = -20
let cancelButtonHotArea = cancelButton.bounds.insetBy(dx: hotAreaInset, dy: hotAreaInset)
if cancelButtonHotArea.contains(cancelLocation) {
self.draftDelegate?.linkPreviewDidCancel()
return
}
}
}
// MARK: Measurement
@objc
public func measure(withSentState state: LinkPreviewSent) -> CGSize {
switch state.imageState() {
case .loaded:
if sentIsHero(state: state) {
return measureSentHero(state: state)
} else {
return measureSentNonHero(state: state, hasImage: true)
}
default:
return measureSentNonHero(state: state, hasImage: false)
}
}
private func measureSentHero(state: LinkPreviewSent) -> CGSize {
let maxMessageWidth = state.conversationStyle.maxMessageWidth
var messageHeight: CGFloat = 0
let heroImageSize = sentHeroImageSize(state: state)
messageHeight += heroImageSize.height
let textStackSize = sentTextStackSize(state: state, maxWidth: maxMessageWidth - 2 * sentHeroHMargin)
messageHeight += textStackSize.height + 2 * sentHeroVMargin
return CGSizeCeil(CGSize(width: maxMessageWidth, height: messageHeight))
}
private func measureSentNonHero(state: LinkPreviewSent, hasImage: Bool) -> CGSize {
let maxMessageWidth = state.conversationStyle.maxMessageWidth
var maxTextWidth = maxMessageWidth - 2 * sentNonHeroHMargin
if hasImage {
maxTextWidth -= (sentNonHeroImageSize + sentNonHeroHSpacing)
}
let textStackSize = sentTextStackSize(state: state, maxWidth: maxTextWidth)
var result = textStackSize
if hasImage {
result.width += sentNonHeroImageSize + sentNonHeroHSpacing
result.height = max(result.height, sentNonHeroImageSize)
}
result.width += 2 * sentNonHeroHMargin
result.height += 2 * sentNonHeroVMargin
return CGSizeCeil(result)
}
private func sentTextStackSize(state: LinkPreviewSent, maxWidth: CGFloat) -> CGSize {
let domainLabel = sentDomainLabel(state: state)
let domainLabelSize = CGSizeCeil(domainLabel.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)))
var result = domainLabelSize
if let titleLabel = sentTitleLabel(state: state) {
let titleLabelSize = CGSizeCeil(titleLabel.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)))
let maxTitleLabelHeight: CGFloat = ceil(CGFloat(sentTitleLineCount) * titleLabel.font.lineHeight)
result.width = max(result.width, titleLabelSize.width)
result.height += min(maxTitleLabelHeight, titleLabelSize.height) + sentVSpacing
}
return result
}
@objc
public func addBorderViews(bubbleView: OWSBubbleView) {
if let heroImageView = self.heroImageView {
let borderView = OWSBubbleShapeView(draw: ())
borderView.strokeColor = UIColor.clear
borderView.strokeThickness = 0
heroImageView.addSubview(borderView)
bubbleView.addPartnerView(borderView)
borderView.ows_autoPinToSuperviewEdges()
}
if let sentBodyView = self.sentBodyView {
let borderView = OWSBubbleShapeView(draw: ())
borderView.strokeColor = UIColor.clear
borderView.strokeThickness = 0
sentBodyView.addSubview(borderView)
bubbleView.addPartnerView(borderView)
borderView.ows_autoPinToSuperviewEdges()
} else {
owsFailDebug("Missing sentBodyView")
}
}
@objc func didTapCancel(sender: UIButton) {
self.draftDelegate?.linkPreviewDidCancel()
}
}

View File

@ -1,114 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class MediaDownloadView: UIView {
// MARK: -
private let attachmentId: String
private let radius: CGFloat
private let shapeLayer1 = CAShapeLayer()
private let shapeLayer2 = CAShapeLayer()
@objc
public required init(attachmentId: String, radius: CGFloat) {
self.attachmentId = attachmentId
self.radius = radius
super.init(frame: .zero)
shapeLayer1.zPosition = 1
shapeLayer2.zPosition = 2
layer.addSublayer(shapeLayer1)
layer.addSublayer(shapeLayer2)
NotificationCenter.default.addObserver(forName: NSNotification.Name.attachmentDownloadProgress, object: nil, queue: nil) { [weak self] notification in
guard let strongSelf = self else { return }
guard let notificationAttachmentId = notification.userInfo?[kAttachmentDownloadAttachmentIDKey] as? String else {
return
}
guard notificationAttachmentId == strongSelf.attachmentId else {
return
}
strongSelf.updateLayers()
}
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc public override var bounds: CGRect {
didSet {
if oldValue != bounds {
updateLayers()
}
}
}
@objc public override var frame: CGRect {
didSet {
if oldValue != frame {
updateLayers()
}
}
}
internal func updateLayers() {
AssertIsOnMainThread()
shapeLayer1.frame = self.bounds
shapeLayer2.frame = self.bounds
shapeLayer1.path = nil
shapeLayer2.path = nil
return
// We can't display download progress yet
/*
// Prevent the shape layer from animating changes.
CATransaction.begin()
CATransaction.setDisableActions(true)
let center = CGPoint(x: self.bounds.width * 0.5,
y: self.bounds.height * 0.5)
let outerRadius: CGFloat = radius * 1.0
let innerRadius: CGFloat = radius * 0.9
let startAngle: CGFloat = CGFloat.pi * 1.5
let endAngle: CGFloat = CGFloat.pi * (1.5 + 2 * CGFloat(progress.floatValue))
let bezierPath1 = UIBezierPath()
bezierPath1.append(UIBezierPath(ovalIn: CGRect(origin: center.minus(CGPoint(x: innerRadius,
y: innerRadius)),
size: CGSize(width: innerRadius * 2,
height: innerRadius * 2))))
bezierPath1.append(UIBezierPath(ovalIn: CGRect(origin: center.minus(CGPoint(x: outerRadius,
y: outerRadius)),
size: CGSize(width: outerRadius * 2,
height: outerRadius * 2))))
shapeLayer1.path = bezierPath1.cgPath
let fillColor1: UIColor = UIColor(white: 1.0, alpha: 0.4)
shapeLayer1.fillColor = fillColor1.cgColor
shapeLayer1.fillRule = .evenOdd
let bezierPath2 = UIBezierPath()
bezierPath2.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
bezierPath2.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
shapeLayer2.path = bezierPath2.cgPath
let fillColor2: UIColor = (Theme.isDarkThemeEnabled ? .ows_gray25 : .ows_white)
shapeLayer2.fillColor = fillColor2.cgColor
CATransaction.commit()
*/
}
}

View File

@ -1,102 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class MediaUploadView: UIView {
// MARK: -
private let attachmentId: String
private let radius: CGFloat
private let shapeLayer1 = CAShapeLayer()
private let shapeLayer2 = CAShapeLayer()
private var isAttachmentReady: Bool = false
private var lastProgress: CGFloat = 0
@objc
public required init(attachmentId: String, radius: CGFloat) {
self.attachmentId = attachmentId
self.radius = radius
super.init(frame: .zero)
layer.addSublayer(shapeLayer1)
layer.addSublayer(shapeLayer2)
}
@available(*, unavailable, message: "use other init() instead.")
required public init?(coder aDecoder: NSCoder) {
notImplemented()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc public override var bounds: CGRect {
didSet {
if oldValue != bounds {
updateLayers()
}
}
}
@objc public override var frame: CGRect {
didSet {
if oldValue != frame {
updateLayers()
}
}
}
internal func updateLayers() {
AssertIsOnMainThread()
shapeLayer1.frame = self.bounds
shapeLayer2.frame = self.bounds
guard !isAttachmentReady else {
shapeLayer1.path = nil
shapeLayer2.path = nil
return
}
// Prevent the shape layer from animating changes.
CATransaction.begin()
CATransaction.setDisableActions(true)
let center = CGPoint(x: self.bounds.width * 0.5,
y: self.bounds.height * 0.5)
let outerRadius: CGFloat = radius * 1.0
let innerRadius: CGFloat = radius * 0.9
let startAngle: CGFloat = CGFloat.pi * 1.5
let endAngle: CGFloat = CGFloat.pi * (1.5 + 2 * lastProgress)
let bezierPath1 = UIBezierPath()
bezierPath1.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
bezierPath1.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
shapeLayer1.path = bezierPath1.cgPath
shapeLayer1.fillColor = UIColor.ows_white.cgColor
let innerCircleBounds = CGRect(x: center.x - innerRadius,
y: center.y - innerRadius,
width: innerRadius * 2,
height: innerRadius * 2)
let outerCircleBounds = CGRect(x: center.x - outerRadius,
y: center.y - outerRadius,
width: outerRadius * 2,
height: outerRadius * 2)
let bezierPath2 = UIBezierPath()
bezierPath2.append(UIBezierPath(ovalIn: innerCircleBounds))
bezierPath2.append(UIBezierPath(ovalIn: outerCircleBounds))
shapeLayer2.path = bezierPath2.cgPath
shapeLayer2.fillColor = UIColor(white: 1.0, alpha: 0.4).cgColor
shapeLayer2.fillRule = .evenOdd
CATransaction.commit()
}
}

View File

@ -1,180 +0,0 @@
@objc(LKMentionCandidateSelectionView)
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
@objc var publicChatServer: String?
var publicChatChannel: UInt64?
@objc var delegate: MentionCandidateSelectionViewDelegate?
// MARK: Convenience
@objc(setPublicChatChannel:)
func setPublicChatChannel(to publicChatChannel: UInt64) {
self.publicChatChannel = publicChatChannel != 0 ? publicChatChannel : nil
}
// MARK: Components
@objc lazy var tableView: UITableView = { // TODO: Make this private
let result = UITableView()
result.dataSource = self
result.delegate = self
result.register(Cell.self, forCellReuseIdentifier: "Cell")
result.separatorStyle = .none
result.backgroundColor = .clear
result.showsVerticalScrollIndicator = false
return result
}()
// MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
addSubview(tableView)
tableView.pin(to: self)
let topSeparator = UIView()
topSeparator.backgroundColor = Colors.separator
topSeparator.set(.height, to: Values.separatorThickness)
addSubview(topSeparator)
topSeparator.pin(.leading, to: .leading, of: self)
topSeparator.pin(.top, to: .top, of: self)
topSeparator.pin(.trailing, to: .trailing, of: self)
let bottomSeparator = UIView()
bottomSeparator.backgroundColor = Colors.separator
bottomSeparator.set(.height, to: Values.separatorThickness)
addSubview(bottomSeparator)
bottomSeparator.pin(.leading, to: .leading, of: self)
bottomSeparator.pin(.trailing, to: .trailing, of: self)
bottomSeparator.pin(.bottom, to: .bottom, of: self)
}
// MARK: Data
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return mentionCandidates.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
let mentionCandidate = mentionCandidates[indexPath.row]
cell.mentionCandidate = mentionCandidate
cell.publicChatServer = publicChatServer
cell.publicChatChannel = publicChatChannel
cell.separator.isHidden = (indexPath.row == (mentionCandidates.count - 1))
return cell
}
// MARK: Interaction
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let mentionCandidate = mentionCandidates[indexPath.row]
delegate?.handleMentionCandidateSelected(mentionCandidate, from: self)
}
}
// MARK: - Cell
private extension MentionCandidateSelectionView {
final class Cell : UITableViewCell {
var mentionCandidate = Mention(publicKey: "", displayName: "") { didSet { update() } }
var publicChatServer: String?
var publicChatChannel: UInt64?
// MARK: Components
private lazy var profilePictureView = ProfilePictureView()
private lazy var moderatorIconImageView: UIImageView = {
let result = UIImageView(image: #imageLiteral(resourceName: "Crown"))
return result
}()
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.smallFontSize)
result.lineBreakMode = .byTruncatingTail
return result
}()
lazy var separator: UIView = {
let result = UIView()
result.backgroundColor = Colors.separator
result.set(.height, to: Values.separatorThickness)
return result
}()
// MARK: Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
// Set the cell background color
backgroundColor = Colors.cellBackground
// Set up the highlight color
let selectedBackgroundView = UIView()
selectedBackgroundView.backgroundColor = Colors.cellBackground // Intentionally not Colors.cellSelected
self.selectedBackgroundView = selectedBackgroundView
// Set up the profile picture image view
let profilePictureViewSize = Values.verySmallProfilePictureSize
profilePictureView.set(.width, to: profilePictureViewSize)
profilePictureView.set(.height, to: profilePictureViewSize)
profilePictureView.size = profilePictureViewSize
// Set up the main stack view
let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = Values.mediumSpacing
stackView.set(.height, to: profilePictureViewSize)
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing)
stackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.smallSpacing)
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
// Set up the moderator icon image view
moderatorIconImageView.set(.width, to: 20)
moderatorIconImageView.set(.height, to: 20)
contentView.addSubview(moderatorIconImageView)
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView)
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 3.5)
// Set up the separator
addSubview(separator)
separator.pin(.leading, to: .leading, of: self)
separator.pin(.trailing, to: .trailing, of: self)
separator.pin(.bottom, to: .bottom, of: self)
}
// MARK: Updating
private func update() {
displayNameLabel.text = mentionCandidate.displayName
profilePictureView.hexEncodedPublicKey = mentionCandidate.publicKey
profilePictureView.update()
if let server = publicChatServer, let channel = publicChatChannel {
let isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, for: channel, on: server)
moderatorIconImageView.isHidden = !isUserModerator
} else {
moderatorIconImageView.isHidden = true
}
}
}
}
// MARK: - Delegate
@objc(LKMentionCandidateSelectionViewDelegate)
protocol MentionCandidateSelectionViewDelegate {
func handleMentionCandidateSelected(_ mentionCandidate: Mention, from mentionCandidateSelectionView: MentionCandidateSelectionView)
}

View File

@ -1,42 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
NS_ASSUME_NONNULL_BEGIN
@class OWSBubbleView;
// While rendering message bubbles, we often need to render
// into a subregion of the bubble that reflects the intersection
// of some subview (e.g. a media view) and the bubble shape
// (including its rounding).
//
// This view serves three different roles:
//
// * Drawing: Filling and/or stroking a subregion of the bubble shape.
// * Shadows: Casting a shadow over a subregion of the bubble shape.
// * Clipping: Clipping subviews to subregion of the bubble shape.
@interface OWSBubbleShapeView : UIView <OWSBubbleViewPartner>
@property (nonatomic, nullable) UIColor *fillColor;
@property (nonatomic, nullable) UIColor *strokeColor;
@property (nonatomic) CGFloat strokeThickness;
@property (nonatomic, nullable) UIColor *innerShadowColor;
@property (nonatomic) CGFloat innerShadowRadius;
@property (nonatomic) float innerShadowOpacity;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initDraw NS_DESIGNATED_INITIALIZER;
- (instancetype)initShadow NS_DESIGNATED_INITIALIZER;
- (instancetype)initClip NS_DESIGNATED_INITIALIZER;
- (instancetype)initInnerShadowWithColor:(UIColor *)color
radius:(CGFloat)radius
opacity:(float)opacity NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,290 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleShapeView.h"
#import "OWSBubbleView.h"
#import <SessionUtilitiesKit/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, OWSBubbleShapeViewMode) {
// For stroking or filling.
OWSBubbleShapeViewMode_Draw,
OWSBubbleShapeViewMode_Shadow,
OWSBubbleShapeViewMode_Clip,
OWSBubbleShapeViewMode_InnerShadow,
};
@interface OWSBubbleShapeView ()
@property (nonatomic) OWSBubbleShapeViewMode mode;
@property (nonatomic) CAShapeLayer *shapeLayer;
@property (nonatomic) CAShapeLayer *maskLayer;
@property (nonatomic, nullable, weak) OWSBubbleView *bubbleView;
@property (nonatomic) BOOL isConfigured;
@end
#pragma mark -
@implementation OWSBubbleShapeView
- (void)configure
{
self.opaque = NO;
self.backgroundColor = [UIColor clearColor];
self.layoutMargins = UIEdgeInsetsZero;
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
self.maskLayer = [CAShapeLayer new];
self.isConfigured = YES;
[self updateLayers];
}
- (instancetype)initDraw
{
self = [super initWithFrame:CGRectZero];
if (!self) {
return self;
}
self.mode = OWSBubbleShapeViewMode_Draw;
[self configure];
return self;
}
- (instancetype)initShadow
{
self = [super initWithFrame:CGRectZero];
if (!self) {
return self;
}
self.mode = OWSBubbleShapeViewMode_Shadow;
[self configure];
return self;
}
- (instancetype)initClip
{
self = [super initWithFrame:CGRectZero];
if (!self) {
return self;
}
self.mode = OWSBubbleShapeViewMode_Clip;
[self configure];
return self;
}
- (instancetype)initInnerShadowWithColor:(UIColor *)color radius:(CGFloat)radius opacity:(float)opacity
{
self = [super initWithFrame:CGRectZero];
if (!self) {
return self;
}
self.mode = OWSBubbleShapeViewMode_InnerShadow;
_innerShadowColor = color;
_innerShadowRadius = radius;
_innerShadowOpacity = opacity;
[self configure];
return self;
}
- (void)setFillColor:(nullable UIColor *)fillColor
{
_fillColor = fillColor;
[self updateLayers];
}
- (void)setStrokeColor:(nullable UIColor *)strokeColor
{
_strokeColor = strokeColor;
[self updateLayers];
}
- (void)setStrokeThickness:(CGFloat)strokeThickness
{
_strokeThickness = strokeThickness;
[self updateLayers];
}
- (void)setInnerShadowColor:(nullable UIColor *)innerShadowColor
{
_innerShadowColor = innerShadowColor;
[self updateLayers];
}
- (void)setInnerShadowRadius:(CGFloat)innerShadowRadius
{
_innerShadowRadius = innerShadowRadius;
[self updateLayers];
}
- (void)setInnerShadowOpacity:(float)innerShadowOpacity
{
_innerShadowOpacity = innerShadowOpacity;
[self updateLayers];
}
- (void)setFrame:(CGRect)frame
{
BOOL didChange = !CGRectEqualToRect(self.frame, frame);
[super setFrame:frame];
if (didChange) {
[self updateLayers];
}
}
- (void)setBounds:(CGRect)bounds
{
BOOL didChange = !CGRectEqualToRect(self.bounds, bounds);
[super setBounds:bounds];
if (didChange) {
[self updateLayers];
}
}
- (void)setCenter:(CGPoint)center
{
[super setCenter:center];
[self updateLayers];
}
- (void)setBubbleView:(nullable OWSBubbleView *)bubbleView
{
_bubbleView = bubbleView;
[self updateLayers];
}
- (void)updateLayers
{
if (!self.shapeLayer) {
return;
}
if (!self.bubbleView) {
return;
}
if (!self.isConfigured) {
return;
}
// Prevent the layer from animating changes.
[CATransaction begin];
[CATransaction setDisableActions:YES];
UIBezierPath *bezierPath = [UIBezierPath new];
// Add the bubble view's path to the local path.
UIBezierPath *bubbleBezierPath = [self.bubbleView maskPath];
// We need to convert between coordinate systems using layers, not views.
CGPoint bubbleOffset = [self.layer convertPoint:CGPointZero fromLayer:self.bubbleView.layer];
CGAffineTransform transform = CGAffineTransformMakeTranslation(bubbleOffset.x, bubbleOffset.y);
[bubbleBezierPath applyTransform:transform];
[bezierPath appendPath:bubbleBezierPath];
switch (self.mode) {
case OWSBubbleShapeViewMode_Draw: {
UIBezierPath *boundsBezierPath = [UIBezierPath bezierPathWithRect:self.bounds];
[bezierPath appendPath:boundsBezierPath];
self.clipsToBounds = YES;
if (self.strokeColor) {
self.shapeLayer.strokeColor = self.strokeColor.CGColor;
self.shapeLayer.lineWidth = self.strokeThickness;
self.shapeLayer.zPosition = 100.f;
} else {
self.shapeLayer.strokeColor = nil;
self.shapeLayer.lineWidth = 0.f;
}
if (self.fillColor) {
self.shapeLayer.fillColor = self.fillColor.CGColor;
} else {
self.shapeLayer.fillColor = nil;
}
self.shapeLayer.path = bezierPath.CGPath;
break;
}
case OWSBubbleShapeViewMode_Shadow:
self.clipsToBounds = NO;
if (self.fillColor) {
self.shapeLayer.fillColor = self.fillColor.CGColor;
} else {
self.shapeLayer.fillColor = nil;
}
self.shapeLayer.path = bezierPath.CGPath;
self.shapeLayer.frame = self.bounds;
self.shapeLayer.masksToBounds = YES;
break;
case OWSBubbleShapeViewMode_Clip:
self.maskLayer.path = bezierPath.CGPath;
self.layer.mask = self.maskLayer;
break;
case OWSBubbleShapeViewMode_InnerShadow: {
self.maskLayer.path = bezierPath.CGPath;
self.layer.mask = self.maskLayer;
// Inner shadow.
// This should usually not be visible; it is used to distinguish
// profile pics from the background if they are similar.
self.shapeLayer.frame = self.bounds;
self.shapeLayer.masksToBounds = YES;
CGRect shadowBounds = self.bounds;
UIBezierPath *shadowPath = [bezierPath copy];
// This can be any value large enough to cast a sufficiently large shadow.
CGFloat shadowInset = -(self.innerShadowRadius * 4.f);
[shadowPath
appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(shadowBounds, shadowInset, shadowInset)]];
// This can be any color since the fill should be clipped.
self.shapeLayer.fillColor = UIColor.blackColor.CGColor;
self.shapeLayer.path = shadowPath.CGPath;
self.shapeLayer.fillRule = kCAFillRuleEvenOdd;
self.shapeLayer.shadowColor = self.innerShadowColor.CGColor;
self.shapeLayer.shadowRadius = self.innerShadowRadius;
self.shapeLayer.shadowOpacity = self.innerShadowOpacity;
self.shapeLayer.shadowOffset = CGSizeZero;
break;
}
}
[CATransaction commit];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,60 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat kOWSMessageCellCornerRadius_Large;
extern const CGFloat kOWSMessageCellCornerRadius_Small;
typedef NS_OPTIONS(NSUInteger, OWSDirectionalRectCorner) {
OWSDirectionalRectCornerTopLeading = 1 << 0,
OWSDirectionalRectCornerTopTrailing = 1 << 1,
OWSDirectionalRectCornerBottomLeading = 1 << 2,
OWSDirectionalRectCornerBottomTrailing = 1 << 3,
OWSDirectionalRectCornerAllCorners = ~0UL
};
@class OWSBubbleView;
@protocol OWSBubbleViewPartner <NSObject>
- (void)updateLayers;
- (void)setBubbleView:(nullable OWSBubbleView *)bubbleView;
@end
#pragma mark -
@interface OWSBubbleView : UIView
+ (UIBezierPath *)roundedBezierRectWithBubbleTop:(CGFloat)bubbleTop
bubbleLeft:(CGFloat)bubbleLeft
bubbleBottom:(CGFloat)bubbleBottom
bubbleRight:(CGFloat)bubbleRight
sharpCornerRadius:(CGFloat)sharpCornerRadius
wideCornerRadius:(CGFloat)wideCornerRadius
sharpCorners:(OWSDirectionalRectCorner)sharpCorners;
@property (nonatomic, nullable) UIColor *bubbleColor;
@property (nonatomic) OWSDirectionalRectCorner sharpCorners;
- (UIBezierPath *)maskPath;
#pragma mark - Coordination
- (void)addPartnerView:(id<OWSBubbleViewPartner>)view;
- (void)clearPartnerViews;
- (void)updatePartnerViews;
- (CGFloat)minWidth;
- (CGFloat)minHeight;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,284 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
#import "MainAppContext.h"
#import <SessionUtilitiesKit/UIView+OWS.h>
#import "Session-Swift.h"
NS_ASSUME_NONNULL_BEGIN
UIRectCorner UIRectCornerForOWSDirectionalRectCorner(OWSDirectionalRectCorner corner);
UIRectCorner UIRectCornerForOWSDirectionalRectCorner(OWSDirectionalRectCorner corner)
{
if (corner == OWSDirectionalRectCornerAllCorners) {
return UIRectCornerAllCorners;
}
UIRectCorner rectCorner = 0;
BOOL isRTL = CurrentAppContext().isRTL;
if (corner & OWSDirectionalRectCornerTopLeading) {
rectCorner = rectCorner | (isRTL ? UIRectCornerTopRight : UIRectCornerTopLeft);
}
if (corner & OWSDirectionalRectCornerTopTrailing) {
rectCorner = rectCorner | (isRTL ? UIRectCornerTopLeft : UIRectCornerTopRight);
}
if (corner & OWSDirectionalRectCornerBottomTrailing) {
rectCorner = rectCorner | (isRTL ? UIRectCornerBottomLeft : UIRectCornerBottomRight);
}
if (corner & OWSDirectionalRectCornerBottomLeading) {
rectCorner = rectCorner | (isRTL ? UIRectCornerBottomRight : UIRectCornerBottomLeft);
}
return rectCorner;
}
const CGFloat kOWSMessageCellCornerRadius_Large = 10; // LKValues.messageBubbleCornerRadius
const CGFloat kOWSMessageCellCornerRadius_Small = 4;
@interface OWSBubbleView ()
@property (nonatomic) CAShapeLayer *maskLayer;
@property (nonatomic) CAShapeLayer *shapeLayer;
@property (nonatomic, readonly) NSMutableArray<id<OWSBubbleViewPartner>> *partnerViews;
@end
#pragma mark -
@implementation OWSBubbleView
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.layoutMargins = UIEdgeInsetsZero;
self.shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:self.shapeLayer];
self.maskLayer = [CAShapeLayer new];
self.layer.mask = self.maskLayer;
_partnerViews = [NSMutableArray new];
return self;
}
- (void)setFrame:(CGRect)frame
{
// We only need to update our layers if the _size_ of this view
// changes since the contents of the layers are in local coordinates.
BOOL didChangeSize = !CGSizeEqualToSize(self.frame.size, frame.size);
[super setFrame:frame];
if (didChangeSize) {
[self updateLayers];
}
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self updatePartnerViews];
}
- (void)setBounds:(CGRect)bounds
{
// We only need to update our layers if the _size_ of this view
// changes since the contents of the layers are in local coordinates.
BOOL didChangeSize = !CGSizeEqualToSize(self.bounds.size, bounds.size);
[super setBounds:bounds];
if (didChangeSize) {
[self updateLayers];
}
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self updatePartnerViews];
}
- (void)setCenter:(CGPoint)center
{
[super setCenter:center];
// We always need to inform the "bubble stroke view" (if any) if our
// frame/bounds/center changes. Its contents are not in local coordinates.
[self updatePartnerViews];
}
- (void)setBubbleColor:(nullable UIColor *)bubbleColor
{
_bubbleColor = bubbleColor;
[self updateLayers];
// Prevent the shape layer from animating changes.
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.shapeLayer.fillColor = bubbleColor.CGColor;
[CATransaction commit];
}
- (void)setSharpCorners:(OWSDirectionalRectCorner)sharpCorners
{
_sharpCorners = sharpCorners;
[self updateLayers];
}
- (void)updateLayers
{
if (!self.maskLayer) {
return;
}
if (!self.shapeLayer) {
return;
}
UIBezierPath *bezierPath = [self maskPath];
// Prevent the shape layer from animating changes.
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.shapeLayer.fillColor = self.bubbleColor.CGColor;
self.shapeLayer.path = bezierPath.CGPath;
self.maskLayer.path = bezierPath.CGPath;
[CATransaction commit];
}
- (UIBezierPath *)maskPath
{
return [self.class maskPathForSize:self.bounds.size sharpCorners:self.sharpCorners];
}
+ (UIBezierPath *)maskPathForSize:(CGSize)size sharpCorners:(OWSDirectionalRectCorner)sharpCorners
{
CGRect bounds = CGRectZero;
bounds.size = size;
CGFloat bubbleTop = 0.f;
CGFloat bubbleLeft = 0.f;
CGFloat bubbleBottom = size.height;
CGFloat bubbleRight = size.width;
return [OWSBubbleView roundedBezierRectWithBubbleTop:bubbleTop
bubbleLeft:bubbleLeft
bubbleBottom:bubbleBottom
bubbleRight:bubbleRight
sharpCornerRadius:kOWSMessageCellCornerRadius_Small
wideCornerRadius:kOWSMessageCellCornerRadius_Large
sharpCorners:sharpCorners];
}
+ (UIBezierPath *)roundedBezierRectWithBubbleTop:(CGFloat)bubbleTop
bubbleLeft:(CGFloat)bubbleLeft
bubbleBottom:(CGFloat)bubbleBottom
bubbleRight:(CGFloat)bubbleRight
sharpCornerRadius:(CGFloat)sharpCornerRadius
wideCornerRadius:(CGFloat)wideCornerRadius
sharpCorners:(OWSDirectionalRectCorner)sharpCorners
{
UIBezierPath *bezierPath = [UIBezierPath new];
UIRectCorner uiSharpCorners = UIRectCornerForOWSDirectionalRectCorner(sharpCorners);
const CGFloat topLeftRounding = (uiSharpCorners & UIRectCornerTopLeft) ? sharpCornerRadius : wideCornerRadius;
const CGFloat topRightRounding = (uiSharpCorners & UIRectCornerTopRight) ? sharpCornerRadius : wideCornerRadius;
const CGFloat bottomRightRounding
= (uiSharpCorners & UIRectCornerBottomRight) ? sharpCornerRadius : wideCornerRadius;
const CGFloat bottomLeftRounding = (uiSharpCorners & UIRectCornerBottomLeft) ? sharpCornerRadius : wideCornerRadius;
const CGFloat topAngle = 3.0f * M_PI_2;
const CGFloat rightAngle = 0.0f;
const CGFloat bottomAngle = M_PI_2;
const CGFloat leftAngle = M_PI;
// starting just to the right of the top left corner and working clockwise
[bezierPath moveToPoint:CGPointMake(bubbleLeft + topLeftRounding, bubbleTop)];
// top right corner
[bezierPath addArcWithCenter:CGPointMake(bubbleRight - topRightRounding, bubbleTop + topRightRounding)
radius:topRightRounding
startAngle:topAngle
endAngle:rightAngle
clockwise:true];
// bottom right corner
[bezierPath addArcWithCenter:CGPointMake(bubbleRight - bottomRightRounding, bubbleBottom - bottomRightRounding)
radius:bottomRightRounding
startAngle:rightAngle
endAngle:bottomAngle
clockwise:true];
// bottom left corner
[bezierPath addArcWithCenter:CGPointMake(bubbleLeft + bottomLeftRounding, bubbleBottom - bottomLeftRounding)
radius:bottomLeftRounding
startAngle:bottomAngle
endAngle:leftAngle
clockwise:true];
// top left corner
[bezierPath addArcWithCenter:CGPointMake(bubbleLeft + topLeftRounding, bubbleTop + topLeftRounding)
radius:topLeftRounding
startAngle:leftAngle
endAngle:topAngle
clockwise:true];
return bezierPath;
}
#pragma mark - Coordination
- (void)addPartnerView:(id<OWSBubbleViewPartner>)partnerView
{
OWSAssertDebug(self.partnerViews);
[partnerView setBubbleView:self];
[self.partnerViews addObject:partnerView];
}
- (void)clearPartnerViews
{
OWSAssertDebug(self.partnerViews);
[self.partnerViews removeAllObjects];
}
- (void)updatePartnerViews
{
[self layoutIfNeeded];
for (id<OWSBubbleViewPartner> partnerView in self.partnerViews) {
[partnerView updateLayers];
}
}
- (CGFloat)minWidth
{
return (kOWSMessageCellCornerRadius_Large * 2);
}
- (CGFloat)minHeight
{
return (kOWSMessageCellCornerRadius_Large * 2);
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,24 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class TSAttachment;
@protocol ConversationViewItem;
@interface OWSGenericAttachmentView : UIStackView
- (instancetype)initWithAttachment:(TSAttachment *)attachment
isIncoming:(BOOL)isIncoming
viewItem:(id<ConversationViewItem>)viewItem;
- (void)createContentsWithConversationStyle:(ConversationStyle *)conversationStyle;
- (CGSize)measureSizeWithMaxMessageWidth:(CGFloat)maxMessageWidth;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,242 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSGenericAttachmentView.h"
#import "OWSBezierPathView.h"
#import "Session-Swift.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SignalUtilitiesKit/OWSFormat.h>
#import <SignalUtilitiesKit/UIColor+OWS.h>
#import <SessionUtilitiesKit/MIMETypeUtil.h>
#import <SessionUtilitiesKit/NSString+SSK.h>
#import <SessionMessagingKit/TSAttachmentStream.h>
#import <SignalCoreKit/NSString+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSGenericAttachmentView ()
@property (nonatomic) TSAttachment *attachment;
@property (nonatomic, nullable) TSAttachmentStream *attachmentStream;
@property (nonatomic, weak) id<ConversationViewItem> viewItem;
@property (nonatomic) BOOL isIncoming;
@property (nonatomic) UILabel *topLabel;
@property (nonatomic) UILabel *bottomLabel;
@end
#pragma mark -
@implementation OWSGenericAttachmentView
- (instancetype)initWithAttachment:(TSAttachment *)attachment
isIncoming:(BOOL)isIncoming
viewItem:(id<ConversationViewItem>)viewItem
{
self = [super init];
if (self) {
_attachment = attachment;
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
_attachmentStream = (TSAttachmentStream *)attachment;
}
_isIncoming = isIncoming;
_viewItem = viewItem;
}
return self;
}
#pragma mark -
- (CGFloat)hMargin
{
return 0.f;
}
- (CGFloat)hSpacing
{
return 8.f;
}
- (CGFloat)vMargin
{
return 4.f;
}
- (CGSize)measureSizeWithMaxMessageWidth:(CGFloat)maxMessageWidth
{
CGSize result = CGSizeZero;
CGFloat labelsHeight = ([OWSGenericAttachmentView topLabelFont].lineHeight +
[OWSGenericAttachmentView bottomLabelFont].lineHeight + [OWSGenericAttachmentView labelVSpacing]);
CGFloat contentHeight = MAX(self.iconHeight, labelsHeight);
result.height = contentHeight + self.vMargin * 2;
CGFloat labelsWidth
= MAX([self.topLabel sizeThatFits:CGSizeZero].width, [self.bottomLabel sizeThatFits:CGSizeZero].width);
CGFloat contentWidth = (self.iconWidth + labelsWidth + self.hSpacing);
result.width = MIN(maxMessageWidth, contentWidth + self.hMargin * 2);
return CGSizeCeil(result);
}
- (CGFloat)iconWidth
{
return 36.f;
}
- (CGFloat)iconHeight
{
return 48.0f;
}
- (void)createContentsWithConversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(conversationStyle);
self.axis = UILayoutConstraintAxisHorizontal;
self.alignment = UIStackViewAlignmentCenter;
self.spacing = self.hSpacing;
self.layoutMarginsRelativeArrangement = YES;
self.layoutMargins = UIEdgeInsetsMake(self.vMargin, 0, self.vMargin - 4, 0);
// attachment_file
UIImage *image = [UIImage imageNamed:@"generic-attachment"];
OWSAssertDebug(image);
OWSAssertDebug(image.size.width == self.iconWidth);
OWSAssertDebug(image.size.height == self.iconHeight);
UIImageView *imageView = [UIImageView new];
imageView.image = image;
[self addArrangedSubview:imageView];
[imageView setContentHuggingHigh];
NSString *_Nullable filename = self.attachment.sourceFilename;
if (!filename) {
filename = [[self.attachmentStream originalFilePath] lastPathComponent];
}
NSString *fileExtension = filename.pathExtension;
if (fileExtension.length < 1) {
fileExtension = [MIMETypeUtil fileExtensionForMIMEType:self.attachment.contentType];
}
UILabel *fileTypeLabel = [UILabel new];
fileTypeLabel.text = fileExtension.localizedUppercaseString;
fileTypeLabel.textColor = [UIColor ows_gray90Color];
fileTypeLabel.lineBreakMode = NSLineBreakByTruncatingTail;
fileTypeLabel.font = [UIFont ows_dynamicTypeCaption1Font].ows_mediumWeight;
fileTypeLabel.adjustsFontSizeToFitWidth = YES;
fileTypeLabel.textAlignment = NSTextAlignmentCenter;
// Center on icon.
[imageView addSubview:fileTypeLabel];
[fileTypeLabel autoCenterInSuperview];
[fileTypeLabel autoSetDimension:ALDimensionWidth toSize:self.iconWidth - 20.f];
[self replaceIconWithDownloadProgressIfNecessary:imageView];
UIStackView *labelsView = [UIStackView new];
labelsView.axis = UILayoutConstraintAxisVertical;
labelsView.spacing = [OWSGenericAttachmentView labelVSpacing];
labelsView.alignment = UIStackViewAlignmentLeading;
[self addArrangedSubview:labelsView];
NSString *topText = [self.attachment.sourceFilename ows_stripped];
if (topText.length < 1) {
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachment.contentType].localizedUppercaseString;
}
if (topText.length < 1) {
topText = NSLocalizedString(@"GENERIC_ATTACHMENT_LABEL", @"A label for generic attachments.");
}
UILabel *topLabel = [UILabel new];
self.topLabel = topLabel;
topLabel.text = topText;
topLabel.textColor = [conversationStyle bubbleTextColorWithIsIncoming:self.isIncoming];
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
topLabel.font = [OWSGenericAttachmentView topLabelFont];
[labelsView addArrangedSubview:topLabel];
unsigned long long fileSize = 0;
if (self.attachmentStream) {
NSError *error;
fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:[self.attachmentStream originalFilePath]
error:&error]
.fileSize;
OWSAssertDebug(!error);
}
// We don't want to show the file size while the attachment is downloading.
// To avoid layout jitter when the download completes, we reserve space in
// the layout using a whitespace string.
NSString *bottomText = @" ";
if (fileSize > 0) {
bottomText = [OWSFormat formatFileSize:fileSize];
}
UILabel *bottomLabel = [UILabel new];
self.bottomLabel = bottomLabel;
bottomLabel.text = bottomText;
bottomLabel.textColor = [conversationStyle bubbleSecondaryTextColorWithIsIncoming:self.isIncoming];
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
bottomLabel.font = [OWSGenericAttachmentView bottomLabelFont];
[labelsView addArrangedSubview:bottomLabel];
}
- (void)replaceIconWithDownloadProgressIfNecessary:(UIView *)iconView
{
if (!self.viewItem.attachmentPointer) {
return;
}
switch (self.viewItem.attachmentPointer.state) {
case TSAttachmentPointerStateFailed:
// We don't need to handle the "tap to retry" state here,
// only download progress.
return;
case TSAttachmentPointerStateEnqueued:
case TSAttachmentPointerStateDownloading:
break;
}
switch (self.viewItem.attachmentPointer.pointerType) {
case TSAttachmentPointerTypeRestoring:
// TODO: Show "restoring" indicator and possibly progress.
return;
case TSAttachmentPointerTypeUnknown:
case TSAttachmentPointerTypeIncoming:
break;
}
NSString *_Nullable uniqueId = self.viewItem.attachmentPointer.uniqueId;
if (uniqueId.length < 1) {
OWSFailDebug(@"Missing uniqueId.");
return;
}
CGSize iconViewSize = [iconView sizeThatFits:CGSizeZero];
CGFloat downloadViewSize = MIN(iconViewSize.width, iconViewSize.height);
MediaDownloadView *downloadView =
[[MediaDownloadView alloc] initWithAttachmentId:uniqueId radius:downloadViewSize * 0.5f];
iconView.layer.opacity = 0.01f;
[self addSubview:downloadView];
[downloadView autoSetDimensionsToSize:CGSizeMake(downloadViewSize, downloadViewSize)];
[downloadView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:iconView];
[downloadView autoAlignAxis:ALAxisVertical toSameAxisOfView:iconView];
}
+ (UIFont *)topLabelFont
{
return [UIFont systemFontOfSize:LKValues.mediumFontSize];
}
+ (UIFont *)bottomLabelFont
{
return [UIFont ows_dynamicTypeCaption1Font];
}
+ (CGFloat)labelVSpacing
{
return 2.f;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,11 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@interface OWSLabel : UILabel
@end
NS_ASSUME_NONNULL_END

View File

@ -1,112 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSLabel.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSLabel ()
@property (nonatomic, nullable) NSValue *cachedSize;
@end
#pragma mark -
@implementation OWSLabel
- (void)setText:(nullable NSString *)text
{
if ([NSObject isNullableObject:text equalTo:self.text]) {
return;
}
[super setText:text];
self.cachedSize = nil;
}
- (void)setAttributedText:(nullable NSAttributedString *)attributedText
{
if ([NSObject isNullableObject:attributedText equalTo:self.attributedText]) {
return;
}
[super setAttributedText:attributedText];
self.cachedSize = nil;
}
- (void)setTextColor:(nullable UIColor *)textColor
{
if ([NSObject isNullableObject:textColor equalTo:self.textColor]) {
return;
}
[super setTextColor:textColor];
// No need to clear cached size here.
}
- (void)setFont:(nullable UIFont *)font
{
if ([NSObject isNullableObject:font equalTo:self.font]) {
return;
}
[super setFont:font];
self.cachedSize = nil;
}
- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode
{
if (self.lineBreakMode == lineBreakMode) {
return;
}
[super setLineBreakMode:lineBreakMode];
self.cachedSize = nil;
}
- (void)setNumberOfLines:(NSInteger)numberOfLines
{
if (self.numberOfLines == numberOfLines) {
return;
}
[super setNumberOfLines:numberOfLines];
self.cachedSize = nil;
}
- (void)setAdjustsFontSizeToFitWidth:(BOOL)adjustsFontSizeToFitWidth
{
if (self.adjustsFontSizeToFitWidth == adjustsFontSizeToFitWidth) {
return;
}
[super setAdjustsFontSizeToFitWidth:adjustsFontSizeToFitWidth];
self.cachedSize = nil;
}
- (void)setMinimumScaleFactor:(CGFloat)minimumScaleFactor
{
if (self.minimumScaleFactor == minimumScaleFactor) {
return;
}
[super setMinimumScaleFactor:minimumScaleFactor];
self.cachedSize = nil;
}
- (void)setMinimumFontSize:(CGFloat)minimumFontSize
{
if (self.minimumFontSize == minimumFontSize) {
return;
}
[super setMinimumFontSize:minimumFontSize];
self.cachedSize = nil;
}
- (CGSize)sizeThatFits:(CGSize)size
{
if (self.cachedSize) {
return self.cachedSize.CGSizeValue;
}
CGSize result = [super sizeThatFits:size];
self.cachedSize = [NSValue valueWithCGSize:result];
return result;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,101 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ContactShareViewModel;
@class ConversationStyle;
@protocol ConversationViewItem;
@class OWSContact;
@class OWSLinkPreview;
@class OWSQuotedReplyModel;
@class TSAttachmentPointer;
@class TSAttachmentStream;
@class TSOutgoingMessage;
typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
// Message text, etc.
OWSMessageGestureLocation_Default,
OWSMessageGestureLocation_OversizeText,
OWSMessageGestureLocation_Media,
OWSMessageGestureLocation_QuotedReply,
OWSMessageGestureLocation_LinkPreview,
};
@protocol OWSMessageBubbleViewDelegate
- (void)didTapImageViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView;
- (void)didTapVideoViewItem:(id<ConversationViewItem>)viewItem
attachmentStream:(TSAttachmentStream *)attachmentStream
imageView:(UIView *)imageView;
- (void)didTapAudioViewItem:(id<ConversationViewItem>)viewItem attachmentStream:(TSAttachmentStream *)attachmentStream;
- (void)didPanAudioViewItemToCurrentTime:(NSTimeInterval)currentTime;
- (void)didTapTruncatedTextMessage:(id<ConversationViewItem>)conversationItem;
- (void)didTapFailedIncomingAttachment:(id<ConversationViewItem>)viewItem;
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem quotedReply:(OWSQuotedReplyModel *)quotedReply;
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem
quotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview;
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
@end
#pragma mark -
@interface OWSMessageBubbleView : UIView
@property (nonatomic, nullable) id<ConversationViewItem> viewItem;
@property (nonatomic) ConversationStyle *conversationStyle;
@property (nonatomic) NSCache *cellMediaCache;
@property (nonatomic, nullable, readonly) UIView *bodyMediaView;
@property (nonatomic, weak) id<OWSMessageBubbleViewDelegate> delegate;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (void)configureViews;
- (void)loadContent;
- (void)unloadContent;
- (CGSize)measureSize;
- (void)prepareForReuse;
+ (NSDictionary *)senderNamePrimaryAttributes;
+ (NSDictionary *)senderNameSecondaryAttributes;
#pragma mark - Gestures
- (OWSMessageGestureLocation)gestureLocationForLocation:(CGPoint)locationInMessageBubble;
// This only needs to be called when we use the cell _outside_ the context
// of a conversation view message cell.
- (void)addTapGestureHandler;
- (void)handleTapGesture:(UITapGestureRecognizer *)sender;
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewCell.h"
@class OWSMessageBubbleView;
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageCell : ConversationViewCell
@property (nonatomic, readonly) OWSMessageBubbleView *messageBubbleView;
+ (NSString *)cellReuseIdentifier;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,523 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageCell.h"
#import "OWSMessageBubbleView.h"
#import "OWSMessageHeaderView.h"
#import "Session-Swift.h"
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageCell () <UIGestureRecognizerDelegate>
// The nullable properties are created as needed.
// The non-nullable properties are so frequently used that it's easier
// to always keep one around.
@property (nonatomic) OWSMessageHeaderView *headerView;
@property (nonatomic) OWSMessageBubbleView *messageBubbleView;
@property (nonatomic) NSLayoutConstraint *messageBubbleViewBottomConstraint;
@property (nonatomic) LKProfilePictureView *avatarView;
@property (nonatomic) UIImageView *moderatorIconImageView;
@property (nonatomic, nullable) UIImageView *sendFailureBadgeView;
@property (nonatomic, nullable) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
@property (nonatomic) BOOL isPresentingMenuController;
@end
#pragma mark -
@implementation OWSMessageCell
// `[UIView init]` invokes `[self initWithFrame:...]`.
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
// Ensure only called once.
OWSAssertDebug(!self.messageBubbleView);
self.layoutMargins = UIEdgeInsetsZero;
self.contentView.layoutMargins = UIEdgeInsetsZero;
_viewConstraints = [NSMutableArray new];
self.messageBubbleView = [OWSMessageBubbleView new];
[self.contentView addSubview:self.messageBubbleView];
self.headerView = [OWSMessageHeaderView new];
self.avatarView = [[LKProfilePictureView alloc] init];
[self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize];
[self.avatarView autoSetDimension:ALDimensionHeight toSize:self.avatarSize];
self.moderatorIconImageView = [[UIImageView alloc] init];
[self.moderatorIconImageView autoSetDimension:ALDimensionWidth toSize:20.f];
[self.moderatorIconImageView autoSetDimension:ALDimensionHeight toSize:20.f];
self.moderatorIconImageView.hidden = YES;
self.messageBubbleViewBottomConstraint = [self.messageBubbleView autoPinBottomToSuperviewMarginWithInset:0];
self.contentView.userInteractionEnabled = YES;
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
[self addGestureRecognizer:tap];
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self.contentView addGestureRecognizer:longPress];
UIPanGestureRecognizer *pan =
[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
pan.delegate = self;
[self.contentView addGestureRecognizer:pan];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setConversationStyle:(nullable ConversationStyle *)conversationStyle
{
[super setConversationStyle:conversationStyle];
self.messageBubbleView.conversationStyle = conversationStyle;
}
+ (NSString *)cellReuseIdentifier
{
return NSStringFromClass([self class]);
}
#pragma mark - Convenience Accessors
- (OWSMessageCellType)cellType
{
return self.viewItem.messageCellType;
}
- (TSMessage *)message
{
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
return (TSMessage *)self.viewItem.interaction;
}
- (BOOL)isIncoming
{
return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage;
}
- (BOOL)isOutgoing
{
return self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage;
}
- (BOOL)shouldHaveSendFailureBadge
{
if (![self.viewItem.interaction isKindOfClass:[TSOutgoingMessage class]]) {
return NO;
}
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
return outgoingMessage.messageState == TSOutgoingMessageStateFailed;
}
#pragma mark - Load
- (void)loadForDisplay
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.viewItem);
OWSAssertDebug(self.viewItem.interaction);
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
OWSAssertDebug(self.messageBubbleView);
[self.messageBubbleViewBottomConstraint setActive:YES];
self.messageBubbleView.viewItem = self.viewItem;
self.messageBubbleView.cellMediaCache = self.delegate.cellMediaCache;
[self.messageBubbleView configureViews];
[self.messageBubbleView loadContent];
if (self.viewItem.hasCellHeader) {
CGFloat headerHeight =
[self.headerView measureWithConversationViewItem:self.viewItem conversationStyle:self.conversationStyle]
.height;
[self.headerView loadForDisplayWithViewItem:self.viewItem conversationStyle:self.conversationStyle];
[self.contentView addSubview:self.headerView];
[self.viewConstraints addObjectsFromArray:@[
[self.headerView autoSetDimension:ALDimensionHeight toSize:headerHeight],
[self.headerView autoPinEdgeToSuperviewEdge:ALEdgeLeading],
[self.headerView autoPinEdgeToSuperviewEdge:ALEdgeTrailing],
[self.headerView autoPinEdgeToSuperviewEdge:ALEdgeTop],
[self.messageBubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.headerView],
]];
} else {
[self.viewConstraints addObjectsFromArray:@[
[self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeTop],
]];
}
if (self.isIncoming) {
[self.viewConstraints addObjectsFromArray:@[
[self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.gutterLeading],
[self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.gutterTrailing
relation:NSLayoutRelationGreaterThanOrEqual],
]];
} else {
if (self.shouldHaveSendFailureBadge) {
self.sendFailureBadgeView = [UIImageView new];
self.sendFailureBadgeView.image =
[self.sendFailureBadge imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.sendFailureBadgeView.tintColor = LKColors.destructive;
[self.contentView addSubview:self.sendFailureBadgeView];
CGFloat sendFailureBadgeBottomMargin
= round(self.conversationStyle.lastTextLineAxis - self.sendFailureBadgeSize * 0.5f);
[self.viewConstraints addObjectsFromArray:@[
[self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.gutterLeading
relation:NSLayoutRelationGreaterThanOrEqual],
[self.sendFailureBadgeView autoPinLeadingToTrailingEdgeOfView:self.messageBubbleView
offset:self.sendFailureBadgeSpacing],
// V-align the "send failure" badge with the
// last line of the text (if any, or where it
// would be).
[self.messageBubbleView autoPinEdge:ALEdgeBottom
toEdge:ALEdgeBottom
ofView:self.sendFailureBadgeView
withOffset:sendFailureBadgeBottomMargin],
[self.sendFailureBadgeView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.errorGutterTrailing],
[self.sendFailureBadgeView autoSetDimension:ALDimensionWidth toSize:self.sendFailureBadgeSize],
[self.sendFailureBadgeView autoSetDimension:ALDimensionHeight toSize:self.sendFailureBadgeSize],
]];
} else {
[self.viewConstraints addObjectsFromArray:@[
[self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.gutterLeading
relation:NSLayoutRelationGreaterThanOrEqual],
[self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.gutterTrailing],
]];
}
}
if ([self updateAvatarView]) {
[self.viewConstraints addObjectsFromArray:@[
[self.messageBubbleView autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:12],
[self.messageBubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.avatarView],
]];
[self.viewConstraints addObjectsFromArray:@[
[self.moderatorIconImageView autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:self.avatarView],
[self.moderatorIconImageView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.avatarView withOffset:3.5]
]];
}
}
- (UIImage *)sendFailureBadge
{
UIImage *image = [UIImage imageNamed:@"message_status_failed_large"];
OWSAssertDebug(image);
OWSAssertDebug(image.size.width == self.sendFailureBadgeSize && image.size.height == self.sendFailureBadgeSize);
return image;
}
- (CGFloat)sendFailureBadgeSize
{
return 20.f;
}
- (CGFloat)sendFailureBadgeSpacing
{
return 8.f;
}
// * If cell is visible, lazy-load (expensive) view contents.
// * If cell is not visible, eagerly unload view contents.
- (void)ensureMediaLoadState
{
OWSAssertDebug(self.messageBubbleView);
if (!self.isCellVisible) {
[self.messageBubbleView unloadContent];
} else {
[self.messageBubbleView loadContent];
}
}
#pragma mark - Avatar
// Returns YES IFF the avatar view is appropriate and configured.
- (BOOL)updateAvatarView
{
if (!self.viewItem.shouldShowSenderProfilePicture) {
return NO;
}
if (!self.viewItem.isGroupThread) {
OWSFailDebug(@"not a group thread.");
return NO;
}
if (self.viewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) {
OWSFailDebug(@"not an incoming message.");
return NO;
}
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction;
[self.contentView addSubview:self.avatarView];
self.avatarView.size = self.avatarSize;
self.avatarView.hexEncodedPublicKey = incomingMessage.authorId;
[self.avatarView update];
// Loki: Show the moderator icon if needed
if (self.viewItem.isGroupThread) { // FIXME: This logic also shouldn't apply to closed groups
SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:self.viewItem.interaction.uniqueThreadId];
if (publicChat != nil) {
BOOL isModerator = [SNOpenGroupAPI isUserModerator:incomingMessage.authorId forChannel:publicChat.channel onServer:publicChat.server];
UIImage *moderatorIcon = [UIImage imageNamed:@"Crown"];
self.moderatorIconImageView.image = moderatorIcon;
self.moderatorIconImageView.hidden = !isModerator;
}
}
[self.contentView addSubview:self.moderatorIconImageView];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:kNSNotificationName_OtherUsersProfileDidChange
object:nil];
return YES;
}
- (CGFloat)avatarSize
{
return LKValues.smallProfilePictureSize;
}
- (void)otherUsersProfileDidChange:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
if (!self.viewItem.shouldShowSenderProfilePicture) {
return;
}
if (!self.viewItem.isGroupThread) {
OWSFailDebug(@"not a group thread.");
return;
}
if (self.viewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) {
OWSFailDebug(@"not an incoming message.");
return;
}
NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId];
if (recipientId.length == 0) {
return;
}
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction;
if (![incomingMessage.authorId isEqualToString:recipientId]) {
return;
}
[self updateAvatarView];
}
#pragma mark - Measurement
- (CGSize)cellSize
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
OWSAssertDebug(self.viewItem);
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
OWSAssertDebug(self.messageBubbleView);
self.messageBubbleView.viewItem = self.viewItem;
self.messageBubbleView.cellMediaCache = self.delegate.cellMediaCache;
CGSize messageBubbleSize = [self.messageBubbleView measureSize];
CGSize cellSize = messageBubbleSize;
OWSAssertDebug(cellSize.width > 0 && cellSize.height > 0);
if (self.viewItem.hasCellHeader) {
cellSize.height +=
[self.headerView measureWithConversationViewItem:self.viewItem conversationStyle:self.conversationStyle]
.height;
}
if (self.shouldHaveSendFailureBadge) {
cellSize.width += self.sendFailureBadgeSize + self.sendFailureBadgeSpacing;
}
cellSize = CGSizeCeil(cellSize);
return cellSize;
}
#pragma mark - Reuse
- (void)prepareForReuse
{
[super prepareForReuse];
[NSLayoutConstraint deactivateConstraints:self.viewConstraints];
self.viewConstraints = [NSMutableArray new];
[self.messageBubbleView prepareForReuse];
[self.messageBubbleView unloadContent];
[self.headerView removeFromSuperview];
[self.avatarView removeFromSuperview];
self.moderatorIconImageView.image = nil;
[self.moderatorIconImageView removeFromSuperview];
[self.sendFailureBadgeView removeFromSuperview];
self.sendFailureBadgeView = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notifications
- (void)setIsCellVisible:(BOOL)isCellVisible {
BOOL didChange = self.isCellVisible != isCellVisible;
[super setIsCellVisible:isCellVisible];
if (!didChange) {
return;
}
[self ensureMediaLoadState];
}
#pragma mark - Gesture recognizers
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
{
OWSAssertDebug(self.delegate);
if (sender.state != UIGestureRecognizerStateRecognized) {
OWSLogVerbose(@"Ignoring tap on message: %@", self.viewItem.interaction.debugDescription);
return;
}
if ([self isGestureInCellHeader:sender]) {
return;
}
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
[self.delegate didTapFailedOutgoingMessage:outgoingMessage];
return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) {
// Ignore taps on outgoing messages being sent.
return;
}
}
[self.messageBubbleView handleTapGesture:sender];
}
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)sender
{
OWSAssertDebug(self.delegate);
if (sender.state != UIGestureRecognizerStateBegan) {
return;
}
if ([self isGestureInCellHeader:sender]) {
return;
}
BOOL shouldAllowReply = YES;
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
// Don't allow "delete" or "reply" on "failed" outgoing messages.
shouldAllowReply = NO;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) {
// Don't allow "delete" or "reply" on "sending" outgoing messages.
shouldAllowReply = NO;
}
}
CGPoint locationInMessageBubble = [sender locationInView:self.messageBubbleView];
switch ([self.messageBubbleView gestureLocationForLocation:locationInMessageBubble]) {
case OWSMessageGestureLocation_Default:
case OWSMessageGestureLocation_OversizeText:
case OWSMessageGestureLocation_LinkPreview: {
[self.delegate conversationCell:self
shouldAllowReply:shouldAllowReply
didLongpressTextViewItem:self.viewItem];
break;
}
case OWSMessageGestureLocation_Media: {
[self.delegate conversationCell:self
shouldAllowReply:shouldAllowReply
didLongpressMediaViewItem:self.viewItem];
break;
}
case OWSMessageGestureLocation_QuotedReply: {
[self.delegate conversationCell:self
shouldAllowReply:shouldAllowReply
didLongpressQuoteViewItem:self.viewItem];
break;
}
}
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender
{
[self.messageBubbleView handlePanGesture:sender];
}
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
SNVoiceMessageView *voiceMessageView = self.viewItem.lastAudioMessageView;
if (![gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class] || voiceMessageView == nil) { return NO; }
UIPanGestureRecognizer *panGestureRecognizer = (UIPanGestureRecognizer *)gestureRecognizer;
CGPoint location = [panGestureRecognizer locationInView:voiceMessageView];
if (!CGRectContainsPoint(voiceMessageView.bounds, location)) { return NO; }
CGPoint velocity = [panGestureRecognizer velocityInView:voiceMessageView];
return fabs(velocity.x) > fabs(velocity.y);
}
- (BOOL)isGestureInCellHeader:(UIGestureRecognizer *)sender
{
OWSAssertDebug(self.viewItem);
if (!self.viewItem.hasCellHeader) {
return NO;
}
CGPoint location = [sender locationInView:self];
CGPoint headerBottom = [self convertPoint:CGPointMake(0, self.headerView.height) fromView:self.headerView];
return location.y <= headerBottom.y;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,24 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@protocol ConversationViewItem;
@interface OWSMessageFooterView : UIStackView
- (void)configureWithConversationViewItem:(id<ConversationViewItem>)viewItem
isOverlayingMedia:(BOOL)isOverlayingMedia
conversationStyle:(ConversationStyle *)conversationStyle
isIncoming:(BOOL)isIncoming;
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem;
- (void)prepareForReuse;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,247 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageFooterView.h"
#import "DateUtil.h"
#import "OWSLabel.h"
#import "OWSMessageTimerView.h"
#import "Session-Swift.h"
#import <QuartzCore/QuartzCore.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageFooterView ()
@property (nonatomic) UILabel *timestampLabel;
@property (nonatomic) UIImageView *statusIndicatorImageView;
@property (nonatomic) OWSMessageTimerView *messageTimerView;
@end
@implementation OWSMessageFooterView
// `[UIView init]` invokes `[self initWithFrame:...]`.
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commontInit];
}
return self;
}
- (void)commontInit
{
// Ensure only called once.
OWSAssertDebug(!self.timestampLabel);
self.layoutMargins = UIEdgeInsetsZero;
self.axis = UILayoutConstraintAxisHorizontal;
self.alignment = UIStackViewAlignmentCenter;
self.distribution = UIStackViewDistributionEqualSpacing;
UIStackView *leftStackView = [UIStackView new];
leftStackView.axis = UILayoutConstraintAxisHorizontal;
leftStackView.spacing = self.hSpacing;
leftStackView.alignment = UIStackViewAlignmentCenter;
[self addArrangedSubview:leftStackView];
[leftStackView setContentHuggingHigh];
self.timestampLabel = [OWSLabel new];
[leftStackView addArrangedSubview:self.timestampLabel];
self.messageTimerView = [OWSMessageTimerView new];
[self.messageTimerView setContentHuggingHigh];
[leftStackView addArrangedSubview:self.messageTimerView];
self.statusIndicatorImageView = [UIImageView new];
self.userInteractionEnabled = NO;
}
- (void)configureFonts
{
self.timestampLabel.font = [UIFont systemFontOfSize:LKValues.verySmallFontSize];
}
- (CGFloat)hSpacing
{
// TODO: Review constant.
return 8.f;
}
- (CGFloat)maxImageWidth
{
return 18.f;
}
- (CGFloat)imageHeight
{
return 12.f;
}
#pragma mark - Load
- (void)configureWithConversationViewItem:(id<ConversationViewItem>)viewItem
isOverlayingMedia:(BOOL)isOverlayingMedia
conversationStyle:(ConversationStyle *)conversationStyle
isIncoming:(BOOL)isIncoming
{
OWSAssertDebug(viewItem);
OWSAssertDebug(conversationStyle);
[self configureLabelsWithConversationViewItem:viewItem];
UIColor *textColor;
if (isOverlayingMedia) {
textColor = UIColor.whiteColor;
} else {
textColor = [conversationStyle bubbleSecondaryTextColorWithIsIncoming:isIncoming];
}
self.timestampLabel.textColor = textColor;
if (viewItem.isExpiringMessage) {
TSMessage *message = (TSMessage *)viewItem.interaction;
uint64_t expirationTimestamp = message.expiresAt;
uint32_t expiresInSeconds = message.expiresInSeconds;
[self.messageTimerView configureWithExpirationTimestamp:expirationTimestamp
initialDurationSeconds:expiresInSeconds
tintColor:textColor];
self.messageTimerView.hidden = NO;
} else {
self.messageTimerView.hidden = YES;
}
if (viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
UIImage *_Nullable statusIndicatorImage = nil;
__block BOOL isNoteToSelf = NO;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
TSContactThread *thread = [outgoingMessage.thread as:TSContactThread.class];
if (thread != nil) {
NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey];
isNoteToSelf = ([thread.contactIdentifier isEqual:userPublicKey]);
}
}];
if (statusIndicatorImage && !isNoteToSelf) {
[self showStatusIndicatorWithIcon:statusIndicatorImage textColor:textColor];
} else {
[self hideStatusIndicator];
}
} else {
[self hideStatusIndicator];
}
}
- (void)showStatusIndicatorWithIcon:(UIImage *)icon textColor:(UIColor *)textColor
{
OWSAssertDebug(icon.size.width <= self.maxImageWidth);
self.statusIndicatorImageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.statusIndicatorImageView.tintColor = textColor;
[self.statusIndicatorImageView setContentHuggingHigh];
self.spacing = self.hSpacing;
}
- (void)hideStatusIndicator
{
// If there's no status indicator, we want the other
// footer contents to "cling to the leading edge".
// Instead of hiding the status indicator view,
// we clear its contents and let it stretch to fill
// the available space.
self.statusIndicatorImageView.image = nil;
[self.statusIndicatorImageView setContentHuggingLow];
self.spacing = 0;
}
- (void)animateSpinningIcon
{
CABasicAnimation *animation;
animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
animation.toValue = @(M_PI * 2.0);
const CGFloat kPeriodSeconds = 1.f;
animation.duration = kPeriodSeconds;
animation.cumulative = YES;
animation.repeatCount = HUGE_VALF;
[self.statusIndicatorImageView.layer addAnimation:animation forKey:@"animation"];
}
- (BOOL)isFailedOutgoingMessage:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
if (viewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage) {
return NO;
}
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
MessageReceiptStatus messageStatus =
[MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage];
return messageStatus == MessageReceiptStatusFailed;
}
- (void)configureLabelsWithConversationViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
[self configureFonts];
NSString *timestampLabelText;
if ([self isFailedOutgoingMessage:viewItem]) {
timestampLabelText
= NSLocalizedString(@"MESSAGE_STATUS_SEND_FAILED", @"Label indicating that a message failed to send.");
} else {
timestampLabelText = [DateUtil formatMessageTimestamp:viewItem.interaction.timestampForUI];
}
[self.timestampLabel setText:timestampLabelText.localizedUppercaseString];
}
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
[self configureLabelsWithConversationViewItem:viewItem];
CGSize result = CGSizeZero;
result.height = MAX(self.timestampLabel.font.lineHeight, self.imageHeight);
// Measure the actual current width, to be safe.
CGFloat timestampLabelWidth = [self.timestampLabel sizeThatFits:CGSizeZero].width;
result.width = timestampLabelWidth;
if (viewItem.isExpiringMessage) {
result.width += ([OWSMessageTimerView measureSize].width + self.hSpacing);
}
return CGSizeCeil(result);
}
- (nullable NSString *)messageStatusTextForConversationViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
if (viewItem.interaction.interactionType != OWSInteractionType_OutgoingMessage) {
return nil;
}
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)viewItem.interaction;
NSString *statusMessage = [MessageRecipientStatusUtils receiptMessageWithOutgoingMessage:outgoingMessage];
return statusMessage;
}
- (void)prepareForReuse
{
[self.statusIndicatorImageView.layer removeAllAnimations];
[self.messageTimerView prepareForReuse];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,23 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
extern const CGFloat OWSMessageHeaderViewDateHeaderVMargin;
@class ConversationStyle;
@protocol ConversationViewItem;
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageHeaderView : UIStackView
- (void)loadForDisplayWithViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle;
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,196 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageHeaderView.h"
#import "ConversationViewItem.h"
#import "Session-Swift.h"
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
#import <SignalUtilitiesKit/UIColor+OWS.h>
#import <SignalUtilitiesKit/UIFont+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 16; // Values.mediumSpacing
@interface OWSMessageHeaderView ()
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) UILabel *subtitleLabel;
@property (nonatomic) UIView *strokeView;
@property (nonatomic) NSArray<NSLayoutConstraint *> *layoutConstraints;
@property (nonatomic) UIStackView *stackView;
@end
#pragma mark -
@implementation OWSMessageHeaderView
// `[UIView init]` invokes `[self initWithFrame:...]`.
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commontInit];
}
return self;
}
- (void)commontInit
{
OWSAssertDebug(!self.titleLabel);
self.layoutMargins = UIEdgeInsetsZero;
self.layoutConstraints = @[];
// Intercept touches.
// Date breaks and unread indicators are not interactive.
self.userInteractionEnabled = YES;
self.strokeView = [UIView new];
[self.strokeView setContentHuggingHigh];
self.titleLabel = [UILabel new];
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
self.titleLabel.textColor = [LKColors.text colorWithAlphaComponent:0.8];
self.subtitleLabel = [UILabel new];
// The subtitle may wrap to a second line.
self.subtitleLabel.numberOfLines = 0;
self.subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping;
self.subtitleLabel.textAlignment = NSTextAlignmentCenter;
self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.strokeView,
self.titleLabel,
self.subtitleLabel,
]];
self.stackView.axis = NSTextLayoutOrientationVertical;
self.stackView.spacing = 2;
[self addSubview:self.stackView];
}
- (void)loadForDisplayWithViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(viewItem);
OWSAssertDebug(conversationStyle);
OWSAssertDebug(viewItem.unreadIndicator || viewItem.shouldShowDate);
self.titleLabel.textColor = [LKColors.text colorWithAlphaComponent:0.8];
self.subtitleLabel.textColor = [LKColors.text colorWithAlphaComponent:0.8];
[self configureLabelsWithViewItem:viewItem];
CGFloat strokeThickness = [self strokeThicknessWithViewItem:viewItem];
self.strokeView.layer.cornerRadius = strokeThickness * 0.5f;
self.strokeView.backgroundColor = [self strokeColorWithViewItem:viewItem];
self.strokeView.hidden = viewItem.unreadIndicator == nil;
self.subtitleLabel.hidden = self.subtitleLabel.text.length < 1;
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
self.layoutConstraints = @[
[self.strokeView autoSetDimension:ALDimensionHeight toSize:strokeThickness],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:conversationStyle.headerGutterLeading],
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:conversationStyle.headerGutterTrailing]
];
}
- (CGFloat)strokeThicknessWithViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
if (viewItem.unreadIndicator) {
return 1.f;
} else {
return 0.f;
}
}
- (UIColor *)strokeColorWithViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
if (viewItem.unreadIndicator) {
return Theme.secondaryColor;
} else {
return Theme.hairlineColor;
}
}
- (void)configureLabelsWithViewItem:(id<ConversationViewItem>)viewItem
{
OWSAssertDebug(viewItem);
NSDate *date = viewItem.interaction.receivedAtDate;
NSString *dateString = [DateUtil formatDateForConversationDateBreaks:date];
// Update cell to reflect changes in dynamic text.
if (viewItem.unreadIndicator) {
self.titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.verySmallFontSize];
NSString *title = NSLocalizedString(
@"MESSAGES_VIEW_UNREAD_INDICATOR", @"Indicator that separates read from unread messages.");
if (viewItem.shouldShowDate) {
title = [[dateString rtlSafeAppend:@" \u00B7 "] rtlSafeAppend:title];
}
self.titleLabel.text = title;
if (!viewItem.unreadIndicator.hasMoreUnseenMessages) {
self.subtitleLabel.text = nil;
} else {
self.subtitleLabel.text = (viewItem.unreadIndicator.missingUnseenSafetyNumberChangeCount > 0
? NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES",
@"Messages that indicates that there are more unseen messages.")
: NSLocalizedString(
@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_AND_SAFETY_NUMBER_CHANGES",
@"Messages that indicates that there are more unseen messages including safety number "
@"changes."));
}
} else {
self.titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.verySmallFontSize];
self.titleLabel.text = dateString;
self.subtitleLabel.text = nil;
}
}
- (CGSize)measureWithConversationViewItem:(id<ConversationViewItem>)viewItem
conversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(viewItem);
OWSAssertDebug(conversationStyle);
OWSAssertDebug(viewItem.unreadIndicator || viewItem.shouldShowDate);
[self configureLabelsWithViewItem:viewItem];
CGSize result = CGSizeMake(conversationStyle.viewWidth, 0);
CGFloat strokeThickness = [self strokeThicknessWithViewItem:viewItem];
result.height += strokeThickness;
if (strokeThickness != 0) {
result.height += self.stackView.spacing;
}
CGFloat maxTextWidth = conversationStyle.headerViewContentWidth;
CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)];
result.height += titleSize.height;
if (self.subtitleLabel.text.length > 0) {
CGSize subtitleSize = [self.subtitleLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)];
result.height += self.stackView.spacing + subtitleSize.height;
}
result.height += OWSMessageHeaderViewDateHeaderVMargin;
return CGSizeCeil(result);
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,15 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <SignalUtilitiesKit/OWSTextView.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageTextView : OWSTextView
@property (nonatomic) BOOL shouldIgnoreEvents;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,129 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageTextView.h"
#import <SessionUtilitiesKit/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageTextView ()
@property (nonatomic, nullable) NSValue *cachedSize;
@end
#pragma mark -
@implementation OWSMessageTextView
// Our message text views are never used for editing;
// suppress their ability to become first responder
// so that tapping on them doesn't hide keyboard.
- (BOOL)canBecomeFirstResponder
{
return NO;
}
// Ignore interactions with the text view _except_ taps on links.
//
// We want to disable "partial" selection of text in the message
// and we want to enable "tap to resend" by tapping on a message.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event
{
if (self.shouldIgnoreEvents) {
// We ignore all events for failed messages so that users
// can tap-to-resend even "all link" messages.
return NO;
}
// Find the nearest text position to the event.
UITextPosition *_Nullable position = [self closestPositionToPoint:point];
if (!position) {
return NO;
}
// Find the range of the character in the text which contains the event.
//
// Try every layout direction (this might not be necessary).
UITextRange *_Nullable range = nil;
for (NSNumber *textLayoutDirection in @[
@(UITextLayoutDirectionLeft),
@(UITextLayoutDirectionRight),
@(UITextLayoutDirectionUp),
@(UITextLayoutDirectionDown),
]) {
range = [self.tokenizer rangeEnclosingPosition:position
withGranularity:UITextGranularityCharacter
inDirection:(UITextDirection)textLayoutDirection.intValue];
if (range) {
break;
}
}
if (!range) {
return NO;
}
// Ignore the event unless it occurred inside a link.
NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start];
BOOL result =
[self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil;
return result;
}
- (void)setText:(nullable NSString *)text
{
if ([NSObject isNullableObject:text equalTo:self.text]) {
return;
}
[super setText:text];
self.cachedSize = nil;
}
- (void)setAttributedText:(nullable NSAttributedString *)attributedText
{
if ([NSObject isNullableObject:attributedText equalTo:self.attributedText]) {
return;
}
[super setAttributedText:attributedText];
self.cachedSize = nil;
}
- (void)setTextColor:(nullable UIColor *)textColor
{
if ([NSObject isNullableObject:textColor equalTo:self.textColor]) {
return;
}
[super setTextColor:textColor];
// No need to clear cached size here.
}
- (void)setFont:(nullable UIFont *)font
{
if ([NSObject isNullableObject:font equalTo:self.font]) {
return;
}
[super setFont:font];
self.cachedSize = nil;
}
- (void)setLinkTextAttributes:(nullable NSDictionary<NSString *, id> *)linkTextAttributes
{
if ([NSObject isNullableObject:linkTextAttributes equalTo:self.linkTextAttributes]) {
return;
}
[super setLinkTextAttributes:linkTextAttributes];
self.cachedSize = nil;
}
- (CGSize)sizeThatFits:(CGSize)size
{
if (self.cachedSize) {
return self.cachedSize.CGSizeValue;
}
CGSize result = [super sizeThatFits:size];
self.cachedSize = [NSValue valueWithCGSize:result];
return result;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,50 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSBubbleView.h"
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class DisplayableText;
@class OWSBubbleShapeView;
@class OWSQuotedReplyModel;
@class TSAttachmentPointer;
@class TSQuotedMessage;
@protocol OWSQuotedMessageViewDelegate
- (void)didTapQuotedReply:(OWSQuotedReplyModel *)quotedReply
failedThumbnailDownloadAttachmentPointer:(TSAttachmentPointer *)attachmentPointer;
- (void)didCancelQuotedReply;
@end
@interface OWSQuotedMessageView : UIView
@property (nonatomic, nullable, weak) id<OWSQuotedMessageViewDelegate> delegate;
- (instancetype)init NS_UNAVAILABLE;
// Only needs to be called if we're going to render this instance.
- (void)createContents;
// Measurement
- (CGSize)sizeForMaxWidth:(CGFloat)maxWidth;
// Factory method for "message bubble" views.
+ (OWSQuotedMessageView *)quotedMessageViewForConversation:(OWSQuotedReplyModel *)quotedMessage
displayableQuotedText:(nullable DisplayableText *)displayableQuotedText
conversationStyle:(ConversationStyle *)conversationStyle
isOutgoing:(BOOL)isOutgoing
sharpCorners:(OWSDirectionalRectCorner)sharpCorners;
// Factory method for "message compose" views.
+ (OWSQuotedMessageView *)quotedMessageViewForPreview:(OWSQuotedReplyModel *)quotedMessage
conversationStyle:(ConversationStyle *)conversationStyle;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,692 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSQuotedMessageView.h"
#import "ConversationViewItem.h"
#import "Environment.h"
#import "OWSBubbleView.h"
#import "Session-Swift.h"
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SignalUtilitiesKit/UIColor+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
#import <SessionMessagingKit/TSAttachmentStream.h>
#import <SessionMessagingKit/TSMessage.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
const CGFloat kRemotelySourcedContentGlyphLength = 16;
const CGFloat kRemotelySourcedContentRowMargin = 4;
const CGFloat kRemotelySourcedContentRowSpacing = 4;
@interface OWSQuotedMessageView ()
@property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage;
@property (nonatomic, nullable, readonly) DisplayableText *displayableQuotedText;
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@property (nonatomic, readonly) BOOL isForPreview;
@property (nonatomic, readonly) BOOL isOutgoing;
@property (nonatomic, readonly) OWSDirectionalRectCorner sharpCorners;
@property (nonatomic, readonly) UILabel *quotedAuthorLabel;
@property (nonatomic, readonly) UILabel *quotedTextLabel;
@property (nonatomic, readonly) UILabel *quoteContentSourceLabel;
@end
#pragma mark -
@implementation OWSQuotedMessageView
+ (OWSQuotedMessageView *)quotedMessageViewForConversation:(OWSQuotedReplyModel *)quotedMessage
displayableQuotedText:(nullable DisplayableText *)displayableQuotedText
conversationStyle:(ConversationStyle *)conversationStyle
isOutgoing:(BOOL)isOutgoing
sharpCorners:(OWSDirectionalRectCorner)sharpCorners
{
OWSAssertDebug(quotedMessage);
return [[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage
displayableQuotedText:displayableQuotedText
conversationStyle:conversationStyle
isForPreview:NO
isOutgoing:isOutgoing
sharpCorners:sharpCorners];
}
+ (OWSQuotedMessageView *)quotedMessageViewForPreview:(OWSQuotedReplyModel *)quotedMessage
conversationStyle:(ConversationStyle *)conversationStyle
{
OWSAssertDebug(quotedMessage);
DisplayableText *_Nullable displayableQuotedText = nil;
if (quotedMessage.body.length > 0) {
displayableQuotedText = [DisplayableText displayableText:quotedMessage.body];
}
OWSQuotedMessageView *instance = [[OWSQuotedMessageView alloc]
initWithQuotedMessage:quotedMessage
displayableQuotedText:displayableQuotedText
conversationStyle:conversationStyle
isForPreview:YES
isOutgoing:YES
sharpCorners:(OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing)];
[instance createContents];
return instance;
}
- (instancetype)initWithQuotedMessage:(OWSQuotedReplyModel *)quotedMessage
displayableQuotedText:(nullable DisplayableText *)displayableQuotedText
conversationStyle:(ConversationStyle *)conversationStyle
isForPreview:(BOOL)isForPreview
isOutgoing:(BOOL)isOutgoing
sharpCorners:(OWSDirectionalRectCorner)sharpCorners
{
self = [super init];
if (!self) {
return self;
}
OWSAssertDebug(quotedMessage);
_quotedMessage = quotedMessage;
_displayableQuotedText = displayableQuotedText;
_isForPreview = isForPreview;
_conversationStyle = conversationStyle;
_isOutgoing = isOutgoing;
_sharpCorners = sharpCorners;
_quotedAuthorLabel = [UILabel new];
_quotedTextLabel = [UILabel new];
_quoteContentSourceLabel = [UILabel new];
return self;
}
- (BOOL)hasQuotedAttachment
{
return (self.quotedMessage.contentType.length > 0
&& ![OWSMimeTypeOversizeTextMessage isEqualToString:self.quotedMessage.contentType]);
}
- (BOOL)hasQuotedAttachmentThumbnailImage
{
return (self.quotedMessage.contentType.length > 0
&& ![OWSMimeTypeOversizeTextMessage isEqualToString:self.quotedMessage.contentType] &&
[TSAttachmentStream hasThumbnailForMimeType:self.quotedMessage.contentType]);
}
- (UIColor *)highlightColor
{
BOOL isQuotingSelf = [NSObject isNullableObject:self.quotedMessage.authorId equalTo:TSAccountManager.localNumber];
return (isQuotingSelf ? [self.conversationStyle bubbleColorWithIsIncoming:NO]
: [self.conversationStyle quotingSelfHighlightColor]);
}
#pragma mark -
- (CGFloat)bubbleHMargin
{
return (self.isForPreview ? 0.f : 20.f);
}
- (CGFloat)hSpacing
{
return 8.f;
}
- (CGFloat)vSpacing
{
return 4.f;
}
- (CGFloat)stripeThickness
{
return LKValues.accentLineThickness;
}
- (UIColor *)quoteBubbleBackgroundColor
{
return [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:!self.isOutgoing];
}
- (void)createContents
{
// Ensure only called once.
OWSAssertDebug(self.subviews.count < 1);
self.userInteractionEnabled = YES;
self.layoutMargins = UIEdgeInsetsZero;
self.clipsToBounds = YES;
CAShapeLayer *maskLayer = [CAShapeLayer new];
OWSDirectionalRectCorner sharpCorners = self.sharpCorners;
OWSLayerView *innerBubbleView = [[OWSLayerView alloc]
initWithFrame:CGRectZero
layoutCallback:^(UIView *layerView) {
CGRect layerFrame = layerView.bounds;
const CGFloat bubbleLeft = 0.f;
const CGFloat bubbleRight = layerFrame.size.width;
const CGFloat bubbleTop = 0.f;
const CGFloat bubbleBottom = layerFrame.size.height;
const CGFloat sharpCornerRadius = 2;
const CGFloat wideCornerRadius = self.isForPreview ? 14 : 4;
UIBezierPath *bezierPath = [OWSBubbleView roundedBezierRectWithBubbleTop:bubbleTop
bubbleLeft:bubbleLeft
bubbleBottom:bubbleBottom
bubbleRight:bubbleRight
sharpCornerRadius:sharpCornerRadius
wideCornerRadius:wideCornerRadius
sharpCorners:sharpCorners];
maskLayer.path = bezierPath.CGPath;
}];
innerBubbleView.layer.mask = maskLayer;
if (self.isForPreview) {
NSString *userHexEncodedPublicKey = [SNGeneralUtilities getUserPublicKey];
BOOL wasSentByUser = [self.quotedMessage.authorId isEqual:userHexEncodedPublicKey];
innerBubbleView.backgroundColor = [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:wasSentByUser];
} else {
innerBubbleView.backgroundColor = self.quoteBubbleBackgroundColor;
}
[self addSubview:innerBubbleView];
[innerBubbleView autoPinLeadingToSuperviewMarginWithInset:self.bubbleHMargin];
[innerBubbleView autoPinTrailingToSuperviewMarginWithInset:self.bubbleHMargin];
[innerBubbleView autoPinTopToSuperviewMargin];
[innerBubbleView autoPinBottomToSuperviewMargin];
[innerBubbleView setContentHuggingHorizontalLow];
[innerBubbleView setCompressionResistanceHorizontalLow];
UIStackView *hStackView = [UIStackView new];
hStackView.axis = UILayoutConstraintAxisHorizontal;
hStackView.spacing = self.hSpacing;
UIView *stripeView = [UIView new];
if (self.isForPreview) {
stripeView.backgroundColor = LKColors.accent;
} else {
stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing];
}
[stripeView autoSetDimension:ALDimensionWidth toSize:self.stripeThickness];
[stripeView setContentHuggingHigh];
[stripeView setCompressionResistanceHigh];
[hStackView addArrangedSubview:stripeView];
UIStackView *vStackView = [UIStackView new];
vStackView.axis = UILayoutConstraintAxisVertical;
vStackView.layoutMargins = UIEdgeInsetsMake(self.textVMargin, 0, self.textVMargin, 0);
vStackView.layoutMarginsRelativeArrangement = YES;
vStackView.spacing = self.vSpacing;
[vStackView setContentHuggingHorizontalLow];
[vStackView setCompressionResistanceHorizontalLow];
[hStackView addArrangedSubview:vStackView];
UILabel *quotedAuthorLabel = [self configureQuotedAuthorLabel];
[vStackView addArrangedSubview:quotedAuthorLabel];
[quotedAuthorLabel autoSetDimension:ALDimensionHeight toSize:self.quotedAuthorHeight];
[quotedAuthorLabel setContentHuggingVerticalHigh];
[quotedAuthorLabel setContentHuggingHorizontalLow];
[quotedAuthorLabel setCompressionResistanceHorizontalLow];
UILabel *quotedTextLabel = [self configureQuotedTextLabel];
[vStackView addArrangedSubview:quotedTextLabel];
[quotedTextLabel setContentHuggingHorizontalLow];
[quotedTextLabel setCompressionResistanceHorizontalLow];
[quotedTextLabel setCompressionResistanceVerticalHigh];
if (self.hasQuotedAttachment) {
UIView *_Nullable quotedAttachmentView = nil;
UIImage *_Nullable thumbnailImage = [self tryToLoadThumbnailImage];
if (thumbnailImage) {
quotedAttachmentView = [self imageViewForImage:thumbnailImage];
quotedAttachmentView.clipsToBounds = YES;
quotedAttachmentView.backgroundColor = [UIColor whiteColor];
if (self.isVideoAttachment) {
UIImage *contentIcon = [UIImage imageNamed:@"Play"];
contentIcon = [contentIcon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImageView *contentImageView = [self imageViewForImage:contentIcon];
contentImageView.tintColor = LKColors.text;
[quotedAttachmentView addSubview:contentImageView];
[contentImageView autoSetDimension:ALDimensionWidth toSize:16];
[contentImageView autoSetDimension:ALDimensionHeight toSize:16];
[contentImageView autoCenterInSuperview];
}
} else if (self.quotedMessage.thumbnailDownloadFailed) {
// TODO design review icon and color
UIImage *contentIcon =
[[UIImage imageNamed:@"btnRefresh--white"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
UIImageView *contentImageView = [self imageViewForImage:contentIcon];
contentImageView.contentMode = UIViewContentModeScaleAspectFit;
contentImageView.tintColor = UIColor.whiteColor;
quotedAttachmentView = [UIView containerView];
[quotedAttachmentView addSubview:contentImageView];
quotedAttachmentView.backgroundColor = self.highlightColor;
[contentImageView autoCenterInSuperview];
[contentImageView
autoSetDimensionsToSize:CGSizeMake(self.quotedAttachmentSize * 0.5f, self.quotedAttachmentSize * 0.5f)];
UITapGestureRecognizer *tapGesture =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapFailedThumbnailDownload:)];
[quotedAttachmentView addGestureRecognizer:tapGesture];
quotedAttachmentView.userInteractionEnabled = YES;
} else {
UIImage *contentIcon = [UIImage imageNamed:@"generic-attachment"];
UIImageView *contentImageView = [self imageViewForImage:contentIcon];
contentImageView.contentMode = UIViewContentModeScaleAspectFit;
UIView *wrapper = [UIView containerView];
[wrapper addSubview:contentImageView];
[contentImageView autoCenterInSuperview];
[contentImageView autoSetDimension:ALDimensionWidth toSize:self.quotedAttachmentSize * 0.5f];
quotedAttachmentView = wrapper;
}
[quotedAttachmentView autoPinToSquareAspectRatio];
[quotedAttachmentView setContentHuggingHigh];
[quotedAttachmentView setCompressionResistanceHigh];
[hStackView addArrangedSubview:quotedAttachmentView];
} else {
// If there's no attachment, add an empty view so that
// the stack view's spacing serves as a margin between
// the text views and the trailing edge.
UIView *emptyView = [UIView containerView];
[hStackView addArrangedSubview:emptyView];
[emptyView setContentHuggingHigh];
[emptyView autoSetDimension:ALDimensionWidth toSize:0.f];
}
UIView *contentView = hStackView;
[contentView setContentHuggingHorizontalLow];
[contentView setCompressionResistanceHorizontalLow];
if (self.quotedMessage.isRemotelySourced) {
UIStackView *quoteSourceWrapper = [[UIStackView alloc] initWithArrangedSubviews:@[
contentView,
[self buildRemoteContentSourceView],
]];
quoteSourceWrapper.axis = UILayoutConstraintAxisVertical;
contentView = quoteSourceWrapper;
[contentView setContentHuggingHorizontalLow];
[contentView setCompressionResistanceHorizontalLow];
}
if (self.isForPreview) {
UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIColor *tintColor = [LKAppModeUtilities isLightMode] ? UIColor.blackColor : UIColor.whiteColor;
UIImage *cancelIcon = [[UIImage imageNamed:@"X"] asTintedImageWithColor:tintColor];
[cancelButton setImage:cancelIcon forState:UIControlStateNormal];
cancelButton.contentMode = UIViewContentModeScaleAspectFit;
[cancelButton addTarget:self action:@selector(didTapCancel) forControlEvents:UIControlEventTouchUpInside];
[cancelButton autoSetDimension:ALDimensionWidth toSize:14.f];
[cancelButton autoSetDimension:ALDimensionHeight toSize:14.f];
UIStackView *cancelStack = [[UIStackView alloc] initWithArrangedSubviews:@[ cancelButton ]];
cancelStack.axis = UILayoutConstraintAxisHorizontal;
cancelStack.alignment = UIStackViewAlignmentTop;
cancelStack.layoutMarginsRelativeArrangement = YES;
CGFloat hMarginLeading = 8;
CGFloat hMarginTrailing = 8;
cancelStack.layoutMargins = UIEdgeInsetsMake(8,
CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading,
0,
CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing);
UIStackView *cancelWrapper = [[UIStackView alloc] initWithArrangedSubviews:@[
contentView,
cancelStack,
]];
cancelWrapper.axis = UILayoutConstraintAxisHorizontal;
contentView = cancelWrapper;
[contentView setContentHuggingHorizontalLow];
[contentView setCompressionResistanceHorizontalLow];
}
[innerBubbleView addSubview:contentView];
[contentView ows_autoPinToSuperviewEdges];
}
- (UIView *)buildRemoteContentSourceView
{
UIImage *glyphImage =
[[UIImage imageNamed:@"ic_broken_link"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
OWSAssertDebug(glyphImage);
OWSAssertDebug(CGSizeEqualToSize(
CGSizeMake(kRemotelySourcedContentGlyphLength, kRemotelySourcedContentGlyphLength), glyphImage.size));
UIImageView *glyphView = [[UIImageView alloc] initWithImage:glyphImage];
glyphView.tintColor = LKColors.text;
[glyphView
autoSetDimensionsToSize:CGSizeMake(kRemotelySourcedContentGlyphLength, kRemotelySourcedContentGlyphLength)];
UILabel *label = [self configureQuoteContentSourceLabel];
UIStackView *sourceRow = [[UIStackView alloc] initWithArrangedSubviews:@[ glyphView, label ]];
sourceRow.axis = UILayoutConstraintAxisHorizontal;
sourceRow.alignment = UIStackViewAlignmentCenter;
// TODO verify spacing w/ design
sourceRow.spacing = kRemotelySourcedContentRowSpacing;
sourceRow.layoutMarginsRelativeArrangement = YES;
const CGFloat leftMargin = 4;
sourceRow.layoutMargins = UIEdgeInsetsMake(kRemotelySourcedContentRowMargin,
leftMargin,
kRemotelySourcedContentRowMargin,
kRemotelySourcedContentRowMargin);
UIColor *backgroundColor = LKAppModeUtilities.isLightMode ? [UIColor.whiteColor colorWithAlphaComponent:LKValues.mediumOpacity] : [LKColors.text colorWithAlphaComponent:LKValues.mediumOpacity];
[sourceRow addBackgroundViewWithBackgroundColor:backgroundColor];
return sourceRow;
}
- (void)didTapFailedThumbnailDownload:(UITapGestureRecognizer *)gestureRecognizer
{
OWSLogDebug(@"in didTapFailedThumbnailDownload");
if (!self.quotedMessage.thumbnailDownloadFailed) {
OWSFailDebug(@"thumbnailDownloadFailed was unexpectedly false");
return;
}
if (!self.quotedMessage.thumbnailAttachmentPointer) {
OWSFailDebug(@"thumbnailAttachmentPointer was unexpectedly nil");
return;
}
[self.delegate didTapQuotedReply:self.quotedMessage
failedThumbnailDownloadAttachmentPointer:self.quotedMessage.thumbnailAttachmentPointer];
}
- (nullable UIImage *)tryToLoadThumbnailImage
{
if (!self.hasQuotedAttachmentThumbnailImage) {
return nil;
}
// TODO: Possibly ignore data that is too large.
UIImage *_Nullable image = self.quotedMessage.thumbnailImage;
// TODO: Possibly ignore images that are too large.
return image;
}
- (UIImageView *)imageViewForImage:(UIImage *)image
{
OWSAssertDebug(image);
UIImageView *imageView = [UIImageView new];
imageView.image = image;
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
imageView.contentMode = UIViewContentModeScaleAspectFill;
// Use trilinear filters for better scaling quality at
// some performance cost.
imageView.layer.minificationFilter = kCAFilterTrilinear;
imageView.layer.magnificationFilter = kCAFilterTrilinear;
return imageView;
}
- (UILabel *)configureQuotedTextLabel
{
OWSAssertDebug(self.quotedTextLabel);
UIColor *textColor = self.quotedTextColor;
SUPPRESS_DEADSTORE_WARNING(textColor);
UIFont *font = self.quotedTextFont;
SUPPRESS_DEADSTORE_WARNING(font);
NSString *text = @"";
NSString *_Nullable fileTypeForSnippet = [self fileTypeForSnippet];
NSString *_Nullable sourceFilename = [self.quotedMessage.sourceFilename filterStringForDisplay];
if (self.displayableQuotedText.displayText.length > 0) {
text = self.displayableQuotedText.displayText;
textColor = self.quotedTextColor;
font = self.quotedTextFont;
} else if (fileTypeForSnippet) {
text = fileTypeForSnippet;
textColor = self.fileTypeTextColor;
font = self.fileTypeFont;
} else if (sourceFilename) {
text = sourceFilename;
textColor = self.filenameTextColor;
font = self.filenameFont;
} else {
text = NSLocalizedString(
@"QUOTED_REPLY_TYPE_ATTACHMENT", @"Indicates this message is a quoted reply to an attachment of unknown type.");
textColor = self.fileTypeTextColor;
font = self.fileTypeFont;
}
self.quotedTextLabel.numberOfLines = self.isForPreview ? 1 : 2;
self.quotedTextLabel.lineBreakMode = NSLineBreakByTruncatingTail;
self.quotedTextLabel.text = text;
self.quotedTextLabel.textColor = textColor;
self.quotedTextLabel.font = font;
return self.quotedTextLabel;
}
- (UILabel *)configureQuoteContentSourceLabel
{
OWSAssertDebug(self.quoteContentSourceLabel);
self.quoteContentSourceLabel.font = [UIFont systemFontOfSize:LKValues.smallFontSize];
self.quoteContentSourceLabel.textColor = LKColors.text;
self.quoteContentSourceLabel.text = NSLocalizedString(@"QUOTED_REPLY_CONTENT_FROM_REMOTE_SOURCE",
@"Footer label that appears below quoted messages when the quoted content was not derived locally. When the "
@"local user doesn't have a copy of the message being quoted, e.g. if it had since been deleted, we instead "
@"show the content specified by the sender.");
return self.quoteContentSourceLabel;
}
- (nullable NSString *)fileTypeForSnippet
{
// TODO: Are we going to use the filename? For all mimetypes?
NSString *_Nullable contentType = self.quotedMessage.contentType;
if (contentType.length < 1) {
return nil;
}
if ([MIMETypeUtil isAudio:contentType]) {
return NSLocalizedString(
@"QUOTED_REPLY_TYPE_AUDIO", @"Indicates this message is a quoted reply to an audio file.");
} else if ([MIMETypeUtil isVideo:contentType]) {
return NSLocalizedString(
@"QUOTED_REPLY_TYPE_VIDEO", @"Indicates this message is a quoted reply to a video file.");
} else if ([MIMETypeUtil isImage:contentType]) {
return NSLocalizedString(
@"QUOTED_REPLY_TYPE_IMAGE", @"Indicates this message is a quoted reply to an image file.");
} else if ([MIMETypeUtil isAnimated:contentType]) {
return NSLocalizedString(
@"QUOTED_REPLY_TYPE_GIF", @"Indicates this message is a quoted reply to animated GIF file.");
}
return nil;
}
- (BOOL)isAudioAttachment
{
// TODO: Are we going to use the filename? For all mimetypes?
NSString *_Nullable contentType = self.quotedMessage.contentType;
if (contentType.length < 1) {
return NO;
}
return [MIMETypeUtil isAudio:contentType];
}
- (BOOL)isVideoAttachment
{
// TODO: Are we going to use the filename? For all mimetypes?
NSString *_Nullable contentType = self.quotedMessage.contentType;
if (contentType.length < 1) {
return NO;
}
return [MIMETypeUtil isVideo:contentType];
}
- (UILabel *)configureQuotedAuthorLabel
{
OWSAssertDebug(self.quotedAuthorLabel);
NSString *_Nullable localNumber = [TSAccountManager localNumber];
NSString *quotedAuthorText;
if ([localNumber isEqualToString:self.quotedMessage.authorId]) {
quotedAuthorText = NSLocalizedString(@"You", @"");
} else {
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: self.quotedMessage.authorId;
if (quotedAuthor == self.quotedMessage.authorId) {
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:self.quotedMessage.threadId];
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
if (openGroup != nil) {
quotedAuthor = [LKUserDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:openGroup.channel on:openGroup.server using:transaction];
}
}];
}
quotedAuthorText = quotedAuthor;
}
self.quotedAuthorLabel.text = quotedAuthorText;
self.quotedAuthorLabel.font = self.quotedAuthorFont;
// TODO:
self.quotedAuthorLabel.textColor = [self quotedAuthorColor];
self.quotedAuthorLabel.lineBreakMode = NSLineBreakByTruncatingTail;
self.quotedAuthorLabel.numberOfLines = 1;
return self.quotedAuthorLabel;
}
#pragma mark - Measurement
- (CGFloat)textVMargin
{
return 7.f;
}
- (CGSize)sizeForMaxWidth:(CGFloat)maxWidth
{
CGSize result = CGSizeZero;
result.width += self.bubbleHMargin * 2 + self.stripeThickness + self.hSpacing * 2;
CGFloat thumbnailHeight = 0.f;
if (self.hasQuotedAttachment) {
result.width += self.quotedAttachmentSize;
thumbnailHeight += self.quotedAttachmentSize;
}
// Quoted Author
CGFloat textWidth = 0.f;
CGFloat maxTextWidth = maxWidth - result.width;
CGFloat textHeight = self.textVMargin * 2 + self.quotedAuthorHeight + self.vSpacing;
{
UILabel *quotedAuthorLabel = [self configureQuotedAuthorLabel];
CGSize quotedAuthorSize = CGSizeCeil([quotedAuthorLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
textWidth = quotedAuthorSize.width;
}
{
UILabel *quotedTextLabel = [self configureQuotedTextLabel];
CGSize textSize = CGSizeCeil([quotedTextLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
textWidth = MAX(textWidth, textSize.width);
textHeight += textSize.height;
}
if (self.quotedMessage.isRemotelySourced) {
UILabel *quoteContentSourceLabel = [self configureQuoteContentSourceLabel];
CGSize textSize = CGSizeCeil([quoteContentSourceLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
CGFloat sourceStackViewHeight = MAX(kRemotelySourcedContentGlyphLength, textSize.height);
textWidth
= MAX(textWidth, textSize.width + kRemotelySourcedContentGlyphLength + kRemotelySourcedContentRowSpacing);
result.height += kRemotelySourcedContentRowMargin * 2 + sourceStackViewHeight;
}
textWidth = MIN(textWidth, maxTextWidth);
result.width += textWidth;
result.height += MAX(textHeight, thumbnailHeight);
return CGSizeCeil(result);
}
- (UIFont *)quotedAuthorFont
{
return [UIFont boldSystemFontOfSize:LKValues.smallFontSize];
}
- (UIColor *)quotedAuthorColor
{
return [self.conversationStyle quotedReplyAuthorColor];
}
- (UIColor *)quotedTextColor
{
return [self.conversationStyle quotedReplyTextColor];
}
- (UIFont *)quotedTextFont
{
return [UIFont systemFontOfSize:LKValues.smallFontSize];
}
- (UIColor *)fileTypeTextColor
{
return [self.conversationStyle quotedReplyAttachmentColor];
}
- (UIFont *)fileTypeFont
{
return self.quotedTextFont;
}
- (UIColor *)filenameTextColor
{
return [self.conversationStyle quotedReplyAttachmentColor];
}
- (UIFont *)filenameFont
{
return self.quotedTextFont;
}
- (CGFloat)quotedAuthorHeight
{
return (CGFloat)ceil([self quotedAuthorFont].lineHeight * 1.f);
}
- (CGFloat)quotedAttachmentSize
{
return 54.f;
}
#pragma mark -
- (CGSize)sizeThatFits:(CGSize)size
{
return [self sizeForMaxWidth:CGFLOAT_MAX];
}
- (void)didTapCancel
{
[self.delegate didCancelQuotedReply];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,15 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewCell.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSSystemMessageCell : ConversationViewCell
+ (NSString *)cellReuseIdentifier;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,510 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSSystemMessageCell.h"
#import "ConversationViewItem.h"
#import "OWSMessageHeaderView.h"
#import "Session-Swift.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import <SessionMessagingKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
#import <SessionMessagingKit/Environment.h>
#import <SessionMessagingKit/TSErrorMessage.h>
#import <SessionMessagingKit/TSInfoMessage.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^SystemMessageActionBlock)(void);
@interface SystemMessageAction : NSObject
@property (nonatomic) NSString *title;
@property (nonatomic) SystemMessageActionBlock block;
@end
#pragma mark -
@implementation SystemMessageAction
+ (SystemMessageAction *)actionWithTitle:(NSString *)title block:(SystemMessageActionBlock)block
{
SystemMessageAction *action = [SystemMessageAction new];
action.title = title;
action.block = block;
return action;
}
@end
#pragma mark -
@interface OWSSystemMessageCell ()
@property (nonatomic) UIImageView *iconView;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) UIButton *button;
@property (nonatomic) UIStackView *vStackView;
@property (nonatomic) UIView *cellBackgroundView;
@property (nonatomic) OWSMessageHeaderView *headerView;
@property (nonatomic) NSLayoutConstraint *headerViewHeightConstraint;
@property (nonatomic) NSArray<NSLayoutConstraint *> *layoutConstraints;
@property (nonatomic, nullable) SystemMessageAction *action;
@end
#pragma mark -
@implementation OWSSystemMessageCell
// `[UIView init]` invokes `[self initWithFrame:...]`.
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
OWSAssertDebug(!self.iconView);
self.layoutMargins = UIEdgeInsetsZero;
self.contentView.layoutMargins = UIEdgeInsetsZero;
self.layoutConstraints = @[];
self.headerView = [OWSMessageHeaderView new];
self.headerViewHeightConstraint = [self.headerView autoSetDimension:ALDimensionHeight toSize:0];
self.iconView = [UIImageView new];
[self.iconView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[self.iconView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
[self.iconView setContentHuggingHigh];
self.titleLabel = [UILabel new];
self.titleLabel.numberOfLines = 0;
self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
self.titleLabel.textAlignment = NSTextAlignmentCenter;
UIStackView *contentStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
self.iconView,
self.titleLabel,
]];
contentStackView.axis = UILayoutConstraintAxisVertical;
contentStackView.spacing = self.iconVSpacing;
contentStackView.alignment = UIStackViewAlignmentCenter;
self.button = [UIButton new];
[self.button setTitleColor:[UIColor ows_darkSkyBlueColor] forState:UIControlStateNormal];
self.button.titleLabel.textAlignment = NSTextAlignmentCenter;
self.button.layer.cornerRadius = LKValues.modalButtonCornerRadius;
self.button.backgroundColor = LKColors.buttonBackground;
self.button.titleLabel.font = [UIFont systemFontOfSize:LKValues.smallFontSize];
[self.button addTarget:self action:@selector(buttonWasPressed:) forControlEvents:UIControlEventTouchUpInside];
[self.button autoSetDimension:ALDimensionHeight toSize:self.buttonHeight];
self.vStackView = [[UIStackView alloc] initWithArrangedSubviews:@[
contentStackView,
self.button,
]];
self.vStackView.axis = UILayoutConstraintAxisVertical;
self.vStackView.spacing = self.buttonVSpacing;
self.vStackView.alignment = UIStackViewAlignmentCenter;
self.vStackView.layoutMarginsRelativeArrangement = YES;
self.cellBackgroundView = [UIView new];
self.cellBackgroundView.layer.cornerRadius = 5.f;
[self.contentView addSubview:self.cellBackgroundView];
UIStackView *cellStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ self.headerView, self.vStackView ]];
cellStackView.axis = UILayoutConstraintAxisVertical;
[self.contentView addSubview:cellStackView];
[cellStackView autoPinEdgesToSuperviewEdges];
UILongPressGestureRecognizer *longPress =
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];
[self addGestureRecognizer:longPress];
}
- (CGFloat)buttonVSpacing
{
return 12.f;
}
- (CGFloat)iconVSpacing
{
return LKValues.smallSpacing;
}
- (CGFloat)buttonHeight
{
return LKValues.mediumButtonHeight;
}
- (CGFloat)buttonHPadding
{
return 20.f;
}
- (void)configureFonts
{
// Update cell to reflect changes in dynamic text.
self.titleLabel.font = [UIFont systemFontOfSize:LKValues.smallFontSize];
}
+ (NSString *)cellReuseIdentifier
{
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.viewItem);
self.cellBackgroundView.backgroundColor = UIColor.clearColor;
[self.button setBackgroundColor:LKColors.buttonBackground];
TSInteraction *interaction = self.viewItem.interaction;
self.action = [self actionForInteraction:interaction];
UIImage *_Nullable icon = [self iconForInteraction:interaction];
if (icon) {
self.iconView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.iconView.hidden = NO;
self.iconView.tintColor = LKColors.text;
} else {
self.iconView.hidden = YES;
}
self.titleLabel.textColor = [self textColor];
[self applyTitleForInteraction:interaction label:self.titleLabel];
CGSize titleSize = [self titleSize];
if (self.action) {
[self.button setTitle:self.action.title forState:UIControlStateNormal];
UIFont *buttonFont = [UIFont systemFontOfSize:LKValues.smallFontSize];
self.button.titleLabel.font = buttonFont;
[self.button setTitleColor:LKColors.text forState:UIControlStateNormal];
self.button.hidden = NO;
} else {
self.button.hidden = YES;
}
CGSize buttonSize = [self.button sizeThatFits:CGSizeZero];
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
if (self.viewItem.hasCellHeader) {
self.headerView.hidden = NO;
CGFloat headerHeight =
[self.headerView measureWithConversationViewItem:self.viewItem conversationStyle:self.conversationStyle]
.height;
[self.headerView loadForDisplayWithViewItem:self.viewItem conversationStyle:self.conversationStyle];
self.headerViewHeightConstraint.constant = headerHeight;
} else {
self.headerView.hidden = YES;
}
self.vStackView.layoutMargins = UIEdgeInsetsMake(self.topVMargin,
self.conversationStyle.fullWidthGutterLeading,
self.bottomVMargin,
self.conversationStyle.fullWidthGutterLeading);
self.layoutConstraints = @[
[self.titleLabel autoSetDimension:ALDimensionWidth toSize:titleSize.width],
[self.button autoSetDimension:ALDimensionWidth toSize:buttonSize.width + self.buttonHPadding * 2.f],
[self.cellBackgroundView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.vStackView],
[self.cellBackgroundView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.vStackView],
// Text in vStackView might flow right up to the edges, so only use half the gutter.
[self.cellBackgroundView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:self.conversationStyle.fullWidthGutterLeading * 0.5f],
[self.cellBackgroundView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
withInset:self.conversationStyle.fullWidthGutterTrailing * 0.5f],
];
}
- (UIColor *)textColor
{
return LKColors.text;
}
- (UIColor *)iconColorForInteraction:(TSInteraction *)interaction
{
return LKColors.text;
}
- (nullable UIImage *)iconForInteraction:(TSInteraction *)interaction
{
UIImage *result = nil;
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
switch (((TSErrorMessage *)interaction).errorType) {
case TSErrorMessageNonBlockingIdentityChange:
case TSErrorMessageWrongTrustedIdentityKey:
result = [UIImage imageNamed:@"system_message_security"];
break;
case TSErrorMessageInvalidKeyException:
case TSErrorMessageMissingKeyId:
case TSErrorMessageNoSession:
case TSErrorMessageInvalidMessage:
case TSErrorMessageDuplicateMessage:
case TSErrorMessageInvalidVersion:
case TSErrorMessageUnknownContactBlockOffer:
case TSErrorMessageGroupCreationFailed:
return nil;
}
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
switch (((TSInfoMessage *)interaction).messageType) {
case TSInfoMessageUserNotRegistered:
case TSInfoMessageTypeSessionDidEnd:
case TSInfoMessageTypeUnsupportedMessage:
case TSInfoMessageAddToContactsOffer:
case TSInfoMessageAddUserToProfileWhitelistOffer:
case TSInfoMessageAddGroupToProfileWhitelistOffer:
case TSInfoMessageTypeGroupUpdate:
case TSInfoMessageTypeGroupQuit:
return nil;
case TSInfoMessageTypeDisappearingMessagesUpdate: {
BOOL areDisappearingMessagesEnabled = YES;
if ([interaction isKindOfClass:[OWSDisappearingConfigurationUpdateInfoMessage class]]) {
areDisappearingMessagesEnabled
= ((OWSDisappearingConfigurationUpdateInfoMessage *)interaction).configurationIsEnabled;
} else {
OWSFailDebug(@"unexpected interaction type: %@", interaction.class);
}
result = (areDisappearingMessagesEnabled
? [UIImage imageNamed:@"system_message_disappearing_messages"]
: [UIImage imageNamed:@"system_message_disappearing_messages_disabled"]);
break;
}
}
} else {
OWSFailDebug(@"Unknown interaction type: %@", [interaction class]);
return nil;
}
OWSAssertDebug(result);
return result;
}
- (void)applyTitleForInteraction:(TSInteraction *)interaction
label:(UILabel *)label
{
OWSAssertDebug(interaction);
OWSAssertDebug(label);
OWSAssertDebug(self.viewItem.systemMessageText.length > 0);
[self configureFonts];
label.text = self.viewItem.systemMessageText;
}
- (CGFloat)topVMargin
{
return LKValues.smallSpacing;
}
- (CGFloat)bottomVMargin
{
return LKValues.smallSpacing;
}
- (CGFloat)hSpacing
{
return LKValues.mediumSpacing;
}
- (CGFloat)iconSize
{
return 20.f;
}
- (CGSize)titleSize
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.viewItem);
CGFloat maxTitleWidth = (CGFloat)floor(self.conversationStyle.fullWidthContentWidth);
return [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)];
}
- (CGSize)cellSize
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.viewItem);
TSInteraction *interaction = self.viewItem.interaction;
CGSize result = CGSizeMake(self.conversationStyle.viewWidth, 0);
if (self.viewItem.hasCellHeader) {
result.height +=
[self.headerView measureWithConversationViewItem:self.viewItem conversationStyle:self.conversationStyle]
.height;
}
UIImage *_Nullable icon = [self iconForInteraction:interaction];
if (icon) {
result.height += self.iconSize + self.iconVSpacing;
}
[self applyTitleForInteraction:interaction label:self.titleLabel];
CGSize titleSize = [self titleSize];
result.height += titleSize.height;
SystemMessageAction *_Nullable action = [self actionForInteraction:interaction];
if (action) {
result.height += self.buttonHeight + self.buttonVSpacing;
}
result.height += self.topVMargin + self.bottomVMargin;
return result;
}
#pragma mark - Actions
- (nullable SystemMessageAction *)actionForInteraction:(TSInteraction *)interaction
{
OWSAssertIsOnMainThread();
OWSAssertDebug(interaction);
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
return [self actionForErrorMessage:(TSErrorMessage *)interaction];
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
return [self actionForInfoMessage:(TSInfoMessage *)interaction];
} else {
OWSFailDebug(@"Tap for system messages of unknown type: %@", [interaction class]);
return nil;
}
}
- (nullable SystemMessageAction *)actionForErrorMessage:(TSErrorMessage *)message
{
OWSAssertDebug(message);
__weak OWSSystemMessageCell *weakSelf = self;
switch (message.errorType) {
case TSErrorMessageInvalidKeyException:
return nil;
case TSErrorMessageMissingKeyId:
case TSErrorMessageNoSession:
case TSErrorMessageInvalidMessage:
return nil;
case TSErrorMessageDuplicateMessage:
case TSErrorMessageInvalidVersion:
return nil;
case TSErrorMessageUnknownContactBlockOffer:
OWSFailDebug(@"TSErrorMessageUnknownContactBlockOffer");
return nil;
case TSErrorMessageGroupCreationFailed:
return [SystemMessageAction actionWithTitle:CommonStrings.retryButton
block:^{
[weakSelf.delegate resendGroupUpdateForErrorMessage:message];
}];
}
OWSLogWarn(@"Unhandled tap for error message:%@", message);
return nil;
}
- (nullable SystemMessageAction *)actionForInfoMessage:(TSInfoMessage *)message
{
OWSAssertDebug(message);
__weak OWSSystemMessageCell *weakSelf = self;
switch (message.messageType) {
case TSInfoMessageUserNotRegistered:
case TSInfoMessageTypeSessionDidEnd:
return nil;
case TSInfoMessageTypeUnsupportedMessage:
// Unused.
return nil;
case TSInfoMessageAddToContactsOffer:
// Unused.
OWSFailDebug(@"TSInfoMessageAddToContactsOffer");
return nil;
case TSInfoMessageAddUserToProfileWhitelistOffer:
// Unused.
OWSFailDebug(@"TSInfoMessageAddUserToProfileWhitelistOffer");
return nil;
case TSInfoMessageAddGroupToProfileWhitelistOffer:
// Unused.
OWSFailDebug(@"TSInfoMessageAddGroupToProfileWhitelistOffer");
return nil;
case TSInfoMessageTypeGroupUpdate:
return nil;
case TSInfoMessageTypeGroupQuit:
return nil;
case TSInfoMessageTypeDisappearingMessagesUpdate:
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_TAP_TO_CHANGE",
@"Label for button that opens conversation settings.")
block:^{
[weakSelf.delegate showConversationSettings];
}];
}
OWSLogInfo(@"Unhandled tap for info message: %@", message);
return nil;
}
#pragma mark - Events
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
{
OWSAssertDebug(self.delegate);
if ([self isGestureInCellHeader:longPress]) {
return;
}
TSInteraction *interaction = self.viewItem.interaction;
OWSAssertDebug(interaction);
if (longPress.state == UIGestureRecognizerStateBegan) {
[self.delegate conversationCell:self didLongpressSystemMessageViewItem:self.viewItem];
}
}
- (BOOL)isGestureInCellHeader:(UIGestureRecognizer *)sender
{
OWSAssertDebug(self.viewItem);
if (!self.viewItem.hasCellHeader) {
return NO;
}
CGPoint location = [sender locationInView:self];
CGPoint headerBottom = [self convertPoint:CGPointMake(0, self.headerView.height) fromView:self.headerView];
return location.y <= headerBottom.y;
}
- (void)buttonWasPressed:(id)sender
{
if (!self.action.block) {
OWSFailDebug(@"Missing action");
} else {
self.action.block();
}
}
#pragma mark - Reuse
- (void)prepareForReuse
{
[super prepareForReuse];
self.action = nil;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,98 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
protocol QuotedReplyPreviewDelegate: class {
func quotedReplyPreviewDidPressCancel(_ preview: QuotedReplyPreview)
}
@objc
class QuotedReplyPreview: UIView, OWSQuotedMessageViewDelegate {
@objc
public weak var delegate: QuotedReplyPreviewDelegate?
private let quotedReply: OWSQuotedReplyModel
private let conversationStyle: ConversationStyle
private var quotedMessageView: OWSQuotedMessageView?
private var heightConstraint: NSLayoutConstraint!
@available(*, unavailable, message:"use other constructor instead.")
required init(coder aDecoder: NSCoder) {
notImplemented()
}
@available(*, unavailable, message:"use other constructor instead.")
override init(frame: CGRect) {
notImplemented()
}
@objc
init(quotedReply: OWSQuotedReplyModel, conversationStyle: ConversationStyle) {
self.quotedReply = quotedReply
self.conversationStyle = conversationStyle
super.init(frame: .zero)
self.heightConstraint = self.autoSetDimension(.height, toSize: 0)
updateContents()
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
}
private let draftMarginTop: CGFloat = 6
func updateContents() {
subviews.forEach { $0.removeFromSuperview() }
let hMargin: CGFloat = 6
self.layoutMargins = UIEdgeInsets(top: draftMarginTop,
left: hMargin,
bottom: 0,
right: hMargin)
// We instantiate quotedMessageView late to ensure that it is updated
// every time contentSizeCategoryDidChange (i.e. when dynamic type
// sizes changes).
let quotedMessageView = OWSQuotedMessageView(forPreview: quotedReply, conversationStyle: conversationStyle)
quotedMessageView.delegate = self
self.quotedMessageView = quotedMessageView
quotedMessageView.setContentHuggingHorizontalLow()
quotedMessageView.setCompressionResistanceHorizontalLow()
quotedMessageView.backgroundColor = .clear
self.addSubview(quotedMessageView)
quotedMessageView.ows_autoPinToSuperviewMargins()
updateHeight()
}
// MARK: Sizing
func updateHeight() {
guard let quotedMessageView = quotedMessageView else {
owsFailDebug("missing quotedMessageView")
return
}
let size = quotedMessageView.size(forMaxWidth: CGFloat.infinity)
self.heightConstraint.constant = size.height + draftMarginTop
}
@objc func contentSizeCategoryDidChange(_ notification: Notification) {
Logger.debug("")
updateContents()
}
// MARK: - OWSQuotedMessageViewDelegate
@objc public func didTapQuotedReply(_ quotedReply: OWSQuotedReplyModel, failedThumbnailDownloadAttachmentPointer attachmentPointer: TSAttachmentPointer) {
// Do nothing.
}
@objc public func didCancelQuotedReply() {
self.delegate?.quotedReplyPreviewDidPressCancel(self)
}
}

View File

@ -1,149 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc(OWSTypingIndicatorCell)
public class TypingIndicatorCell: ConversationViewCell {
@objc
public static let cellReuseIdentifier = "TypingIndicatorCell"
@available(*, unavailable, message:"use other constructor instead.")
@objc
public required init(coder aDecoder: NSCoder) {
notImplemented()
}
private let kAvatarSize: CGFloat = 36
private let kAvatarHSpacing: CGFloat = 8
// private let avatarView = AvatarImageView()
private let bubbleView = OWSBubbleView()
private let typingIndicatorView = TypingIndicatorView()
private var viewConstraints = [NSLayoutConstraint]()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
self.layoutMargins = .zero
self.contentView.layoutMargins = .zero
bubbleView.layoutMargins = .zero
bubbleView.addSubview(typingIndicatorView)
contentView.addSubview(bubbleView)
// avatarView.autoSetDimension(.width, toSize: kAvatarSize)
// avatarView.autoSetDimension(.height, toSize: kAvatarSize)
}
@objc
public override func loadForDisplay() {
guard let conversationStyle = self.conversationStyle else {
owsFailDebug("Missing conversationStyle")
return
}
bubbleView.bubbleColor = conversationStyle.bubbleColor(isIncoming: true)
typingIndicatorView.startAnimation()
viewConstraints.append(contentsOf: [
bubbleView.autoPinEdge(toSuperviewEdge: .leading, withInset: conversationStyle.gutterLeading),
bubbleView.autoPinEdge(toSuperviewEdge: .trailing, withInset: conversationStyle.gutterTrailing, relation: .greaterThanOrEqual),
bubbleView.autoPinTopToSuperviewMargin(withInset: 0),
bubbleView.autoPinBottomToSuperviewMargin(withInset: 0),
typingIndicatorView.autoPinEdge(toSuperviewEdge: .leading, withInset: conversationStyle.textInsetHorizontal),
typingIndicatorView.autoPinEdge(toSuperviewEdge: .trailing, withInset: conversationStyle.textInsetHorizontal),
typingIndicatorView.autoPinTopToSuperviewMargin(withInset: conversationStyle.textInsetTop),
typingIndicatorView.autoPinBottomToSuperviewMargin(withInset: conversationStyle.textInsetBottom)
])
// if let avatarView = configureAvatarView() {
// contentView.addSubview(avatarView)
// viewConstraints.append(contentsOf: [
// bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
// bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
// ])
//
// } else {
// avatarView.removeFromSuperview()
// }
}
private func configureAvatarView() -> UIView? {
guard let viewItem = self.viewItem else {
owsFailDebug("Missing viewItem")
return nil
}
guard let typingIndicators = viewItem.interaction as? TypingIndicatorInteraction else {
owsFailDebug("Missing typingIndicators")
return nil
}
guard shouldShowAvatar() else {
return nil
}
guard let colorName = viewItem.authorConversationColorName else {
owsFailDebug("Missing authorConversationColorName")
return nil
}
// guard let authorAvatarImage =
// OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
// colorName: ConversationColorName(rawValue: colorName),
// diameter: UInt(kAvatarSize)).build() else {
// owsFailDebug("Could build avatar image")
// return nil
// }
// avatarView.image = authorAvatarImage
// return avatarView
return UIView()
}
private func shouldShowAvatar() -> Bool {
guard let viewItem = self.viewItem else {
owsFailDebug("Missing viewItem")
return false
}
return viewItem.isGroupThread
}
@objc
public override func cellSize() -> CGSize {
guard let conversationStyle = self.conversationStyle else {
owsFailDebug("Missing conversationStyle")
return .zero
}
let insetsSize = CGSize(width: conversationStyle.textInsetHorizontal * 2,
height: conversationStyle.textInsetTop + conversationStyle.textInsetBottom)
let typingIndicatorSize = typingIndicatorView.sizeThatFits(.zero)
let bubbleSize = CGSizeAdd(insetsSize, typingIndicatorSize)
if shouldShowAvatar() {
return CGSizeCeil(CGSize(width: kAvatarSize + kAvatarHSpacing + bubbleSize.width,
height: max(kAvatarSize, bubbleSize.height)))
} else {
return CGSizeCeil(CGSize(width: bubbleSize.width,
height: max(kAvatarSize, bubbleSize.height)))
}
}
@objc
public override func prepareForReuse() {
super.prepareForReuse()
NSLayoutConstraint.deactivate(viewConstraints)
viewConstraints = [NSLayoutConstraint]()
// avatarView.image = nil
// avatarView.removeFromSuperview()
typingIndicatorView.stopAnimation()
}
}

View File

@ -1,214 +0,0 @@
import Accelerate
import NVActivityIndicatorView
@objc(LKVoiceMessageView)
final class VoiceMessageView : UIView {
private let voiceMessage: TSAttachment
private let isOutgoing: Bool
private var isLoading = false
private var isForcedAnimation = false
private var volumeSamples: [Float] = [] { didSet { updateShapeLayers() } }
@objc var progress: CGFloat = 0 { didSet { updateShapeLayers() } }
@objc var duration: Int = 0 { didSet { updateDurationLabel() } }
@objc var isPlaying = false { didSet { updateToggleImageView() } }
// MARK: Components
private lazy var toggleImageView = UIImageView(image: UIImage(named: "Play"))
private lazy var spinner = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: .black, padding: nil)
private lazy var durationLabel: UILabel = {
let result = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: Values.mediumFontSize)
return result
}()
private lazy var backgroundShapeLayer: CAShapeLayer = {
let result = CAShapeLayer()
result.fillColor = Colors.text.cgColor
return result
}()
private lazy var foregroundShapeLayer: CAShapeLayer = {
let result = CAShapeLayer()
result.fillColor = (isLightMode && isOutgoing) ? UIColor.white.cgColor : Colors.accent.cgColor
return result
}()
// MARK: Settings
private let leadingInset: CGFloat = 0
private let sampleSpacing: CGFloat = 1
private let targetSampleCount = 48
private let toggleContainerSize: CGFloat = 32
private let vMargin: CGFloat = 0
@objc public static let contentHeight: CGFloat = 40
// MARK: Initialization
@objc(initWithVoiceMessage:isOutgoing:)
init(voiceMessage: TSAttachment, isOutgoing: Bool) {
self.voiceMessage = voiceMessage
self.isOutgoing = isOutgoing
super.init(frame: CGRect.zero)
}
override init(frame: CGRect) {
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(voiceMessage:associatedWith:) instead.")
}
@objc func initialize() {
setUpViewHierarchy()
if voiceMessage.isDownloaded {
guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
return SNLog("Couldn't get URL for voice message.")
}
if let cachedVolumeSamples = Storage.shared.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
self.hideLoader()
self.volumeSamples = cachedVolumeSamples
} else {
let voiceMessageID = voiceMessage.uniqueId!
AudioUtilities.getVolumeSamples(for: url, targetSampleCount: targetSampleCount).done(on: DispatchQueue.main) { [weak self] volumeSamples in
guard let self = self else { return }
self.hideLoader()
self.isForcedAnimation = true
self.volumeSamples = volumeSamples
Storage.write { transaction in
Storage.shared.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
}
}.catch(on: DispatchQueue.main) { error in
SNLog("Couldn't sample audio file due to error: \(error).")
}
}
} else {
showLoader()
}
}
private func setUpViewHierarchy() {
set(.width, to: 200)
set(.height, to: VoiceMessageView.contentHeight)
layer.insertSublayer(backgroundShapeLayer, at: 0)
layer.insertSublayer(foregroundShapeLayer, at: 1)
let toggleContainer = UIView()
toggleContainer.clipsToBounds = false
toggleContainer.addSubview(toggleImageView)
toggleImageView.set(.width, to: 12)
toggleImageView.set(.height, to: 12)
toggleImageView.center(in: toggleContainer)
toggleContainer.addSubview(spinner)
spinner.set(.width, to: 24)
spinner.set(.height, to: 24)
spinner.center(in: toggleContainer)
toggleContainer.set(.width, to: toggleContainerSize)
toggleContainer.set(.height, to: toggleContainerSize)
toggleContainer.layer.cornerRadius = toggleContainerSize / 2
toggleContainer.backgroundColor = UIColor.white
let glowRadius: CGFloat = isLightMode ? 1 : 2
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
let glowConfiguration = UIView.CircularGlowConfiguration(size: toggleContainerSize, color: glowColor, radius: glowRadius)
toggleContainer.setCircularGlow(with: glowConfiguration)
addSubview(toggleContainer)
toggleContainer.center(.vertical, in: self)
toggleContainer.pin(.leading, to: .leading, of: self, withInset: leadingInset)
addSubview(durationLabel)
durationLabel.center(.vertical, in: self)
durationLabel.pin(.trailing, to: .trailing, of: self)
}
// MARK: UI & Updating
private func showLoader() {
isLoading = true
toggleImageView.isHidden = true
spinner.startAnimating()
spinner.isHidden = false
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] timer in
guard let self = self else { return timer.invalidate() }
if self.isLoading {
self.updateFakeVolumeSamples()
} else {
timer.invalidate()
}
}
updateFakeVolumeSamples()
}
private func updateFakeVolumeSamples() {
let fakeVolumeSamples = (0..<targetSampleCount).map { _ in Float.random(in: 0...1) }
volumeSamples = fakeVolumeSamples
}
private func hideLoader() {
isLoading = false
toggleImageView.isHidden = false
spinner.stopAnimating()
spinner.isHidden = true
}
override func layoutSubviews() {
super.layoutSubviews()
updateShapeLayers()
}
private func updateShapeLayers() {
clipsToBounds = false // Bit of a hack to do this here, but the containing stack view turns this off all the time
guard !volumeSamples.isEmpty else { return }
let sMin = CGFloat(volumeSamples.min()!)
let sMax = CGFloat(volumeSamples.max()!)
let w = width() - leadingInset - toggleContainerSize - durationLabel.width() - 2 * Values.smallSpacing
let h = height() - 2 * vMargin
let sW = (w - sampleSpacing * CGFloat(volumeSamples.count - 1)) / CGFloat(volumeSamples.count)
let backgroundPath = UIBezierPath()
let foregroundPath = UIBezierPath()
for (i, value) in volumeSamples.enumerated() {
let x = leadingInset + toggleContainerSize + Values.smallSpacing + CGFloat(i) * (sW + sampleSpacing)
let fraction = (CGFloat(value) - sMin) / (sMax - sMin)
let sH = max(8, h * fraction)
let y = vMargin + (h - sH) / 2
let subPath = UIBezierPath(roundedRect: CGRect(x: x, y: y, width: sW, height: sH), cornerRadius: sW / 2)
backgroundPath.append(subPath)
if progress > CGFloat(i) / CGFloat(volumeSamples.count) { foregroundPath.append(subPath) }
}
backgroundPath.close()
foregroundPath.close()
if isLoading || isForcedAnimation {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 0.25
animation.toValue = backgroundPath
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
backgroundShapeLayer.add(animation, forKey: "path")
backgroundShapeLayer.path = backgroundPath.cgPath
} else {
backgroundShapeLayer.path = backgroundPath.cgPath
}
foregroundShapeLayer.path = foregroundPath.cgPath
isForcedAnimation = false
}
private func updateDurationLabel() {
durationLabel.text = OWSFormat.formatDurationSeconds(duration)
updateShapeLayers()
}
private func updateToggleImageView() {
toggleImageView.image = isPlaying ? #imageLiteral(resourceName: "Pause") : #imageLiteral(resourceName: "Play")
}
// MARK: Interaction
@objc(getCurrentTime:)
func getCurrentTime(for panGestureRecognizer: UIPanGestureRecognizer) -> TimeInterval {
guard voiceMessage.isDownloaded else { return 0 }
let locationInSelf = panGestureRecognizer.location(in: self)
let waveformFrameOrigin = CGPoint(x: leadingInset + toggleContainerSize + Values.smallSpacing, y: vMargin)
let waveformFrameSize = CGSize(width: width() - leadingInset - toggleContainerSize - durationLabel.width() - 2 * Values.smallSpacing,
height: height() - 2 * vMargin)
let waveformFrame = CGRect(origin: waveformFrameOrigin, size: waveformFrameSize)
guard waveformFrame.contains(locationInSelf) else { return 0 }
let fraction = (locationInSelf.x - waveformFrame.minX) / (waveformFrame.maxX - waveformFrame.minX)
return Double(fraction) * Double(duration)
}
}

View File

@ -1,80 +0,0 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class VoiceMemoLockView: UIView {
private var offsetConstraint: NSLayoutConstraint!
private let offsetFromToolbar: CGFloat = 40
private let backgroundViewInitialHeight: CGFloat = 80
private var chevronTravel: CGFloat {
return -1 * (backgroundViewInitialHeight - 50)
}
@objc
public override init(frame: CGRect) {
super.init(frame: frame)
addSubview(backgroundView)
backgroundView.addSubview(lockIconView)
backgroundView.addSubview(chevronView)
layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: offsetFromToolbar, trailing: 0)
backgroundView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
self.offsetConstraint = backgroundView.autoPinEdge(toSuperviewMargin: .bottom)
// we anchor the top so that the bottom "slides up" to meet it as the user slides the lock
backgroundView.autoPinEdge(.top, to: .bottom, of: self, withOffset: -offsetFromToolbar - backgroundViewInitialHeight)
backgroundView.layoutMargins = UIEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)
lockIconView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
chevronView.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: -
@objc
public func update(ratioComplete: CGFloat) {
offsetConstraint.constant = CGFloatLerp(0, chevronTravel, ratioComplete)
}
// MARK: - Subviews
private lazy var lockIconView: UIImageView = {
let imageTemplate = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate)
let imageView = UIImageView(image: imageTemplate)
imageView.tintColor = Colors.destructive
imageView.autoSetDimensions(to: CGSize(width: 24, height: 24))
return imageView
}()
private lazy var chevronView: UIView = {
let label = UILabel()
label.text = "\u{2303}"
label.textColor = Colors.destructive
label.textAlignment = .center
return label
}()
private lazy var backgroundView: UIView = {
let view = UIView()
let width: CGFloat = 36
view.autoSetDimension(.width, toSize: width)
view.backgroundColor = Colors.composeViewBackground
view.layer.cornerRadius = width / 2
view.layer.borderColor = Colors.text.withAlphaComponent(Values.lowOpacity).cgColor
view.layer.borderWidth = Values.composeViewTextFieldBorderThickness
return view
}()
}

View File

@ -3,7 +3,7 @@
// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for
// more information on database handling.
final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewControllerPreviewingDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate {
final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate {
private var threads: YapDatabaseViewMappings!
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
private var tableViewTopConstraint: NSLayoutConstraint!
@ -124,10 +124,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewC
view.addSubview(newConversationButtonSet)
newConversationButtonSet.center(.horizontal, in: view)
newConversationButtonSet.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) // Negative due to how the constraint is set up
// Previewing
if traitCollection.forceTouchCapability == .available {
registerForPreviewing(with: self, sourceView: tableView)
}
// Notifications
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject)
@ -295,21 +291,6 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewC
present(navigationController, animated: true, completion: nil)
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
guard let indexPath = tableView.indexPathForRow(at: location), let thread = self.thread(at: indexPath.row) else { return nil }
previewingContext.sourceRect = tableView.rectForRow(at: indexPath)
let conversationVC = ConversationViewController()
conversationVC.configure(for: thread, action: .none, focusMessageId: nil)
conversationVC.peekSetup()
return conversationVC
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
guard let conversationVC = viewControllerToCommit as? ConversationViewController else { return }
conversationVC.popped()
navigationController?.pushViewController(conversationVC, animated: false)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let thread = self.thread(at: indexPath.row) else { return }
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
@ -321,15 +302,8 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIViewC
if let presentedVC = self.presentedViewController {
presentedVC.dismiss(animated: false, completion: nil)
}
let displayName = OWSProfileManager.shared().profileNameForRecipient(withID: getUserHexEncodedPublicKey())
if displayName == "Brendan" && 0.4 < Double.random(in: 0...1) { // Show Brendan the old screen approx 1 out of 5 times to mess with him
let conversationVC = ConversationViewController()
conversationVC.configure(for: thread, action: action, focusMessageId: highlightedMessageID)
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
} else {
let conversationVC = ConversationVC(thread: thread)
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
}
let conversationVC = ConversationVC(thread: thread)
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
}
}

View File

@ -4,9 +4,7 @@
#import "MediaDetailViewController.h"
#import "AttachmentSharing.h"
#import "ConversationViewController.h"
#import "ConversationViewItem.h"
#import "OWSMessageCell.h"
#import "Session-Swift.h"
#import "TSAttachmentStream.h"
#import "TSInteraction.h"

View File

@ -454,7 +454,7 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
detailView.backgroundColor = .clear
navigationController.view.backgroundColor = .clear
navigationController.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius_Small
navigationController.presentationView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
fromViewController.present(navigationController, animated: false) {
@ -635,7 +635,7 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
if changedItems {
self.navigationController.presentationView.alpha = 0
} else {
self.navigationController.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius_Small
self.navigationController.presentationView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius
}
})

View File

@ -584,11 +584,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
self.uiDatabaseConnection.read { transaction in
let message = galleryItem.message
let thread = message.thread(with: transaction)
let conversationStyle = ConversationStyle(thread: thread)
fetchedItem = ConversationInteractionViewItem(interaction: message,
isGroupThread: thread.isGroupThread(),
transaction: transaction,
conversationStyle: conversationStyle)
transaction: transaction)
}
guard let viewItem = fetchedItem else {

View File

@ -8,12 +8,10 @@
// Separate iOS Frameworks from other imports.
#import "AppDelegate.h"
#import "AVAudioSession+OWS.h"
#import "AttachmentUploadView.h"
#import "AvatarViewHelper.h"
#import "AVAudioSession+OWS.h"
#import "ContactCellView.h"
#import "ContactTableViewCell.h"
#import "ConversationViewCell.h"
#import "ConversationViewItem.h"
#import "ConversationViewModel.h"
#import "DateUtil.h"
@ -24,16 +22,11 @@
#import "OWSBackup.h"
#import "OWSBackupIO.h"
#import "OWSBezierPathView.h"
#import "OWSBubbleShapeView.h"
#import "OWSBubbleView.h"
#import "OWSConversationSettingsViewController.h"
#import "OWSDatabaseMigration.h"
#import "OWSMessageBubbleView.h"
#import "OWSMessageCell.h"
#import "OWSMessageTimerView.h"
#import "OWSNavigationController.h"
#import "OWSProgressView.h"
#import "OWSQuotedMessageView.h"
#import "OWSWindowManager.h"
#import "PrivacySettingsTableViewController.h"
#import "OWSQRCodeScanningViewController.h"

View File

@ -2,7 +2,7 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewController.h"
#import "ConversationViewAction.h"
NS_ASSUME_NONNULL_BEGIN

View File

@ -4,7 +4,6 @@
#import "SignalApp.h"
#import "AppDelegate.h"
#import "ConversationViewController.h"
#import "Session-Swift.h"
#import <SignalCoreKit/Threading.h>
#import <SignalUtilitiesKit/DebugLogger.h>
@ -105,16 +104,6 @@ NS_ASSUME_NONNULL_BEGIN
}
DispatchMainThreadSafe(^{
UIViewController *frontmostVC = [[UIApplication sharedApplication] frontmostViewController];
if ([frontmostVC isKindOfClass:[ConversationViewController class]]) {
ConversationViewController *conversationVC = (ConversationViewController *)frontmostVC;
if ([conversationVC.thread.uniqueId isEqualToString:thread.uniqueId]) {
[conversationVC popKeyBoard];
return;
}
}
[self.homeViewController show:thread with:action highlightedMessageID:focusMessageId animated:isAnimated];
});
}
@ -133,16 +122,6 @@ NS_ASSUME_NONNULL_BEGIN
}
DispatchMainThreadSafe(^{
UIViewController *frontmostVC = [[UIApplication sharedApplication] frontmostViewController];
if ([frontmostVC isKindOfClass:[ConversationViewController class]]) {
ConversationViewController *conversationVC = (ConversationViewController *)frontmostVC;
if ([conversationVC.thread.uniqueId isEqualToString:thread.uniqueId]) {
[conversationVC scrollToFirstUnreadMessage:isAnimated];
return;
}
}
[self.homeViewController show:thread with:ConversationViewActionNone highlightedMessageID:nil animated:isAnimated];
});
}

View File

@ -188,7 +188,7 @@ extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee {
return true
}
guard let conversationViewController = UIApplication.shared.frontmostViewController as? ConversationViewController else {
guard let conversationViewController = UIApplication.shared.frontmostViewController as? ConversationVC else {
return true
}