Remove old conversation screen
This commit is contained in:
parent
af05f876d4
commit
49c825eb43
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
@import Foundation;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
|
||||
ConversationViewActionNone,
|
||||
ConversationViewActionCompose,
|
||||
ConversationViewActionAudioCall,
|
||||
ConversationViewActionVideoCall,
|
||||
};
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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 -
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -2,7 +2,6 @@ import UIKit
|
|||
|
||||
class MessageCell : UITableViewCell {
|
||||
var delegate: MessageCellDelegate?
|
||||
var conversationStyle: ConversationStyle?
|
||||
var viewItem: ConversationViewItem? { didSet { update() } }
|
||||
|
||||
// MARK: Settings
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "OWSMessageTimerView.h"
|
||||
#import "ConversationViewController.h"
|
||||
#import "OWSMath.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SelectRecipientViewController.h"
|
||||
|
||||
@interface AddToBlockListViewController : SelectRecipientViewController
|
||||
|
||||
@end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 ]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
*/
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}()
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewController.h"
|
||||
#import "ConversationViewAction.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue